claudedino 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +3 -0
- package/.prettierrc +10 -0
- package/CLAUDE.md +91 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -0
- package/dist/game/dino-game.d.ts +8 -0
- package/dist/game/dino-game.d.ts.map +1 -0
- package/dist/game/dino-game.js +199 -0
- package/dist/game/dino-game.js.map +1 -0
- package/dist/game/obstacles.d.ts +6 -0
- package/dist/game/obstacles.d.ts.map +1 -0
- package/dist/game/obstacles.js +60 -0
- package/dist/game/obstacles.js.map +1 -0
- package/dist/game/physics.d.ts +9 -0
- package/dist/game/physics.d.ts.map +1 -0
- package/dist/game/physics.js +125 -0
- package/dist/game/physics.js.map +1 -0
- package/dist/game/renderer.d.ts +12 -0
- package/dist/game/renderer.d.ts.map +1 -0
- package/dist/game/renderer.js +156 -0
- package/dist/game/renderer.js.map +1 -0
- package/dist/game/sprites.d.ts +8 -0
- package/dist/game/sprites.d.ts.map +1 -0
- package/dist/game/sprites.js +69 -0
- package/dist/game/sprites.js.map +1 -0
- package/dist/game/types.d.ts +78 -0
- package/dist/game/types.d.ts.map +1 -0
- package/dist/game/types.js +56 -0
- package/dist/game/types.js.map +1 -0
- package/dist/hooks/setup.d.ts +3 -0
- package/dist/hooks/setup.d.ts.map +1 -0
- package/dist/hooks/setup.js +74 -0
- package/dist/hooks/setup.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/detect.d.ts +15 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +44 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/split.d.ts +3 -0
- package/dist/platform/split.d.ts.map +1 -0
- package/dist/platform/split.js +40 -0
- package/dist/platform/split.js.map +1 -0
- package/dist/state/watcher.d.ts +6 -0
- package/dist/state/watcher.d.ts.map +1 -0
- package/dist/state/watcher.js +44 -0
- package/dist/state/watcher.js.map +1 -0
- package/docs/design-spec.md +562 -0
- package/docs/implementation-plan.md +602 -0
- package/eslint.config.mjs +96 -0
- package/package.json +38 -0
- package/scripts/check-all.sh +91 -0
- package/scripts/check-suppressions.sh +39 -0
- package/src/app.tsx +19 -0
- package/src/cli.ts +25 -0
- package/src/game/dino-game.tsx +279 -0
- package/src/game/obstacles.ts +81 -0
- package/src/game/physics.ts +171 -0
- package/src/game/renderer.ts +230 -0
- package/src/game/sprites.ts +84 -0
- package/src/game/types.ts +95 -0
- package/src/hooks/setup.ts +105 -0
- package/src/index.ts +4 -0
- package/src/platform/detect.ts +50 -0
- package/src/platform/split.ts +44 -0
- package/src/state/watcher.ts +53 -0
- package/tsconfig.json +35 -0
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ClaudeDino
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
ASCII dino runner game that runs alongside Claude Code in a split terminal pane. Built with Ink (React for terminals) and TypeScript. Uses Claude Code hooks for state detection.
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
- TypeScript (strict mode, all flags enabled)
|
|
10
|
+
- Ink 7 (React for terminals)
|
|
11
|
+
- React 19
|
|
12
|
+
- Node.js 22+
|
|
13
|
+
- ESM modules
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
node dist/cli.js --attach
|
|
21
|
+
# In another terminal: echo working > $TMPDIR/claudedino-state
|
|
22
|
+
# Game starts. echo idle > $TMPDIR/claudedino-state to pause.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
- `npm run build` -- compile TypeScript to dist/
|
|
28
|
+
- `npm run check:all` -- run all 5 checks (typecheck, suppression scan, ESLint, Prettier, tsconfig audit)
|
|
29
|
+
- `npm run lint` -- ESLint only
|
|
30
|
+
- `npm run lint:fix` -- ESLint with auto-fix
|
|
31
|
+
- `npm run format` -- Prettier check
|
|
32
|
+
- `npm run format:fix` -- Prettier write
|
|
33
|
+
- `npm run typecheck` -- TypeScript type check (no emit)
|
|
34
|
+
|
|
35
|
+
## Pre-Commit Workflow
|
|
36
|
+
|
|
37
|
+
1. Run `npm run check:all`
|
|
38
|
+
2. Zero warnings, zero errors, zero suppressions
|
|
39
|
+
|
|
40
|
+
## Linting
|
|
41
|
+
|
|
42
|
+
ESLint with three strict rulesets stacked:
|
|
43
|
+
|
|
44
|
+
- `eslint/all` (every built-in rule)
|
|
45
|
+
- `typescript-eslint/strictTypeChecked` + `stylisticTypeChecked`
|
|
46
|
+
- `unicorn/all`
|
|
47
|
+
|
|
48
|
+
Key enforced rules:
|
|
49
|
+
|
|
50
|
+
- `explicit-function-return-type` -- all functions must have return types
|
|
51
|
+
- `consistent-type-imports` -- use `import type` for type-only imports
|
|
52
|
+
- `naming-convention` -- camelCase default, PascalCase for types/enums/React components, UPPER_CASE for const variables
|
|
53
|
+
- `no-console` -- use `process.stdout.write()` instead
|
|
54
|
+
- Zero lint suppressions allowed (checked by scripts/check-suppressions.sh)
|
|
55
|
+
|
|
56
|
+
## Architecture
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
src/
|
|
60
|
+
cli.ts -- Entry point, arg parsing, auto-split
|
|
61
|
+
app.tsx -- Root Ink component, connects state watcher to game
|
|
62
|
+
game/
|
|
63
|
+
types.ts -- All enums, interfaces, constants
|
|
64
|
+
sprites.ts -- Sprite definitions (2D string arrays)
|
|
65
|
+
physics.ts -- Jump, gravity, crouch, collision (pure functions)
|
|
66
|
+
obstacles.ts -- Spawning, movement, difficulty progression
|
|
67
|
+
renderer.ts -- Frame buffer creation, sprite drawing, text output
|
|
68
|
+
dino-game.tsx -- Main game component, state machine, game loop
|
|
69
|
+
hooks/
|
|
70
|
+
setup.ts -- Claude Code hook configuration in ~/.claude/settings.json
|
|
71
|
+
platform/
|
|
72
|
+
detect.ts -- OS and terminal detection
|
|
73
|
+
split.ts -- Cross-platform terminal splitting
|
|
74
|
+
state/
|
|
75
|
+
watcher.ts -- Watches temp state file for working/idle changes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key Patterns
|
|
79
|
+
|
|
80
|
+
- All game logic functions are pure (return new objects, no mutation)
|
|
81
|
+
- `noUncheckedIndexedAccess` is enabled -- guard all array access with undefined checks
|
|
82
|
+
- Use `execFileSync` not `execSync` to avoid shell injection
|
|
83
|
+
- React components use function declarations with PascalCase names
|
|
84
|
+
- Ink's `useInput` for keyboard, `useStdout` for terminal dimensions
|
|
85
|
+
|
|
86
|
+
## Gotchas
|
|
87
|
+
|
|
88
|
+
- `ink-use-stdout-dimensions` is CJS and incompatible with ink v7 (ESM with top-level await). Use `useStdout()` from ink directly.
|
|
89
|
+
- State file path is `os.tmpdir() + "/claudedino-state"`. On macOS this is `/var/folders/.../claudedino-state`, not `/tmp/`.
|
|
90
|
+
- Hook commands in `~/.claude/settings.json` are tagged with `# claudedino` suffix for identification.
|
|
91
|
+
- Ink has no keyup event. Crouch detection works by resetting `crouchHeld` each tick and re-setting it when down arrow fires.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furkan Erday
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ClaudeDino
|
|
2
|
+
|
|
3
|
+
A tiny dino runner game that takes over the dead space in Claude Code's terminal while the AI is processing. Jump over cacti, dodge birds, rack up points. It pauses when Claude is done and waits for you to send another task before it lets you play again. Turns waiting time into game time and keeps your eyes where the work is happening.
|
|
4
|
+
|
|
5
|
+
## Why This Exists
|
|
6
|
+
|
|
7
|
+
Every time Claude starts working on a task, you wait. And waiting is when your brain starts looking for something else to do. You check your phone. You open YouTube. You scroll X. You switch to a browser tab and forget you were even coding. Five minutes later Claude is done and you have no idea because you are watching a video about why socks disappear in the dryer.
|
|
8
|
+
|
|
9
|
+
ClaudeDino fixes this by giving you something to do right there in your terminal. A simple, dumb, addictive little dino game that only runs while Claude is working. The moment Claude finishes, the game pauses. The only way to keep playing is to give Claude another task. So instead of losing focus, you stay on the screen, you stay in the flow, and you actually notice when Claude is done.
|
|
10
|
+
|
|
11
|
+
It is not productivity software. It is an anti-distraction trick disguised as a game.
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
ClaudeDino runs in a separate terminal pane right below Claude Code. When you type `claudedino`, it automatically splits your terminal and starts the game in the bottom half. No manual setup needed.
|
|
16
|
+
|
|
17
|
+
Behind the scenes, it uses Claude Code's **hooks system** to know when Claude is working and when Claude is idle. When Claude starts processing your task, the game starts with a 3-2-1 countdown. When Claude finishes, the game pauses instantly. Your score and position are saved so you pick up exactly where you left off next time.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ │
|
|
22
|
+
│ Claude Code (normal, unmodified) │
|
|
23
|
+
│ │
|
|
24
|
+
│ > Working on your task... │
|
|
25
|
+
│ ■ Running tests │
|
|
26
|
+
│ │
|
|
27
|
+
│ > type bar │
|
|
28
|
+
├─────────────────────────────────────────────────────────────┤
|
|
29
|
+
│ Score: 00042 HI: 00891 │
|
|
30
|
+
│ ░░ │
|
|
31
|
+
│ ██ │
|
|
32
|
+
│ ██ ████ ██ │
|
|
33
|
+
│ ████ █ ████ ██ │
|
|
34
|
+
│ █ █ ██ ████ │
|
|
35
|
+
│ ████ ████ ████ │
|
|
36
|
+
│ ═══════════████═════════════════════════════════════════════ │
|
|
37
|
+
└─────────────────────────────────────────────────────────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g claudedino
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
Just run it in your terminal while Claude Code is open:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
claudedino
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That is it. The app will:
|
|
55
|
+
|
|
56
|
+
1. Detect your terminal (iTerm2, Windows Terminal, tmux, etc.)
|
|
57
|
+
2. Automatically split the pane and launch the game in the bottom half
|
|
58
|
+
3. Configure Claude Code hooks so the game knows when Claude is working
|
|
59
|
+
4. Start playing the moment Claude gets a task
|
|
60
|
+
|
|
61
|
+
If auto-split does not work on your setup, you can split manually and run:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claudedino --attach
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Controls
|
|
68
|
+
|
|
69
|
+
| Key | Action |
|
|
70
|
+
| --------------------- | ------------------ |
|
|
71
|
+
| `Space` or `Up Arrow` | Jump over cacti |
|
|
72
|
+
| `Down Arrow` | Crouch under birds |
|
|
73
|
+
|
|
74
|
+
Controls only work while the game is active (Claude is working). When Claude is idle, the game is paused and your keyboard works normally.
|
|
75
|
+
|
|
76
|
+
## Game Rules
|
|
77
|
+
|
|
78
|
+
- You are a dino running through an endless desert
|
|
79
|
+
- Jump over cacti, duck under birds
|
|
80
|
+
- Speed increases as your score goes up
|
|
81
|
+
- Birds start appearing after score 200
|
|
82
|
+
- When Claude finishes a task, the game pauses with your score saved
|
|
83
|
+
- When Claude starts the next task, the game resumes after a 3-2-1 countdown
|
|
84
|
+
- If you hit an obstacle, game over. Score resets next round.
|
|
85
|
+
- High score is tracked for the session
|
|
86
|
+
|
|
87
|
+
## Platform Support
|
|
88
|
+
|
|
89
|
+
| OS | Terminal | Auto-Split |
|
|
90
|
+
| ------- | ---------------- | ------------------------ |
|
|
91
|
+
| macOS | iTerm2 | Yes |
|
|
92
|
+
| macOS | Terminal.app | Yes |
|
|
93
|
+
| Windows | Windows Terminal | Yes |
|
|
94
|
+
| Linux | tmux | Yes |
|
|
95
|
+
| Any | Manual split | Fallback with `--attach` |
|
|
96
|
+
|
|
97
|
+
## How the State Detection Works
|
|
98
|
+
|
|
99
|
+
ClaudeDino hooks into Claude Code's event system by adding hooks to your `~/.claude/settings.json`. These hooks write a tiny state flag whenever Claude starts or finishes processing. The game watches this flag and reacts instantly.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAMnD,iBAAS,GAAG,IAAI,KAAK,CAAC,YAAY,CAUjC;AAED,eAAe,GAAG,CAAC"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { ClaudeState } from "./game/types.js";
|
|
4
|
+
import DinoGame from "./game/dino-game.js";
|
|
5
|
+
import { initStateFile, watchState } from "./state/watcher.js";
|
|
6
|
+
function App() {
|
|
7
|
+
const [claudeState, setClaudeState] = useState(ClaudeState.Idle);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
initStateFile();
|
|
10
|
+
const cleanup = watchState(setClaudeState);
|
|
11
|
+
return cleanup;
|
|
12
|
+
}, []);
|
|
13
|
+
return _jsx(DinoGame, { claudeState: claudeState });
|
|
14
|
+
}
|
|
15
|
+
export default App;
|
|
16
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,QAAQ,MAAM,qBAAqB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE/D,SAAS,GAAG;IACV,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAc,WAAW,CAAC,IAAI,CAAC,CAAC;IAE9E,SAAS,CAAC,GAAG,EAAE;QACb,aAAa,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;QAC3C,OAAO,OAAO,CAAC;IACjB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,KAAC,QAAQ,IAAC,WAAW,EAAE,WAAW,GAAI,CAAC;AAChD,CAAC;AAED,eAAe,GAAG,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import App from "./app.js";
|
|
5
|
+
import { setupHooks } from "./hooks/setup.js";
|
|
6
|
+
import { detectTerminal } from "./platform/detect.js";
|
|
7
|
+
import { trySplit } from "./platform/split.js";
|
|
8
|
+
const attachFlag = process.argv.includes("--attach");
|
|
9
|
+
if (!attachFlag) {
|
|
10
|
+
setupHooks();
|
|
11
|
+
const terminal = detectTerminal();
|
|
12
|
+
const didSplit = trySplit(terminal);
|
|
13
|
+
if (didSplit) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
process.stdout.write("Could not auto-split terminal. Running game inline.\n");
|
|
17
|
+
}
|
|
18
|
+
render(React.createElement(App));
|
|
19
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;AAErD,IAAI,CAAC,UAAU,EAAE,CAAC;IAChB,UAAU,EAAE,CAAC;IACb,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEpC,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ClaudeState } from "./types.js";
|
|
3
|
+
interface DinoGameProps {
|
|
4
|
+
claudeState: ClaudeState;
|
|
5
|
+
}
|
|
6
|
+
declare function DinoGame({ claudeState }: DinoGameProps): React.ReactNode;
|
|
7
|
+
export default DinoGame;
|
|
8
|
+
//# sourceMappingURL=dino-game.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dino-game.d.ts","sourceRoot":"","sources":["../../src/game/dino-game.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAG3D,OAAO,EAQL,WAAW,EAOZ,MAAM,YAAY,CAAC;AAgBpB,UAAU,aAAa;IACrB,WAAW,EAAE,WAAW,CAAC;CAC1B;AA2BD,iBAAS,QAAQ,CAAC,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,KAAK,CAAC,SAAS,CAqNjE;AAED,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import { BASE_SPEED, CLOUD_CULL_X, CLOUD_MAX_Y, CLOUD_SPAWN_PROBABILITY, CLOUD_SPEED_FACTOR, COUNTDOWN_SECONDS, ClaudeState, DinoPose, GameState, MAX_CLOUDS, SCORE_INCREMENT_TICKS, SPAWN_X_OFFSET, TICK_INTERVAL_MS, } from "./types.js";
|
|
5
|
+
import { applyGravity, checkCollision, getDefaultDinoY, startCrouch, startJump, stopCrouch, } from "./physics.js";
|
|
6
|
+
import { getCurrentSpeed, moveObstacles, shouldSpawnObstacle, spawnObstacle } from "./obstacles.js";
|
|
7
|
+
import { renderFrame } from "./renderer.js";
|
|
8
|
+
function noop() {
|
|
9
|
+
// intentionally empty
|
|
10
|
+
}
|
|
11
|
+
function createInitialWorld(highScore) {
|
|
12
|
+
return {
|
|
13
|
+
dino: {
|
|
14
|
+
pose: DinoPose.Run1,
|
|
15
|
+
y: getDefaultDinoY(),
|
|
16
|
+
velocityY: 0,
|
|
17
|
+
isCrouching: false,
|
|
18
|
+
isJumping: false,
|
|
19
|
+
},
|
|
20
|
+
obstacles: [],
|
|
21
|
+
clouds: [],
|
|
22
|
+
score: 0,
|
|
23
|
+
highScore,
|
|
24
|
+
speed: BASE_SPEED,
|
|
25
|
+
groundOffset: 0,
|
|
26
|
+
tickCount: 0,
|
|
27
|
+
distanceSinceLastObstacle: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function DinoGame({ claudeState }) {
|
|
31
|
+
const { stdout } = useStdout();
|
|
32
|
+
const columns = stdout.columns;
|
|
33
|
+
const [gameState, setGameState] = useState(GameState.Hidden);
|
|
34
|
+
const [world, setWorld] = useState(() => createInitialWorld(0));
|
|
35
|
+
const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS);
|
|
36
|
+
const worldRef = useRef(world);
|
|
37
|
+
const gameStateRef = useRef(gameState);
|
|
38
|
+
const inputRef = useRef({ jump: false, crouchHeld: false });
|
|
39
|
+
// Keep refs in sync
|
|
40
|
+
worldRef.current = world;
|
|
41
|
+
gameStateRef.current = gameState;
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Claude state effect
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const current = gameStateRef.current;
|
|
47
|
+
if (claudeState === ClaudeState.Working &&
|
|
48
|
+
(current === GameState.Hidden ||
|
|
49
|
+
current === GameState.Paused ||
|
|
50
|
+
current === GameState.GameOver)) {
|
|
51
|
+
if (current === GameState.GameOver) {
|
|
52
|
+
const previousHighScore = worldRef.current.highScore;
|
|
53
|
+
setWorld(createInitialWorld(previousHighScore));
|
|
54
|
+
}
|
|
55
|
+
setCountdown(COUNTDOWN_SECONDS);
|
|
56
|
+
setGameState(GameState.Countdown);
|
|
57
|
+
}
|
|
58
|
+
if (claudeState === ClaudeState.Idle &&
|
|
59
|
+
(current === GameState.Playing || current === GameState.Countdown)) {
|
|
60
|
+
setGameState(GameState.Paused);
|
|
61
|
+
}
|
|
62
|
+
}, [claudeState]);
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Countdown effect
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (gameState !== GameState.Countdown) {
|
|
68
|
+
return noop;
|
|
69
|
+
}
|
|
70
|
+
const interval = setInterval(() => {
|
|
71
|
+
setCountdown((previous) => {
|
|
72
|
+
if (previous <= 1) {
|
|
73
|
+
setGameState(GameState.Playing);
|
|
74
|
+
return COUNTDOWN_SECONDS;
|
|
75
|
+
}
|
|
76
|
+
return previous - 1;
|
|
77
|
+
});
|
|
78
|
+
}, 1000);
|
|
79
|
+
return () => {
|
|
80
|
+
clearInterval(interval);
|
|
81
|
+
};
|
|
82
|
+
}, [gameState]);
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Game loop effect
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (gameState !== GameState.Playing) {
|
|
88
|
+
return noop;
|
|
89
|
+
}
|
|
90
|
+
const interval = setInterval(() => {
|
|
91
|
+
setWorld((previous) => {
|
|
92
|
+
// 1. Read input
|
|
93
|
+
const input = inputRef.current;
|
|
94
|
+
const wantJump = input.jump;
|
|
95
|
+
const wantCrouch = input.crouchHeld;
|
|
96
|
+
// Reset input for next tick
|
|
97
|
+
input.jump = false;
|
|
98
|
+
input.crouchHeld = false;
|
|
99
|
+
// 2. Apply jump/crouch
|
|
100
|
+
let dino = previous.dino;
|
|
101
|
+
if (wantJump) {
|
|
102
|
+
dino = startJump(dino);
|
|
103
|
+
}
|
|
104
|
+
if (wantCrouch && !dino.isJumping) {
|
|
105
|
+
dino = startCrouch(dino);
|
|
106
|
+
}
|
|
107
|
+
else if (!wantCrouch && dino.isCrouching) {
|
|
108
|
+
dino = stopCrouch(dino);
|
|
109
|
+
}
|
|
110
|
+
// 3. Apply gravity
|
|
111
|
+
dino = applyGravity(dino);
|
|
112
|
+
// 4. Move obstacles
|
|
113
|
+
const speed = getCurrentSpeed(previous.score);
|
|
114
|
+
let obstacles = moveObstacles(previous.obstacles, speed);
|
|
115
|
+
// 5. Spawn obstacles
|
|
116
|
+
let distanceSinceLastObstacle = previous.distanceSinceLastObstacle + speed;
|
|
117
|
+
if (shouldSpawnObstacle({ ...previous, distanceSinceLastObstacle }, columns)) {
|
|
118
|
+
obstacles = [...obstacles, spawnObstacle(previous, columns)];
|
|
119
|
+
distanceSinceLastObstacle = 0;
|
|
120
|
+
}
|
|
121
|
+
// 6. Check collisions
|
|
122
|
+
for (const obstacle of obstacles) {
|
|
123
|
+
if (checkCollision(dino, obstacle)) {
|
|
124
|
+
const finalScore = previous.score;
|
|
125
|
+
const updatedHighScore = Math.max(finalScore, previous.highScore);
|
|
126
|
+
setGameState(GameState.GameOver);
|
|
127
|
+
return {
|
|
128
|
+
...previous,
|
|
129
|
+
dino,
|
|
130
|
+
obstacles,
|
|
131
|
+
score: finalScore,
|
|
132
|
+
highScore: updatedHighScore,
|
|
133
|
+
speed,
|
|
134
|
+
distanceSinceLastObstacle,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 7. Move/spawn clouds
|
|
139
|
+
let clouds = previous.clouds
|
|
140
|
+
.map((cloud) => ({
|
|
141
|
+
...cloud,
|
|
142
|
+
x: cloud.x - speed * CLOUD_SPEED_FACTOR,
|
|
143
|
+
}))
|
|
144
|
+
.filter((cloud) => cloud.x >= CLOUD_CULL_X);
|
|
145
|
+
if (clouds.length < MAX_CLOUDS && Math.random() < CLOUD_SPAWN_PROBABILITY) {
|
|
146
|
+
clouds = [
|
|
147
|
+
...clouds,
|
|
148
|
+
{
|
|
149
|
+
x: columns + SPAWN_X_OFFSET,
|
|
150
|
+
y: Math.floor(Math.random() * CLOUD_MAX_Y),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
// 8. Increment score
|
|
155
|
+
const tickCount = previous.tickCount + 1;
|
|
156
|
+
const score = tickCount % SCORE_INCREMENT_TICKS === 0 ? previous.score + 1 : previous.score;
|
|
157
|
+
// 9. Update ground offset
|
|
158
|
+
const groundOffset = (previous.groundOffset + speed) % columns;
|
|
159
|
+
return {
|
|
160
|
+
dino,
|
|
161
|
+
obstacles,
|
|
162
|
+
clouds,
|
|
163
|
+
score,
|
|
164
|
+
highScore: Math.max(score, previous.highScore),
|
|
165
|
+
speed,
|
|
166
|
+
groundOffset,
|
|
167
|
+
tickCount,
|
|
168
|
+
distanceSinceLastObstacle,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}, TICK_INTERVAL_MS);
|
|
172
|
+
return () => {
|
|
173
|
+
clearInterval(interval);
|
|
174
|
+
};
|
|
175
|
+
}, [gameState, columns]);
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Input handling
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
useInput((input, key) => {
|
|
180
|
+
if (input === " " || key.upArrow) {
|
|
181
|
+
inputRef.current.jump = true;
|
|
182
|
+
}
|
|
183
|
+
if (key.downArrow) {
|
|
184
|
+
inputRef.current.crouchHeld = true;
|
|
185
|
+
}
|
|
186
|
+
}, { isActive: gameState === GameState.Playing });
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Render
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
const frame = renderFrame({
|
|
191
|
+
world,
|
|
192
|
+
width: columns,
|
|
193
|
+
gameState,
|
|
194
|
+
countdownValue: countdown,
|
|
195
|
+
});
|
|
196
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: frame }) }));
|
|
197
|
+
}
|
|
198
|
+
export default DinoGame;
|
|
199
|
+
//# sourceMappingURL=dino-game.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dino-game.js","sourceRoot":"","sources":["../../src/game/dino-game.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GameWorld, type Obstacle } from "./types.js";
|
|
2
|
+
export declare function getCurrentSpeed(score: number): number;
|
|
3
|
+
export declare function shouldSpawnObstacle(world: GameWorld, terminalWidth: number): boolean;
|
|
4
|
+
export declare function spawnObstacle(world: GameWorld, terminalWidth: number): Obstacle;
|
|
5
|
+
export declare function moveObstacles(obstacles: Obstacle[], speed: number): Obstacle[];
|
|
6
|
+
//# sourceMappingURL=obstacles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obstacles.d.ts","sourceRoot":"","sources":["../../src/game/obstacles.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,QAAQ,EAMd,MAAM,YAAY,CAAC;AAkBpB,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGrD;AAkBD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAOpF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,QAAQ,CAY/E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAU9E"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BASE_SPEED, DIFFICULTY_TABLE, MIN_OBSTACLE_GAP, ObstacleType, SPAWN_X_OFFSET, } from "./types.js";
|
|
2
|
+
function getCurrentTier(score) {
|
|
3
|
+
for (const tier of DIFFICULTY_TABLE) {
|
|
4
|
+
if (score <= tier.maxScore) {
|
|
5
|
+
return tier;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// Fallback to last tier (unreachable since last tier has Infinity maxScore)
|
|
9
|
+
const lastTier = DIFFICULTY_TABLE.at(-1);
|
|
10
|
+
if (lastTier === undefined) {
|
|
11
|
+
throw new Error("DIFFICULTY_TABLE is empty");
|
|
12
|
+
}
|
|
13
|
+
return lastTier;
|
|
14
|
+
}
|
|
15
|
+
export function getCurrentSpeed(score) {
|
|
16
|
+
const tier = getCurrentTier(score);
|
|
17
|
+
return BASE_SPEED * tier.speedMultiplier;
|
|
18
|
+
}
|
|
19
|
+
function getAvailableObstacleTypes(score) {
|
|
20
|
+
const tier = getCurrentTier(score);
|
|
21
|
+
const types = [ObstacleType.SmallCactus];
|
|
22
|
+
if (score >= 100) {
|
|
23
|
+
types.push(ObstacleType.LargeCactus, ObstacleType.CactusGroup);
|
|
24
|
+
}
|
|
25
|
+
if (tier.birdsEnabled) {
|
|
26
|
+
types.push(ObstacleType.BirdHigh, ObstacleType.BirdMid);
|
|
27
|
+
}
|
|
28
|
+
return types;
|
|
29
|
+
}
|
|
30
|
+
export function shouldSpawnObstacle(world, terminalWidth) {
|
|
31
|
+
const tier = getCurrentTier(world.score);
|
|
32
|
+
const speed = BASE_SPEED * tier.speedMultiplier;
|
|
33
|
+
const minGap = Math.max(MIN_OBSTACLE_GAP, Math.round(40 / speed));
|
|
34
|
+
const spawnThreshold = minGap + Math.round((Math.random() * 15) / tier.spawnRateMultiplier);
|
|
35
|
+
return world.distanceSinceLastObstacle > spawnThreshold && terminalWidth > 0;
|
|
36
|
+
}
|
|
37
|
+
export function spawnObstacle(world, terminalWidth) {
|
|
38
|
+
const types = getAvailableObstacleTypes(world.score);
|
|
39
|
+
const randomIndex = Math.floor(Math.random() * types.length);
|
|
40
|
+
const type = types[randomIndex] ?? ObstacleType.SmallCactus;
|
|
41
|
+
return {
|
|
42
|
+
type,
|
|
43
|
+
position: {
|
|
44
|
+
x: terminalWidth + SPAWN_X_OFFSET,
|
|
45
|
+
y: 0,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function moveObstacles(obstacles, speed) {
|
|
50
|
+
return obstacles
|
|
51
|
+
.map((obstacle) => ({
|
|
52
|
+
...obstacle,
|
|
53
|
+
position: {
|
|
54
|
+
...obstacle.position,
|
|
55
|
+
x: obstacle.position.x - speed,
|
|
56
|
+
},
|
|
57
|
+
}))
|
|
58
|
+
.filter((obstacle) => obstacle.position.x >= -10);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=obstacles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obstacles.js","sourceRoot":"","sources":["../../src/game/obstacles.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,SAAS,cAAc,CAAC,KAAa;IACnC,KAAK,MAAM,IAAI,IAAI,gBAAgB,EAAE,CAAC;QACpC,IAAI,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,MAAM,QAAQ,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;AAC3C,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAa;IAC9C,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAEnC,MAAM,KAAK,GAAmB,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAEzD,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAgB,EAAE,aAAqB;IACzE,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IAClE,MAAM,cAAc,GAAG,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAE5F,OAAO,KAAK,CAAC,yBAAyB,GAAG,cAAc,IAAI,aAAa,GAAG,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,aAAqB;IACnE,MAAM,KAAK,GAAG,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,YAAY,CAAC,WAAW,CAAC;IAE5D,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE;YACR,CAAC,EAAE,aAAa,GAAG,cAAc;YACjC,CAAC,EAAE,CAAC;SACL;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAqB,EAAE,KAAa;IAChE,OAAO,SAAS;SACb,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClB,GAAG,QAAQ;QACX,QAAQ,EAAE;YACR,GAAG,QAAQ,CAAC,QAAQ;YACpB,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,KAAK;SAC/B;KACF,CAAC,CAAC;SACF,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DinoState, type Obstacle, ObstacleType } from "./types.js";
|
|
2
|
+
export declare function getDefaultDinoY(): number;
|
|
3
|
+
export declare function startJump(dino: DinoState): DinoState;
|
|
4
|
+
export declare function startCrouch(dino: DinoState): DinoState;
|
|
5
|
+
export declare function stopCrouch(dino: DinoState): DinoState;
|
|
6
|
+
export declare function applyGravity(dino: DinoState): DinoState;
|
|
7
|
+
export declare function getObstacleY(type: ObstacleType): number;
|
|
8
|
+
export declare function checkCollision(dino: DinoState, obstacle: Obstacle): boolean;
|
|
9
|
+
//# sourceMappingURL=physics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/game/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,QAAQ,EAMb,YAAY,EACb,MAAM,YAAY,CAAC;AAmCpB,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAWpD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAWtD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAOrD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAwBvD;AAwBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAoBvD;AASD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAU3E"}
|