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

19 KiB

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:

// 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:

// 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:

// 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:

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

/**
 * 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

// 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...)
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:

// 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:

// 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:

// 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:

// 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)

// 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):

// 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

// 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)

// 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

// 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

// 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

/**
 * 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

// 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:

// 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():

// 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):

// 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:

// 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:

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:

@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):

/**
 * 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):

/**
 * 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).