diff --git a/docs/code-standards.md b/docs/code-standards.md new file mode 100644 index 0000000..a4e21b7 --- /dev/null +++ b/docs/code-standards.md @@ -0,0 +1,795 @@ +# Code Standards & Guidelines + +## Core Principles + +**YAGNI** — You Aren't Gonna Need It (don't add features not in spec) +**KISS** — Keep It Simple, Stupid (prefer straightforward solutions) +**DRY** — Don't Repeat Yourself (extract common patterns) + +All code must be: +- **Readable** — Clear naming, logical structure, self-documenting +- **Maintainable** — Under 200 lines per file, single responsibility +- **Testable** — Isolated logic, mockable dependencies +- **Documented** — Public APIs have Javadoc/JSDoc + +--- + +## Java Code Standards + +### Package Naming + +``` +org.nico.ratel.landlords.{component}.{subcomponent} +``` + +**Examples:** +- `org.nico.ratel.landlords.entity` — Data models +- `org.nico.ratel.landlords.helper` — Game logic & utilities +- `org.nico.ratel.landlords.server.event` — Server event handlers +- `org.nico.ratel.landlords.client.proxy` — Client network layer + +### Class Naming + +**Style:** PascalCase + +**Patterns:** +- `FooBar` — Regular classes +- `FooBarListener` — Event listeners +- `FooBarHandler` — Protocol/network handlers +- `FooBarProxy` — Message sending proxies +- `FooBarTest` — Unit test classes +- `FooBarImpl` — Concrete implementations (rare) +- `AbstractFooBar` — Base classes + +**Examples:** +```java +// Good +public class Board { ... } +public class ServerEventListener_CODE_GAME_MOVE { ... } +public class GomokuHelper { ... } + +// Avoid +public class board { ... } // lowercase +public class GameMoveListener { ... } // unclear purpose +``` + +### File Organization + +**One public class per file:** +``` +ClassName.java +├── Package declaration +├── Imports (alphabetical) +├── Class declaration +├── Constants (static final) +├── Instance fields (private) +├── Constructor(s) +├── Public methods +├── Package-private methods +└── Private methods +``` + +**Max file size:** 200 lines (including comments & blanks) + +**If exceeding 200 lines:** Split into multiple classes or extract to utility class. + +### Method Naming + +**Style:** camelCase, verb-first + +**Patterns:** +```java +// Getters +public String getName() { ... } +public boolean isValid() { ... } +public List getRooms() { ... } + +// Setters +public void setName(String name) { ... } + +// Predicates +public boolean isValidMove(int row, int col) { ... } +public boolean canWin(Board board, int row, int col) { ... } + +// Executors +public boolean makeMove(int row, int col, PieceType piece) { ... } +public void execute(ClientTransferData data) { ... } + +// Factory/Builder +public static Board create() { ... } +public GameMove build() { ... } + +// Converter/Parser +public static GameMove fromJson(String json) { ... } +public String toJson() { ... } +``` + +### Variable Naming + +**Style:** camelCase, noun-first + +**Rules:** +- Single letter only for loop counters: `for (int i = 0; i < size; i++)` +- Prefer descriptive names over abbreviations +- Boolean variables start with `is`, `has`, `can`, `should` + +**Examples:** +```java +// Good +int moveCount = 0; +String playerNickname = "Alice"; +boolean isValidMove = true; +List moveHistory = new ArrayList<>(); +PieceType[][] board = new PieceType[15][15]; + +// Avoid +int count = 0; // unclear what's being counted +String name = "Alice"; // unclear which name +boolean valid = true; // unclear what's valid +List moves = new ArrayList(); // raw type +int b[][] = ... // unclear purpose +``` + +### Constants + +**Style:** UPPER_SNAKE_CASE, always `static final` + +**Examples:** +```java +public static final int BOARD_SIZE = 15; +public static final int WIN_CONDITION = 5; +public static final long HEARTBEAT_INTERVAL_MS = 30_000; +private static final String RESOURCE_PATH = "static/"; +``` + +### Comments & Documentation + +**Javadoc:** Required for all public classes, methods, fields + +```java +/** + * Checks if placing a piece at (row, col) results in a win. + * + * @param board The game board state + * @param row Row index (0-14) + * @param col Column index (0-14) + * @param piece The piece type (BLACK or WHITE) + * @return true if this move wins the game, false otherwise + */ +public static boolean canWin(Board board, int row, int col, PieceType piece) { + // ... +} +``` + +**Inline Comments:** Document WHY, not WHAT + +```java +// Bad +i++; // increment i + +// Good +moveCount++; // Move count used to detect draw condition (full board = 225) + +// Good +if (moveCount >= BOARD_SIZE * BOARD_SIZE) { // 225 moves = draw + result = GameResult.DRAW; +} +``` + +### Imports + +**Rules:** +- No wildcard imports (`import java.util.*`) +- Alphabetical order +- Group by package (java, javax, third-party, org.nico...) + +```java +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; + +import org.nico.ratel.landlords.entity.Board; +import org.nico.ratel.landlords.enums.PieceType; +``` + +### Error Handling + +**All errors must be caught and handled:** + +```java +// Good +try { + int row = Integer.parseInt(input.split(",")[0]); + int col = Integer.parseInt(input.split(",")[1]); + if (!board.isValidMove(row, col)) { + sendError(ctx, "Invalid move"); + return; + } +} catch (NumberFormatException e) { + sendError(ctx, "Invalid format, expected 'row,col'"); + return; +} catch (ArrayIndexOutOfBoundsException e) { + sendError(ctx, "Missing row or column"); + return; +} + +// Avoid +int row = Integer.parseInt(input.split(",")[0]); // Uncaught exceptions +``` + +**Never silently swallow exceptions:** + +```java +// Bad +try { + doSomething(); +} catch (Exception e) { + // Silent failure +} + +// Good +try { + doSomething(); +} catch (IOException e) { + logger.error("Failed to do something", e); + throw new RuntimeException("Unrecoverable error", e); +} +``` + +### Type Safety + +**Use specific types, not Object:** + +```java +// Bad +Object room = new Room(); +Object move = new GameMove(7, 7, BLACK); + +// Good +Room room = new Room(); +GameMove move = new GameMove(7, 7, PieceType.BLACK); +``` + +**Use enums, not strings/ints for fixed values:** + +```java +// Bad +String roomType = "PVP"; // Could be "pvp", "pve", "PvP", etc. +int difficulty = 2; // What does 2 mean? + +// Good +RoomType roomType = RoomType.PVP; +int difficulty = 2; // Medium (clear from context or const) +``` + +--- + +## JavaScript Code Standards + +### Module System + +**Style:** ES modules (import/export) + +```javascript +// Export +export function getNextMove(board, difficulty) { ... } +export class Board { ... } +export const BOARD_SIZE = 15; + +// Import +import { getNextMove, BOARD_SIZE } from './ai.js'; +import Board from './board.js'; +``` + +**One default export per file (when possible):** + +```javascript +// good: single responsibility +export default class GameScene extends Phaser.Scene { ... } + +// good: utility module with multiple exports +export function connectToServer(url) { ... } +export function sendMove(move) { ... } +``` + +### File Naming + +**Style:** kebab-case + +**Patterns:** +- `game-scene.js` — Phaser scene class +- `connection-service.js` — Service module +- `event-bus.js` — Utility/helper +- `protocol-constants.js` — Constant definitions +- `board.js` — Game object class + +**Examples:** +``` +// Good +game-scene.js +connection-service.js +protocol-constants.js +menu-ui.js + +// Avoid +gameScene.js // camelCase in web +game_scene.js // snake_case in web +GameScene.js // PascalCase in web +``` + +### Variable Naming + +**Style:** camelCase + +```javascript +// Good +const playerNickname = "Alice"; +let isConnected = false; +const moveHistory = []; +const gameBoard = board; + +// Avoid +const player_nickname = "Alice"; // snake_case +const PlayerNickname = "Alice"; // PascalCase +const p = "Alice"; // single letter (non-loop) +``` + +### Constants + +**Style:** camelCase (JavaScript convention) or UPPER_SNAKE_CASE (for global constants) + +```javascript +// Module-level constants +const BOARD_SIZE = 15; +const WIN_CONDITION = 5; +const HEARTBEAT_INTERVAL_MS = 30_000; + +// Local constants (same case as variables) +const maxRetries = 3; +const timeout = 5000; +``` + +### Function Naming + +**Style:** camelCase, verb-first + +```javascript +// Declarative +export function connectToServer(url) { ... } +export function sendMove(row, col) { ... } +export function parseEventCode(code) { ... } + +// Predicates +function isValidMove(row, col) { ... } +function hasWon(board, row, col) { ... } +function canReconnect() { ... } + +// Handlers (suffix with 'Handler' or 'On{Event}') +function handleConnection() { ... } +function onMoveMade(move) { ... } +``` + +### Classes & Objects + +**Style:** PascalCase for class constructors + +```javascript +// Good: Class definition +export class Board { + constructor(size = 15) { + this.size = size; + this.grid = []; + } + + placeStone(row, col, color) { ... } +} + +// Usage +const board = new Board(); + +// Good: Object literal +const config = { + type: Phaser.AUTO, + width: 800, + height: 800, +}; + +// Avoid +const board = Board(); // Should use new +``` + +### Comments & Documentation + +**JSDoc:** Required for all exports + +```javascript +/** + * Connects to the WebSocket server and establishes game communication. + * + * @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:1025/ratel') + * @returns {Promise} Promise resolving to the connected WebSocket + * @throws {Error} If connection fails or timeout occurs + */ +export function connectToServer(url) { + // ... +} + +/** + * @typedef {Object} GameMove + * @property {number} row - Row index (0-14) + * @property {number} col - Column index (0-14) + * @property {string} piece - 'BLACK' or 'WHITE' + */ + +/** + * Sends a move to the server. + * + * @param {GameMove} move - The move to send + * @returns {Promise} + */ +export async function sendMove(move) { + // ... +} +``` + +**Inline Comments:** Document non-obvious logic + +```javascript +// Good +const reconnectDelay = Math.min(1000 * Math.pow(2, retryCount), 30000); // Exponential backoff, max 30s + +// Bad +const x = Math.min(1000 * Math.pow(2, r), 30000); // No context +``` + +### Error Handling + +**Use try/catch for async operations:** + +```javascript +// Good +try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.json(); +} catch (error) { + console.error('Failed to fetch:', error); + showToast('Connection error'); + throw error; +} + +// Avoid +fetch(url).then(r => r.json()); // Unhandled rejection +``` + +### Async/Await + +**Prefer async/await over .then():** + +```javascript +// Good +async function loadBoard() { + try { + const response = await fetch('/api/board'); + const data = await response.json(); + return data; + } catch (error) { + console.error('Failed to load board:', error); + } +} + +// Avoid +function loadBoard() { + return fetch('/api/board') + .then(r => r.json()) + .catch(e => console.error(e)); +} +``` + +### Array & Object Methods + +**Use modern methods (map, filter, reduce):** + +```javascript +// Good +const evenNumbers = numbers.filter(n => n % 2 === 0); +const doubled = numbers.map(n => n * 2); +const sum = numbers.reduce((acc, n) => acc + n, 0); + +// Avoid +const evenNumbers = []; +for (let i = 0; i < numbers.length; i++) { + if (numbers[i] % 2 === 0) { + evenNumbers.push(numbers[i]); + } +} +``` + +### Destructuring + +**Use destructuring for clarity:** + +```javascript +// Good +const { row, col, piece } = move; +const [ x, y ] = coordinates; +const { code, data } = message; + +// Avoid +const row = move.row; +const col = move.col; +const piece = move.piece; +``` + +--- + +## Shared Standards + +### File Size Management + +**Java:** Keep files under 200 lines (including comments) +**JavaScript:** Keep files under 300 lines + +**When to split:** +- File approaching limit +- Multiple independent classes/functions +- Different concerns (e.g., UI + logic mixed) + +**How to split:** +- Extract to new module with clear responsibility +- Update imports in calling code +- Ensure no circular dependencies + +### Naming Conventions Summary + +| Item | Java | JavaScript | +|------|------|-----------| +| **Package/Module** | `org.nico.ratel.landlords.foo` | `foo-bar.js`, `/services/` | +| **Class** | `PascalCase` | `PascalCase` (exported) | +| **Function** | `camelCase()` | `camelCase()` | +| **Variable** | `camelCase` | `camelCase` | +| **Constant** | `UPPER_SNAKE_CASE` | `UPPER_SNAKE_CASE` or `camelCase` | +| **Boolean** | `isValid`, `hasWon`, `canMove` | `isValid`, `hasWon`, `canMove` | +| **File** | `ClassName.java` | `kebab-case.js` | + +### Git Commit Messages + +**Format:** Conventional Commits + +``` +(): + + + +