Initial commit: game development

This commit is contained in:
robi committed 2026-09-23 16:31:05 +03:00
commit 176d7cc300
5 files changed
+1084

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
# Tic-Tac-Toe vs CPU — Implementation Plan
A polished, mobile-friendly tic-tac-toe game (X vs O) playable in the browser against a
random-move CPU opponent. Pure HTML/CSS/JS — no build step, no dependencies.
Open `index.html` directly in a browser to play.
## 1. Project Structure
```
game1/
├── index.html # Page structure (board, scoreboard, status, overlay)
├── style.css # All styling: glassmorphism dark theme, animations
├── game.js # Game logic: state, win detection, CPU, scoring, FX
└── docs/
├── PLAN.md # This document
└── mockup.svg # UI mockup (open in a browser to view)
```
## 2. Visual Design
Research (Dribbble/Figma/CodePen examples) shows the current popular style for
tic-tac-toe: **dark glassmorphism with neon accents**, frosted panels, soft glows,
and confetti celebrations on wins.
- **Background**: deep gradient (dark navy → violet) with two soft blurred
colored blobs (cyan + magenta) for depth.
- **Panels**: frosted glass cards — `background: rgba(255,255,255,0.06)`,
`backdrop-filter: blur(14px)`, 1px translucent white border, soft shadow.
(Blur kept modest for mobile performance.)
- **Colors**:
- X (player) — neon cyan `#22d3ee`
- O (CPU) — neon pink/magenta `#f472b6`
- Text: white / muted gray
- **Typography**: system font stack, bold title, tabular numerals for scores.
- **Mockup**: see `docs/mockup.svg` (open in a browser) for the envisioned
look — dark glass panels, neon cyan X / pink O with glow, gradient title.
- **Layout (top → bottom)**:
1. Title: "Tic-Tac-Toe" with subtle gradient text
2. Scoreboard: three glass chips — You (X) · CPU (O) · Draws
3. Status bar: "Your turn" / "CPU is thinking…" / result text
4. Board: 3×3 grid of glass cells
5. Buttons: "New Game" (primary) and "Reset Scores" (ghost)
## 3. Mobile-Friendliness
- `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`
- Board width: `min(88vw, 340px)`; cells are large touch targets (≥ 72px on phones).
- `touch-action: manipulation` on buttons/cells to kill double-tap zoom delay.
- No hover-only interaction: hover glow is a desktop enhancement only; taps work
everywhere. Use `:active` press feedback.
- Layout uses flex column, centered, max-width 420px — looks like a mobile app
on desktop too.
- `user-select: none` on the board to avoid text selection while tapping fast.
- Test: 360px-wide viewport, fast repeated taps, orientation change.
## 4. Game Rules
- Standard 3×3 tic-tac-toe. Player is **X**, CPU is **O**.
- Player always starts the round with X (keeps it simple and predictable).
- Click/tap an empty cell to place your mark.
- CPU responds on a random empty cell after a **600 ms "thinking" delay**
(feels natural, shows the "CPU is thinking…" status).
- Win: 3 in a row (8 winning lines). Draw: board full with no winner.
- After a round ends, a result overlay appears; "Play Again" starts a fresh round
(board clears, scores persist).
## 5. Scoring
- Track **You**, **CPU**, and **Draws** across rounds.
- Persisted in `localStorage` (key `ttt-scores`) so scores survive reloads.
- "Reset Scores" button zeroes them (with a short confirm-less toast feedback).
- Score chips animate a quick pop when the value changes.
## 6. Game-Over Reactions
- **Winning line**: an SVG line is drawn across the three winning cells with a
stroke-dashoffset animation (draws itself in ~400 ms).
- **Result overlay**: frosted glass modal fades/scales in with:
- Win → "You Win! 🎉" + confetti burst (DOM particles, ~40 pieces, random
colors/velocities, fall + fade over ~1.8 s, then removed).
- Loss → "CPU Wins" + a subtle red-tinted shake of the board.
- Draw → "It's a Draw" + neutral pulse.
- Overlay has a single big "Play Again" button.
- Small micro-feedback during play:
- Marks pop in with a scale+fade animation (150 ms).
- Empty cells glow faintly on hover (desktop) and brighten on `:active`.
- Status bar text cross-fades on turn change; "CPU is thinking…" pulses.
## 7. JavaScript Architecture (game.js)
Single file, plain functions + one state object:
```
state = {
board: Array(9).fill(null), // null | 'X' | 'O'
turn: 'X', // 'X' = player, 'O' = CPU
gameOver: false,
scores: { you: 0, cpu: 0, draws: 0 }
}
```
Modules (sections in the file):
1. **DOM refs** — cells, status, score chips, overlay, buttons.
2. **Win detection** — `WIN_LINES` = 8 combos; `checkWinner(board)` returns
`{winner, line}` or `{winner: 'draw'}` or `null`.
3. **Player move** — `onCellTap(i)`: ignore if occupied/game over/CPU turn;
place X, render, check end; else schedule CPU.
4. **CPU move** — `cpuMove()`: after 600 ms, pick a random empty index,
place O, render, check end.
5. **Render** — `renderMark(i, mark)` (pop animation), `renderStatus()`,
`renderScores()`, `drawWinLine(line)` (SVG overlay on the board).
6. **Round end** — `endRound(result)`: update + persist scores, show overlay
with appropriate FX (confetti / shake / pulse).
7. **Confetti** — `burstConfetti()`: create N absolutely-positioned divs with
random transforms, animate via CSS transitions, remove after finish.
8. **Reset** — `newRound()` (clear board, player first), `resetScores()`.
9. **Init** — load scores from localStorage, build board DOM, bind events.
## 8. Implementation Steps
1. `index.html` — semantic structure, viewport meta, SVG defs for win line.
2. `style.css` — theme tokens (CSS custom properties), background, glass panels,
board grid, marks (X/O drawn with CSS or inline SVG for crispness),
animations (pop, pulse, shake, confetti fall), overlay, responsive tweaks.
3. `game.js` — implement sections 1–9 above.
4. Manual test pass:
- Win all 8 line types (play through scenarios).
- Draw scenario.
- Rapid tapping (no double placement, no CPU overlap).
- Scores persist across reload; reset works.
- 360px viewport: no overflow, big touch targets, no zoom on tap.
5. Polish: check animation smoothness on a mid-range phone if available.
## 9. Hosting the App
The app is three static files, so any static file server works. The goal is a
one-line start, no config files, no build step.
**Primary option — Python (already installed, zero setup):**
```bash
python3 -m http.server 8000
```
Run from the `game1/` folder. It binds to all network interfaces by default,
so the game is immediately reachable:
- **Same machine**: http://localhost:8000
- **Same network (LAN)**: http://\<machine-ip\>:8000
(find the IP with `hostname -I` or `ip addr`)
- **Internet**: forward port 8000 in the router/firewall, or put the machine
behind a reverse proxy. For public exposure, prefer caddy (see below) and
keep the raw port closed otherwise.
**Alternative — Caddy (nicest for internet exposure, auto-TLS with a domain):**
```bash
caddy file-server --root . --listen :8000
```
**Alternative — Node (if Python is unavailable):**
```bash
npx serve -l 8000
```
Notes:
- These dev servers are fine for LAN / home use. For a public internet
endpoint, use caddy behind a domain (it handles TLS automatically) and do
not expose the raw port to the internet with a bare dev server.
- No deploy step: the folder *is* the app — copy it anywhere and serve it.
## 10. Non-Goals (kept simple on purpose)
- No minimax / unbeatable AI (CPU is random, as requested).
- No sound effects, no multi-language support, no user-vs-user mode.
- No framework, no bundler — the game is three files.
+92
View File
@@ -0,0 +1,92 @@
<svg xmlns="http://www.w3.org/2000/svg" width="420" height="780" viewBox="0 0 420 780" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0d0f24"/>
<stop offset="1" stop-color="#1c1140"/>
</linearGradient>
<linearGradient id="titleGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#22d3ee"/>
<stop offset="1" stop-color="#f472b6"/>
</linearGradient>
<linearGradient id="btnGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#22d3ee"/>
<stop offset="1" stop-color="#f472b6"/>
</linearGradient>
<filter id="blob" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="55"/>
</filter>
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="5" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- background -->
<rect width="420" height="780" fill="url(#bgGrad)"/>
<circle cx="70" cy="170" r="130" fill="#22d3ee" opacity="0.22" filter="url(#blob)"/>
<circle cx="350" cy="580" r="150" fill="#f472b6" opacity="0.18" filter="url(#blob)"/>
<!-- title -->
<text x="210" y="66" text-anchor="middle" font-size="30" font-weight="700" fill="url(#titleGrad)">Tic-Tac-Toe</text>
<!-- scoreboard -->
<g>
<rect x="18" y="96" width="120" height="64" rx="16" fill="#ffffff" fill-opacity="0.06" stroke="#ffffff" stroke-opacity="0.14"/>
<text x="78" y="120" text-anchor="middle" font-size="11" letter-spacing="1.5" fill="#ffffff" opacity="0.55">YOU (X)</text>
<text x="78" y="148" text-anchor="middle" font-size="22" font-weight="700" fill="#22d3ee">2</text>
<rect x="150" y="96" width="120" height="64" rx="16" fill="#ffffff" fill-opacity="0.06" stroke="#ffffff" stroke-opacity="0.14"/>
<text x="210" y="120" text-anchor="middle" font-size="11" letter-spacing="1.5" fill="#ffffff" opacity="0.55">CPU (O)</text>
<text x="210" y="148" text-anchor="middle" font-size="22" font-weight="700" fill="#f472b6">1</text>
<rect x="282" y="96" width="120" height="64" rx="16" fill="#ffffff" fill-opacity="0.06" stroke="#ffffff" stroke-opacity="0.14"/>
<text x="342" y="120" text-anchor="middle" font-size="11" letter-spacing="1.5" fill="#ffffff" opacity="0.55">DRAWS</text>
<text x="342" y="148" text-anchor="middle" font-size="22" font-weight="700" fill="#ffffff">1</text>
</g>
<!-- status -->
<text x="210" y="200" text-anchor="middle" font-size="14" fill="#ffffff" opacity="0.65">CPU is thinking...</text>
<!-- board panel -->
<rect x="40" y="220" width="340" height="340" rx="24" fill="#ffffff" fill-opacity="0.05" stroke="#ffffff" stroke-opacity="0.12"/>
<!-- cells -->
<g fill="#ffffff" fill-opacity="0.06" stroke="#ffffff" stroke-opacity="0.10">
<rect x="54" y="234" width="96" height="96" rx="16"/>
<rect x="162" y="234" width="96" height="96" rx="16"/>
<rect x="270" y="234" width="96" height="96" rx="16"/>
<rect x="54" y="342" width="96" height="96" rx="16"/>
<rect x="162" y="342" width="96" height="96" rx="16"/>
<rect x="270" y="342" width="96" height="96" rx="16"/>
<rect x="54" y="450" width="96" height="96" rx="16"/>
<rect x="162" y="450" width="96" height="96" rx="16"/>
<rect x="270" y="450" width="96" height="96" rx="16"/>
</g>
<!-- marks -->
<g filter="url(#glow)" stroke="#22d3ee" stroke-width="7" stroke-linecap="round">
<line x1="80" y1="260" x2="124" y2="304"/>
<line x1="124" y1="260" x2="80" y2="304"/>
</g>
<circle cx="210" cy="282" r="24" fill="none" stroke="#f472b6" stroke-width="7" filter="url(#glow)"/>
<circle cx="102" cy="390" r="24" fill="none" stroke="#f472b6" stroke-width="7" filter="url(#glow)"/>
<g filter="url(#glow)" stroke="#22d3ee" stroke-width="7" stroke-linecap="round">
<line x1="188" y1="368" x2="232" y2="412"/>
<line x1="232" y1="368" x2="188" y2="412"/>
</g>
<g filter="url(#glow)" stroke="#22d3ee" stroke-width="7" stroke-linecap="round">
<line x1="296" y1="368" x2="340" y2="412"/>
<line x1="340" y1="368" x2="296" y2="412"/>
</g>
<circle cx="210" cy="498" r="24" fill="none" stroke="#f472b6" stroke-width="7" filter="url(#glow)"/>
<!-- buttons -->
<rect x="60" y="600" width="300" height="54" rx="27" fill="url(#btnGrad)"/>
<text x="210" y="633" text-anchor="middle" font-size="16" font-weight="700" fill="#0d1025">New Game</text>
<rect x="60" y="668" width="300" height="48" rx="24" fill="#ffffff" fill-opacity="0.05" stroke="#ffffff" stroke-opacity="0.18"/>
<text x="210" y="697" text-anchor="middle" font-size="14" fill="#ffffff" opacity="0.7">Reset Scores</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB