Files
scopone/docs/CODE_STYLE.md
2026-04-02 20:16:27 +02:00

5.6 KiB

Code Style

Last Updated: 2026-04-02T18:12:18.000Z

Language & Version

  • Primary language: TypeScript 5.x.
  • Compiler settings from tsconfig.json: target: ES2020, module: ESNext, moduleResolution: bundler, strict: true, noEmit: true.
  • Secondary language: Java for the Capacitor Android wrapper.

Naming Conventions

Kind Convention Real examples
Classes PascalCase BootScene, MenuScene, GameScene, CardTracker
Interfaces PascalCase Card, Capture, GameState, AIDecisionProgress, SearchProfile
Type aliases PascalCase Suit, PlayerIndex, Difficulty
Functions camelCase installFullscreenRequest, findCaptures, createInitialState, chooseMove, updateThinkBar
Constants UPPER_SNAKE or descriptive const names SUITS, PRIMIERA_VALUES, SEARCH_PROFILES, SCOREBAR_H
Private fields camelCase with private modifier state, tracker, thinkBar, selectedGlowTween
Parameters and locals camelCase playerIdx, captureChoice, remainingRatio, partnerHandSize

Structural Conventions

  • Phaser scenes extend Phaser.Scene and put setup in create() and asset loading in preload().
  • Core rule modules use exported functions and interfaces rather than classes.
  • Stateful scene code uses private fields for long-lived UI and interaction state.
  • AI configuration is table-driven through SEARCH_PROFILES and helper interfaces rather than nested literals inside chooseMove().

Indentation & Formatting

  • TypeScript files use 2-space indentation.
  • String literals use single quotes consistently.
  • Semicolons are used consistently.
  • Early returns are common for guard clauses.
  • Multi-line object literals and function calls keep trailing commas when the surrounding style already uses them.
  • const is the default; let is used only where reassignment is needed.

The Android Java wrapper follows the generated template style instead of the TypeScript style. MainActivity.java uses tab indentation and standard Android brace placement.

Import Patterns

Named imports are used for local modules:

import { Card, GameState, PlayerIndex, Difficulty } from './types';
import { findCaptures, canCapture, teamOf, applyMove } from './engine';
import { CardTracker } from './card-tracker';

Framework imports use default or type-only imports where appropriate:

import Phaser from 'phaser';
import type { CapacitorConfig } from '@capacitor/cli';

The project uses only relative import paths. No path aliases are configured.

Export Patterns

  • Public APIs use named exports: export function, export class, export interface, export type, export const.
  • Default exports are reserved for configuration entry points: vite.config.ts and capacitor.config.ts.
  • Internal helpers remain file-local, for example getSubsets, scoreRound, shuffleArray, and alphaBeta.

Typing Patterns

  • Union types model constrained domains: PlayerIndex = 0 | 1 | 2 | 3, Difficulty = 'beginner' | 'advanced' | 'master'.
  • Fixed-size tuples represent stable game structures such as four players and two team scores.
  • Record<number, number> is used for lookup tables like PRIMIERA_VALUES.
  • Exported functions generally declare explicit return types.
  • Async behavior is typed directly in signatures such as chooseMove(...): Promise<AIMove>.

Comments & Documentation

  • Section banners are used heavily in larger files:
// ---------------------------------------------------------------------------
// Turn management
// ---------------------------------------------------------------------------
  • JSDoc appears on rule-heavy or non-obvious APIs, for example findCaptures(), applyMove(), and CardTracker methods.
  • Scene files include [trueref] provenance notes near Phaser-specific APIs.
  • Inline comments explain rule edge cases, heuristics, and visual-effect intent rather than restating syntax.

Code Examples

Guarded DOM capability checks

const installFullscreenRequest = (host: HTMLElement): void => {
  const canRequestFullscreen =
    typeof document.fullscreenEnabled === 'boolean'
      ? document.fullscreenEnabled
      : typeof host.requestFullscreen === 'function';

  if (!canRequestFullscreen || typeof host.requestFullscreen !== 'function') {
    return;
  }
};

Rule-first capture selection

export function findCaptures(played: Card, table: Card[]): Card[][] {
  const directMatches = table.filter(c => c.value === played.value);
  if (directMatches.length > 0) {
    return directMatches.map((directMatch): Card[] => [directMatch]);
  }

  const results: Card[][] = [];
  const subsets = getSubsets(table);
  for (const subset of subsets) {
    if (subset.length >= 2) {
      const sum = subset.reduce((acc, c) => acc + c.value, 0);
      if (sum === played.value) {
        results.push(subset);
      }
    }
  }
  return results;
}

Async AI entry point with progress reporting

export async function chooseMove(
  state: GameState,
  playerIdx: PlayerIndex,
  difficulty: Difficulty = 'advanced',
  tracker?: CardTracker,
  onProgress?: (progress: AIDecisionProgress) => void,
): Promise<AIMove> {
  const startedAt = Date.now();
  const profile = getSearchProfile(state, difficulty);
  reportDecisionProgress(onProgress, difficulty, startedAt, profile.timeBudgetMs, 0, 0);
  // ...
}

Linting & Formatting

No ESLint or Prettier configuration is present.

Formatting is maintained manually, with TypeScript compiler strictness acting as the main automated correctness gate. The only repository-wide verification command currently supplied is npx tsc --noEmit.