Coding Standards
This guide defines the coding standards for martini-kit SDK to ensure consistency and maintainability across the codebase.
TypeScript Standards
Strict Mode
All packages use TypeScript strict mode:
{
"compilerOptions": {
"strict": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
} What this means:
- No implicit
anytypes - Null and undefined checking enabled
- Strict property initialization
- No implicit
this
Type-First Design
Design with types before implementation:
Good:
// Define the interface first
interface GameState {
players: Record<string, Player>;
projectiles: Projectile[];
score: number;
}
// Then implement
function updateScore(state: GameState, points: number): void {
state.score += points;
} Bad:
// Implementation without clear types
function updateScore(state, points) {
state.score += points;
} Generic Constraints
Use generic constraints for type safety:
Good:
function createSpriteManager<TData extends { id: string }>(
config: SpriteManagerConfig<TData>
): SpriteManager<TData> {
// TData must have an 'id' property
} Bad:
function createSpriteManager<TData>(
config: SpriteManagerConfig<TData>
): SpriteManager<TData> {
// No constraints, less type safety
} Avoid any
Prefer unknown or specific types over any:
Good:
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
// Use with type guard
const data = parseJSON(jsonString);
if (isGameState(data)) {
// TypeScript knows data is GameState here
} Bad:
function parseJSON(json: string): any {
return JSON.parse(json);
} When any is acceptable:
- External library types you can’t control
- Truly dynamic data (with clear documentation)
- Temporary during refactoring (add
// TODO: type thiscomment)
Use readonly for Immutability
Mark properties that shouldn’t change:
Good:
interface GameRuntimeConfig {
readonly isHost: boolean;
readonly playerIds: readonly string[];
readonly seed?: number;
} Bad:
interface GameRuntimeConfig {
isHost: boolean;
playerIds: string[];
seed?: number;
} Explicit Return Types
Always specify return types for public functions:
Good:
export function createPlayer(id: string): Player {
return { id, x: 0, y: 0, health: 100 };
} Bad:
export function createPlayer(id: string) {
return { id, x: 0, y: 0, health: 100 };
} Code Style
Formatting
We use Prettier for automatic formatting:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": true,
"trailingComma": "es5",
"printWidth": 100
} Setup:
# Install Prettier
pnpm add -D prettier
# Format code
pnpm exec prettier --write . Use editor integration:
- VS Code: Install Prettier extension
- Enable “Format on Save”
Indentation
- Use tabs for indentation
- Tab width: 2 spaces
- Be consistent within a file
Quotes
- Single quotes for strings:
'hello' - Double quotes for JSX/HTML attributes:
<div class="foo"> - Template literals for string interpolation:
`Hello ${name}`
Semicolons
- Always use semicolons - Required by our Prettier config
- Prevents ASI (Automatic Semicolon Insertion) bugs
Line Length
- Max 100 characters per line
- Break long lines for readability:
// Good
const runtime = new GameRuntime(
game,
transport,
{
isHost: true,
playerIds: ['p1', 'p2'],
}
);
// Bad - too long
const runtime = new GameRuntime(game, transport, { isHost: true, playerIds: ['p1', 'p2'] }); Naming Conventions
Classes
Use PascalCase:
class GameRuntime {}
class PhaserAdapter {}
class LocalTransport {} Functions and Variables
Use camelCase:
function submitAction() {}
const playerCount = 5;
let isConnected = false; Constants
Use UPPER_SNAKE_CASE for true constants:
const DEFAULT_SYNC_RATE_MS = 50;
const MAX_PLAYERS = 8; Not for config objects:
// This is configuration, use camelCase
const defaultConfig = {
syncRateMs: 50,
maxPlayers: 8,
}; Interfaces and Types
Use PascalCase:
interface GameState {}
type ActionHandler = () => void; Interface prefix:
- Generally avoid
Iprefix - Use it only when the interface and implementation have the same name:
// Good - clear distinction
interface Transport {}
class LocalTransport implements Transport {}
// Only use I prefix when needed
interface ITransport {}
class Transport implements ITransport {} Files
Two conventions:
- PascalCase for classes:
GameRuntime.ts,PhaserAdapter.ts - kebab-case for modules:
seeded-random.ts,input-manager.ts
Test files:
GameRuntime.test.ts
input-manager.test.ts File Organization
Standard Structure
package/
├── src/
│ ├── index.ts # Public exports only
│ ├── GameRuntime.ts # Main classes (one per file)
│ ├── types.ts # Shared type definitions
│ ├── constants.ts # Constants
│ ├── helpers/ # Helper functions
│ │ ├── index.ts
│ │ ├── createPlayer.ts
│ │ └── createTick.ts
│ └── __tests__/ # Test files
│ ├── GameRuntime.test.ts
│ └── helpers.test.ts
├── dist/ # Build output (gitignored)
├── package.json
├── tsconfig.json
├── README.md
└── .gitignore Exports
index.ts should only re-export:
// Good - clean public API
export { GameRuntime } from './GameRuntime';
export { defineGame } from './defineGame';
export type { GameDefinition, GameState } from './types';
export * from './helpers'; Don’t implement in index.ts:
// Bad - implementation in index
export function defineGame() {
// ...implementation
} Import Order
Organize imports in this order:
// 1. External dependencies
import Phaser from 'phaser';
import { describe, it, expect } from 'vitest';
// 2. Internal dependencies from other packages
import { GameRuntime } from '@martini-kit/core';
import { LocalTransport } from '@martini-kit/transport-local';
// 3. Relative imports
import { Player } from './types';
import { createPlayer } from './helpers';
// 4. Type-only imports (separate)
import type { GameState } from './types'; Documentation Standards
TSDoc Comments
Use TSDoc for all public APIs:
/**
* Creates a new game runtime instance.
*
* @param game - The game definition created with defineGame()
* @param transport - The transport layer for network communication
* @param config - Runtime configuration options
* @returns A new GameRuntime instance
*
* @example
* ```typescript
* const runtime = new GameRuntime(game, transport, {
* isHost: true,
* playerIds: ['p1']
* });
* ```
*
* @see {@link GameDefinition}
* @see {@link Transport}
*/
export class GameRuntime<TState> {
constructor(
game: GameDefinition<TState>,
transport: Transport,
config: GameRuntimeConfig
) {
// ...
}
} TSDoc tags to use:
@param- Parameter description@returns- Return value description@throws- Exceptions that can be thrown@example- Usage examples@see- Links to related docs@deprecated- Mark deprecated APIs
Inline Comments
Use inline comments for complex logic:
// Calculate velocity with acceleration curve
// Using quadratic easing for smoother feel
const velocity = Math.pow(timeElapsed / duration, 2) * maxVelocity; When to comment:
- Complex algorithms
- Non-obvious optimizations
- Workarounds for bugs
- Business logic reasoning
When NOT to comment:
- Obvious code (let the code speak)
- Redundant information
- Instead of clear names
// Bad - comment states the obvious
// Increment count
count++;
// Good - clear variable name, no comment needed
playerCount++; README Files
Every package should have a README:
# @martini-kit/package-name
Brief description of what this package does.
## Installation
```bash
pnpm add @martini-kit/package-name
```
## Usage
```typescript
import { Something } from '@martini-kit/package-name';
```
## API
Link to full API docs.
## License
MIT Testing Standards
Test File Naming
- Pattern:
FileName.test.ts - Location:
src/__tests__/FileName.test.ts
Test Structure
Use Arrange-Act-Assert pattern:
import { describe, it, expect } from 'vitest';
import { GameRuntime } from '../GameRuntime';
describe('GameRuntime', () => {
describe('submitAction', () => {
it('should update state when action is submitted', () => {
// Arrange
const game = defineGame({
setup: () => ({ count: 0 }),
actions: {
increment: {
apply: (state) => { state.count++; }
}
}
});
const runtime = new GameRuntime(game, transport, config);
// Act
runtime.submitAction('increment');
// Assert
expect(runtime.getState().count).toBe(1);
});
});
}); Test Descriptions
- Use descriptive test names that explain what is being tested
- Format: “should [expected behavior] when [condition]”
Good:
it('should throw error when transport is disconnected', () => {});
it('should sync state to all clients when host updates', () => {}); Bad:
it('works', () => {});
it('test1', () => {}); Test Coverage
- Aim for >80% coverage
- Focus on critical paths
- Test edge cases
- Don’t test trivial code
Resource Cleanup
Always clean up resources:
it('should cleanup on destroy', () => {
const runtime = new GameRuntime(game, transport, config);
runtime.destroy();
// Verify cleanup
expect(runtime.isDestroyed).toBe(true);
}); Error Handling
Descriptive Errors
Provide helpful error messages:
Good:
throw new Error(
`Invalid targetId "${targetId}". Player not found in state. ` +
`Available players: ${Object.keys(state.players).join(', ')}`
); Bad:
throw new Error('Invalid player'); Error Types
Create custom error classes for specific errors:
export class TransportError extends Error {
constructor(message: string) {
super(message);
this.name = 'TransportError';
}
}
export class StateValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'StateValidationError';
}
} Error Recovery
Handle errors gracefully:
try {
transport.send(message);
} catch (error) {
logger.error('Failed to send message:', error);
// Attempt recovery
reconnect();
} Performance Best Practices
Avoid Premature Optimization
- Write clear code first
- Profile before optimizing
- Document optimization reasoning
Common Optimizations
Object pooling:
class ProjectilePool {
private pool: Projectile[] = [];
acquire(): Projectile {
return this.pool.pop() ?? new Projectile();
}
release(projectile: Projectile): void {
projectile.reset();
this.pool.push(projectile);
}
} Memoization:
const memoizedDistance = memoize((x1, y1, x2, y2) => {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}); Git Standards
Commit Messages
Follow Conventional Commits:
feat(core): add host migration support
fix(phaser): resolve sprite interpolation bug
docs(api): update GameRuntime documentation
test(core): add player lifecycle tests
refactor(transport): simplify message routing
perf(sync): optimize diff generation algorithm
chore(deps): update dependencies Branch Names
Use descriptive branch names:
feature/host-migration
fix/sprite-interpolation
docs/add-unity-guide
refactor/simplify-transport Checklist for Contributors
Before submitting a PR, ensure:
- Code follows TypeScript standards
- All functions have TSDoc comments
- Tests are written and passing
- Code is formatted with Prettier
- No TypeScript errors
- Documentation is updated
- Examples are tested
- Commit messages follow convention
- PR description is clear and complete
Questions about coding standards? Ask in GitHub Discussions!