Professional, accessible, and lightweight Tic Tac Toe implementation using plain HTML, CSS and JavaScript.
This project is intended as a compact example of a small web game with good UX, accessibility, and clear, testable game logic.
Table of contents
- Demo
- Features
- Quick start
- How to play
- Architecture & implementation notes
- Developer workflow
- Tests
- Accessibility
- Troubleshooting
- Contributing
- License
Open index.html in your browser or serve the folder using a static server (recommended for best behaviour):
python3 -m http.server 8000
# then open http://127.0.0.1:8000- Two play modes: Player vs Player (local) and Player vs Computer
- Three difficulty levels for the computer opponent:
- Easy — random moves
- Medium — simple heuristics (win/block/center/corner)
- Hard — minimax algorithm (optimal play)
- Choose which symbol you (the human) play as (X or O)
- Persistent scoreboard using browser localStorage (X / O / Draws)
- Keyboard support: focus a cell and press Enter or Space to play
- Responsive layout and clear focus/hover states for accessibility
- Choose a mode (PvP or PvC).
- Optionally choose which symbol you want to play as (X or O).
- Select difficulty (for PvC).
- Click (or focus + Enter/Space) a cell to make a move. The game announces wins/draws and updates the scoreboard.
index.html— main UI and control elementsstyles.css— responsive styling and visual polishapp.js— game logic (state management, event handlers, AI, persistence)README.md— this file
- Game state is maintained as a simple 9-element array representing the board.
- Win detection uses the standard eight tic-tac-toe win combinations.
- AI:
- Easy: picks a random empty square.
- Medium: attempts to win on the next move, then block the opponent, then take center/corners.
- Hard: uses minimax recursion to choose an optimal move (fine for 3x3 board performance-wise).
- Persistence: scoreboard and basic UI settings are stored in localStorage under keys
ttt:fresh:scoresandttt:fresh:settings.
Recommended environment: a modern browser and Node/Python for running a local static server.
- Install dependencies (none required for this small app).
- Run a static server from the project folder:
python3 -m http.server 8000
# or, if you prefer, use an npm static server like 'serve'- Open http://127.0.0.1:8000 in your browser. Edit files and refresh to see changes.
If you prefer live-reload while editing, install a simple dev server, for example:
# optional: install 'live-server' for automatic reload during development
npm install -g live-server
live-server --port=8000This repository currently doesn't include automated tests, but the game logic is structured so you can add unit tests for:
- Win/draw detection (check all winning combos and draw states)
- Minimax correctness (ensure bestMove returns winning/blocking moves in simple positions)
To add tests you can use a test runner such as Jest or Mocha and export the core functions from app.js into a module for testing.
- All board cells are implemented as focusable buttons and support keyboard activation with Enter and Space.
- Status text uses
aria-liveto announce updates to assistive technologies. - Color contrast and focus outlines are tuned for readability; if you need further enhancements (high-contrast mode, larger fonts), I can add them.
- If the scoreboard isn't saving: ensure your browser allows localStorage for the page (private/incognito modes sometimes block storage).
- If the page appears unstyled: confirm
styles.cssis reachable and not blocked by the browser.
Contributions are welcome. Suggested small tasks:
- Add unit tests for core game logic
- Add animations for winning lines
- Add an undo/redo feature or move history
- Improve AI (e.g., alpha-beta pruning or adjustable difficulty blending)
Please open an issue or a pull request with a clear description and small incremental changes.
This project is provided under the MIT License (see LICENSE file if present). Use it freely for learning and small projects.
If you need a specific feature, prefer a different UX, or want this packaged as a reusable component, tell me what you'd like and I can implement it.