Step 1 - Project setup

4 min read

Project setup

Create the skeleton the rest of the build sits on. Nothing game-related is implemented here; the goal is that npm run verify runs green on an empty project so every later step has a working safety net.

Repository layout

tic-tac-toe/
  index.html
  package.json
  tsconfig.json
  vite.config.ts
  vitest.config.ts
  eslint.config.js
  .prettierrc
  .gitignore
  LICENSE
  README.md
  public/
    favicon.svg
  src/
    main.ts              composition root, the only file that touches both UI and state
    engine/
      types.ts           Player, Cell, Board, GameStatus, GameState, InvalidMoveError
      constants.ts       BOARD_SIZE, CELL_COUNT, WIN_LINES
      game.ts            createGame, applyMove, undoLastMove, availableMoves, evaluateBoard
      game.test.ts
    ai/
      types.ts           Difficulty, MoveChooser
      random.ts          easy strategy
      heuristic.ts       medium strategy
      minimax.ts         hard strategy with alpha-beta
      choose-move.ts     difficulty -> strategy dispatch
      ai.test.ts
    ui/
      board-view.ts      renders the grid, emits cell selections
      status-view.ts     status line and the aria-live region
      controls-view.ts   mode, difficulty, starting mark, new game, reset scores
      scoreboard-view.ts
      keyboard.ts        roving focus and arrow-key navigation
      strings.ts         every user-visible string
    state/
      app-state.ts       AppState shape and reducers
      storage.ts         localStorage read/write with schema versioning
      storage.test.ts
    styles/
      tokens.css         design tokens as custom properties
      layout.css
      board.css
      controls.css
      index.css          imports the above, in this order

Create every directory now, with the files as stubs where they are not yet implemented. A stub is an empty module with a TSDoc comment naming what it will contain — not a placeholder implementation that will be silently forgotten.

Tooling

Use Vite with the vanilla-TypeScript template as the dev server and bundler, and Vitest for tests. Node 20 or newer.

tsconfig.json must set at minimum:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "verbatimModuleSyntax": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "types": ["vite/client"]
  },
  "include": ["src"]
}

noUncheckedIndexedAccess is deliberate: it forces the engine to handle board[i] being possibly undefined, which catches off-by-one bugs at compile time.

ESLint runs with the recommended TypeScript rules plus a ban on any, non-null assertions and console.log (console.error is allowed). Prettier: 2-space indent, single quotes, semicolons, 100-character print width.

npm scripts

Script Command Purpose
dev vite Dev server on port 5173 with hot reload
build tsc --noEmit && vite build Type-check, then produce dist/
preview vite preview Serve the production build locally
test vitest run Run the suite once
test:watch vitest Watch mode
lint eslint src Lint
format prettier --write . Format
verify npm run lint && npm run build && npm run test The gate used by every later step

index.html

A single document with a lang="en" attribute, a viewport meta tag, a descriptive <title>, a theme-color meta tag, and one <div id="app"></div> plus <script type="module" src="/src/main.ts">. No inline styles and no inline scripts — a strict CSP must be possible without changes.

Add this meta tag so the page cannot be embedded or sniffed into another type:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'">

If Vite's dev server needs a looser policy in development, keep the strict policy in the production build and document the difference in a comment.

Housekeeping

  • .gitignore covers node_modules/, dist/, coverage/, .DS_Store, *.local.
  • LICENSE is the MIT licence text with the current year.
  • public/favicon.svg is a simple two-colour X-and-O mark drawn with SVG paths, no raster fallback.
  • The repository README.md (distinct from this specification) explains install, dev, build and test in under 30 lines.

Acceptance criteria

  • npm install completes with zero runtime dependencies in package.json (dependencies is empty or absent).
  • Every directory and file in the layout above exists; stub modules carry a TSDoc comment describing their future contents.
  • npm run dev serves a page at http://localhost:5173 that renders without console errors.
  • npm run verify exits 0 on the empty project.
  • tsc --noEmit passes with strict and noUncheckedIndexedAccess enabled.
  • ESLint reports zero errors and zero warnings; any and ! assertions are rule-blocked.
  • npm run build produces a dist/ folder whose total size is under 100 kB uncompressed.
  • index.html contains no inline script or style, and declares the CSP meta tag.
  • LICENSE contains the MIT licence.

Discussion

0 comments

No comments yet. Start the discussion.