Files
caro/docs/code-standards.md
tiennm99 cbad690565 docs,chore: single-port 1999 websocket protobuf
Phase 05 — sync infrastructure and documentation with the typed-protobuf
refactor:
- docker-compose.yml: drop 1024/1025 mappings, single "1999:1999"
- server/Dockerfile: EXPOSE 1999, -p 1999 entrypoint
- README.md: rewrite transport description, architecture diagram, protocol
  section, server options, project structure, add proto:gen script note
- docs/project-overview.md: update transport + dependencies sections
- docs/system-architecture.md: rewrite diagrams + pipeline + file inventory
  for the WebSocket-only typed-dispatch path
- docs/codebase-summary.md: refresh file tree, java package inventory,
  gradle deps, vite deps, networking and game-flow sections
- docs/deployment-guide.md: single-port walkthrough for local / docker /
  systemd / nginx; remove all 1024/1025 firewall and troubleshooting
- docs/code-standards.md: replace dead ServerEventListener_CODE_* class-name
  example, fix sample ws:// URL to port 1999
2026-04-11 08:33:46 +07:00

805 lines
19 KiB
Markdown

# 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
```
com.miti99.caro.{common|server}.{subcomponent}
```
**Examples:**
- `com.miti99.caro.common.entity` — Shared data models
- `com.miti99.caro.common.helper` — Game logic & utilities
- `com.miti99.caro.common.robot` — AI engine
- `com.miti99.caro.server.event` — Server event handlers
- `com.miti99.caro.server.handler` — Netty pipeline handlers
- `com.miti99.caro.server.proxy` — TCP/WebSocket server bootstrap
### Class Naming
**Style:** PascalCase
**Patterns:**
- `FooBar` — Regular classes
- `FooBarHandler` — Protocol/network handlers
- `FooRequest` / `FooRequestRecord` — Sealed record variants (typed requests)
- `FooTest` — Unit test classes
- `FooImpl` — Concrete implementations (rare)
- `AbstractFooBar` — Base classes
**Examples:**
```java
// Good
public class Board { ... }
public class GameMoveHandler { ... }
public record GameMoveRequestRecord(...) implements ClientRequest { }
public class GomokuHelper { ... }
// Avoid
public class board { ... } // lowercase
public class game_move_handler { ... } // snake_case — use PascalCase
```
### 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<Room> 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<GameMove> 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 com.miti99.caro.common.entity.Board;
import com.miti99.caro.common.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:1999/ratel')
* @returns {Promise<WebSocket>} 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<void>}
*/
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** | `com.miti99.caro.{common,server}.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
```
<type>(<scope>): <subject>
<body>
<footer>
```
**Types:**
- `feat:` — New feature
- `fix:` — Bug fix
- `docs:` — Documentation only
- `refactor:` — Code restructure (no behavior change)
- `test:` — Add/update tests
- `perf:` — Performance improvement
- `chore:` — Build, deps, config (no code logic)
**Examples:**
```bash
git commit -m "feat(server): add room spectator support"
git commit -m "fix(client): prevent out-of-bounds moves"
git commit -m "docs: update deployment guide"
git commit -m "refactor: extract game logic to helper"
git commit -m "test: add 20 new AI test cases"
```
**Rules:**
- Keep subject under 50 characters
- Use imperative mood ("add", not "added")
- No period at end of subject
- Reference issues if applicable: `fix #123`
- No AI references in message
### Code Review Checklist
Before submitting code:
- [ ] Compiles/builds without errors
- [ ] All tests pass (run `./gradlew -p server test` or `npm --prefix client test`)
- [ ] No dead code or commented-out lines
- [ ] File size under limit (200 lines Java, 300 JS)
- [ ] Naming follows conventions
- [ ] Public methods have Javadoc/JSDoc
- [ ] Error handling present (try/catch or validation)
- [ ] No hardcoded values (use constants)
- [ ] No console.log/System.out.println left (except logging)
- [ ] Git commit message follows conventional commits
### Testing Requirements
**Unit Tests:**
- Required for public methods that have business logic
- Test happy path, error cases, edge cases
- Use descriptive test names: `test{Feature}{Condition}{Result}`
**Example:**
```java
@Test
public void testCanWinDetectsHorizontalFive() { ... }
@Test
public void testCanWinReturnsfalseForFour() { ... }
@Test
public void testCanWinHandlesBoardEdges() { ... }
```
**Test Coverage:**
- Game logic: 100%
- Network layer: 80%+
- UI: 60%+ (integration tests)
---
## Linting & Formatting
### Java
- Use IDE default formatter (IntelliJ IDEA / Eclipse)
- No trailing whitespace
- 4-space indentation
- Max line length: 120 characters (soft limit)
### JavaScript
- No specific linter configured (Prettier optional)
- 2-space indentation
- Max line length: 100 characters
- Use `const` by default, `let` when rebinding needed, avoid `var`
### Common Rules (Both)
- No trailing whitespace
- Imports sorted alphabetically
- No unused imports/variables
- No circular dependencies
---
## Performance Guidelines
### Java
- Avoid creating objects in hot loops (game move validation)
- Use appropriate data structures (ArrayList vs LinkedList)
- Cache frequently accessed values (board size constant)
- Profile before optimizing
### JavaScript
- Avoid DOM manipulation in render loops (use Phaser rendering)
- Debounce event handlers if called frequently
- Use `requestAnimationFrame` for animations (Phaser handles this)
- Minimize WebSocket message size (encode data efficiently)
---
## Security Guidelines
### Java
- Validate all input (move coordinates, nickname length)
- Sanitize strings before broadcast (prevent injection)
- Use immutable objects for shared state when possible
- Never expose internal state directly
### JavaScript
- Validate server messages before trusting (type checks)
- Sanitize HTML from user input (prevent XSS)
- Don't store sensitive data in localStorage
- Use HTTPS/WSS in production (TLS required)
---
## Documentation Requirements
**Every public method/function must have:**
1. **Brief description** — One-line summary
2. **Parameters** — Type and purpose of each argument
3. **Return value** — Type and meaning of return
4. **Exceptions** — What can be thrown and why
**Example (Javadoc):**
```java
/**
* Determines the winner of the game by checking for 5-in-a-row.
*
* @param board The current game board
* @param lastRow Row of the last placed piece
* @param lastCol Column of the last placed piece
* @param lastPiece The piece type placed (BLACK or WHITE)
* @return GameResult.BLACK_WIN, GameResult.WHITE_WIN, GameResult.DRAW, or GameResult.IN_PROGRESS
*/
public static GameResult checkGameResult(Board board, int lastRow, int lastCol, PieceType lastPiece) {
// ...
}
```
**Example (JSDoc):**
```javascript
/**
* Sends a game move to the server.
* @param {number} row - Row index (0-14)
* @param {number} col - Column index (0-14)
* @returns {Promise<void>} Resolves when move is confirmed by server
* @throws {Error} If move is invalid or connection lost
*/
export async function sendMove(row, col) {
// ...
}
```
---
## Maintenance & Refactoring
### When to Refactor
- File exceeds size limit
- Duplicated logic appears (DRY violation)
- Method/function has 3+ responsibilities
- Test coverage below target
- Code is confusing (needs better naming)
### Refactoring Rules
- Keep behavior identical (use tests to verify)
- One refactoring per commit
- Descriptive commit message: `refactor(scope): description`
- Code review required before merge
### Deprecation
If removing a feature:
1. Mark with `@Deprecated` (Java) or JSDoc comment
2. Provide migration path in docs
3. Give 2+ release cycles notice
4. Remove after deprecation period
---
## Tools & Environment
**Recommended Tools:**
- **IDE:** IntelliJ IDEA (Java), VS Code (JavaScript)
- **Java:** Gradle 9.x (wrapper committed), Java 25 (LTS) — uses toolchains, records, switch expressions, `var`
- **JavaScript:** Node.js 22+, Vite 6+
- **Version Control:** Git
**Java 25 modernization guidelines:**
- Prefer `record` for immutable DTOs (e.g. wire-format envelopes like `Msg`)
- Use `var` for local variables where the RHS type is obvious at a glance
- Use switch expressions (`switch (...) { case X -> ...; }`) for pure mapping
- Use text blocks (`"""..."""`) for multi-line strings
- Test framework is JUnit 5 (`org.junit.jupiter.api`) — use `@Test`, `Assertions.*`
**CI/CD:**
- GitHub Actions runs on every push
- Build must pass before merge to master
- Tests must be green (no failures ignored)
- Linting recommended (but not enforced)
---
## Common Anti-Patterns to Avoid
| Anti-Pattern | Example | Better Approach |
|--------------|---------|-----------------|
| Magic numbers | `if (x > 225)` | `if (x > BOARD_SIZE * BOARD_SIZE)` |
| Overly long methods | 100+ line method | Extract to smaller methods |
| Null pointers | `user.getRoom().getId()` | Check nulls or use Optional |
| Silent failures | `try { } catch (Exception e) { }` | Log error and handle appropriately |
| String concatenation | `"Hello " + name + "!"` | Use StringBuilder or template strings |
| Global state | `static globalBoard` | Pass as parameter or inject |
| Callback hell | `.then(...).then(...)` | Use async/await |
| Hardcoded paths | `"/Users/alice/data"` | Use constants or config files |
---
## Summary
Caro codebase prioritizes:
1. **Clarity** over cleverness
2. **Simplicity** over features
3. **Testability** over 100% coverage
4. **Maintainability** over premature optimization
Follow these standards to ensure code is understandable, modifiable, and trustworthy for the next developer (or your future self).