brainrot-cli 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.
Files changed (162) hide show
  1. package/README.md +372 -0
  2. package/dist/AchievementNotification.d.ts +28 -0
  3. package/dist/AchievementNotification.d.ts.map +1 -0
  4. package/dist/AchievementNotification.js +74 -0
  5. package/dist/AchievementNotification.js.map +1 -0
  6. package/dist/GameSelector.d.ts +25 -0
  7. package/dist/GameSelector.d.ts.map +1 -0
  8. package/dist/GameSelector.js +105 -0
  9. package/dist/GameSelector.js.map +1 -0
  10. package/dist/HelpOverlay.d.ts +15 -0
  11. package/dist/HelpOverlay.d.ts.map +1 -0
  12. package/dist/HelpOverlay.js +134 -0
  13. package/dist/HelpOverlay.js.map +1 -0
  14. package/dist/Layout.d.ts +49 -0
  15. package/dist/Layout.d.ts.map +1 -0
  16. package/dist/Layout.js +83 -0
  17. package/dist/Layout.js.map +1 -0
  18. package/dist/Leaderboard.d.ts +46 -0
  19. package/dist/Leaderboard.d.ts.map +1 -0
  20. package/dist/Leaderboard.js +68 -0
  21. package/dist/Leaderboard.js.map +1 -0
  22. package/dist/LogViewer.d.ts +33 -0
  23. package/dist/LogViewer.d.ts.map +1 -0
  24. package/dist/LogViewer.js +179 -0
  25. package/dist/LogViewer.js.map +1 -0
  26. package/dist/LoopAlertOverlay.d.ts +15 -0
  27. package/dist/LoopAlertOverlay.d.ts.map +1 -0
  28. package/dist/LoopAlertOverlay.js +17 -0
  29. package/dist/LoopAlertOverlay.js.map +1 -0
  30. package/dist/LoopManagementPanel.d.ts +44 -0
  31. package/dist/LoopManagementPanel.d.ts.map +1 -0
  32. package/dist/LoopManagementPanel.js +220 -0
  33. package/dist/LoopManagementPanel.js.map +1 -0
  34. package/dist/SettingsMenu.d.ts +22 -0
  35. package/dist/SettingsMenu.d.ts.map +1 -0
  36. package/dist/SettingsMenu.js +367 -0
  37. package/dist/SettingsMenu.js.map +1 -0
  38. package/dist/SplitPane.d.ts +63 -0
  39. package/dist/SplitPane.d.ts.map +1 -0
  40. package/dist/SplitPane.js +104 -0
  41. package/dist/SplitPane.js.map +1 -0
  42. package/dist/StatsMenu.d.ts +15 -0
  43. package/dist/StatsMenu.d.ts.map +1 -0
  44. package/dist/StatsMenu.js +230 -0
  45. package/dist/StatsMenu.js.map +1 -0
  46. package/dist/StatusBar.d.ts +58 -0
  47. package/dist/StatusBar.d.ts.map +1 -0
  48. package/dist/StatusBar.js +106 -0
  49. package/dist/StatusBar.js.map +1 -0
  50. package/dist/__tests__/ralph-loop-parser.test.d.ts +2 -0
  51. package/dist/__tests__/ralph-loop-parser.test.d.ts.map +1 -0
  52. package/dist/__tests__/ralph-loop-parser.test.js +143 -0
  53. package/dist/__tests__/ralph-loop-parser.test.js.map +1 -0
  54. package/dist/claude-code-process.d.ts +76 -0
  55. package/dist/claude-code-process.d.ts.map +1 -0
  56. package/dist/claude-code-process.js +221 -0
  57. package/dist/claude-code-process.js.map +1 -0
  58. package/dist/cli.d.ts +42 -0
  59. package/dist/cli.d.ts.map +1 -0
  60. package/dist/cli.js +265 -0
  61. package/dist/cli.js.map +1 -0
  62. package/dist/config.d.ts +206 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +270 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/game-types.d.ts +177 -0
  67. package/dist/game-types.d.ts.map +1 -0
  68. package/dist/game-types.js +55 -0
  69. package/dist/game-types.js.map +1 -0
  70. package/dist/games/MinesweeperGame.d.ts +15 -0
  71. package/dist/games/MinesweeperGame.d.ts.map +1 -0
  72. package/dist/games/MinesweeperGame.js +555 -0
  73. package/dist/games/MinesweeperGame.js.map +1 -0
  74. package/dist/games/PongGame.d.ts +15 -0
  75. package/dist/games/PongGame.d.ts.map +1 -0
  76. package/dist/games/PongGame.js +379 -0
  77. package/dist/games/PongGame.js.map +1 -0
  78. package/dist/games/SnakeGame.d.ts +15 -0
  79. package/dist/games/SnakeGame.d.ts.map +1 -0
  80. package/dist/games/SnakeGame.js +333 -0
  81. package/dist/games/SnakeGame.js.map +1 -0
  82. package/dist/games/TetrisGame.d.ts +15 -0
  83. package/dist/games/TetrisGame.d.ts.map +1 -0
  84. package/dist/games/TetrisGame.js +654 -0
  85. package/dist/games/TetrisGame.js.map +1 -0
  86. package/dist/games/index.d.ts +23 -0
  87. package/dist/games/index.d.ts.map +1 -0
  88. package/dist/games/index.js +47 -0
  89. package/dist/games/index.js.map +1 -0
  90. package/dist/high-scores.d.ts +57 -0
  91. package/dist/high-scores.d.ts.map +1 -0
  92. package/dist/high-scores.js +230 -0
  93. package/dist/high-scores.js.map +1 -0
  94. package/dist/index.d.ts +3 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +264 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/ralph-loop-parser.d.ts +58 -0
  99. package/dist/ralph-loop-parser.d.ts.map +1 -0
  100. package/dist/ralph-loop-parser.js +315 -0
  101. package/dist/ralph-loop-parser.js.map +1 -0
  102. package/dist/stats.d.ts +142 -0
  103. package/dist/stats.d.ts.map +1 -0
  104. package/dist/stats.js +521 -0
  105. package/dist/stats.js.map +1 -0
  106. package/dist/styled-components.d.ts +231 -0
  107. package/dist/styled-components.d.ts.map +1 -0
  108. package/dist/styled-components.js +192 -0
  109. package/dist/styled-components.js.map +1 -0
  110. package/dist/theme.d.ts +301 -0
  111. package/dist/theme.d.ts.map +1 -0
  112. package/dist/theme.js +372 -0
  113. package/dist/theme.js.map +1 -0
  114. package/dist/themes.d.ts +117 -0
  115. package/dist/themes.d.ts.map +1 -0
  116. package/dist/themes.js +296 -0
  117. package/dist/themes.js.map +1 -0
  118. package/dist/ui/index.d.ts +13 -0
  119. package/dist/ui/index.d.ts.map +1 -0
  120. package/dist/ui/index.js +29 -0
  121. package/dist/ui/index.js.map +1 -0
  122. package/dist/use-claude-code.d.ts +30 -0
  123. package/dist/use-claude-code.d.ts.map +1 -0
  124. package/dist/use-claude-code.js +84 -0
  125. package/dist/use-claude-code.js.map +1 -0
  126. package/dist/use-config.d.ts +58 -0
  127. package/dist/use-config.d.ts.map +1 -0
  128. package/dist/use-config.js +113 -0
  129. package/dist/use-config.js.map +1 -0
  130. package/dist/use-game-loop.d.ts +47 -0
  131. package/dist/use-game-loop.d.ts.map +1 -0
  132. package/dist/use-game-loop.js +136 -0
  133. package/dist/use-game-loop.js.map +1 -0
  134. package/dist/use-high-scores.d.ts +41 -0
  135. package/dist/use-high-scores.d.ts.map +1 -0
  136. package/dist/use-high-scores.js +94 -0
  137. package/dist/use-high-scores.js.map +1 -0
  138. package/dist/use-layout-state.d.ts +77 -0
  139. package/dist/use-layout-state.d.ts.map +1 -0
  140. package/dist/use-layout-state.js +160 -0
  141. package/dist/use-layout-state.js.map +1 -0
  142. package/dist/use-ralph-loop.d.ts +41 -0
  143. package/dist/use-ralph-loop.d.ts.map +1 -0
  144. package/dist/use-ralph-loop.js +106 -0
  145. package/dist/use-ralph-loop.js.map +1 -0
  146. package/dist/use-spinner.d.ts +46 -0
  147. package/dist/use-spinner.d.ts.map +1 -0
  148. package/dist/use-spinner.js +71 -0
  149. package/dist/use-spinner.js.map +1 -0
  150. package/dist/use-stats.d.ts +59 -0
  151. package/dist/use-stats.d.ts.map +1 -0
  152. package/dist/use-stats.js +150 -0
  153. package/dist/use-stats.js.map +1 -0
  154. package/dist/use-terminal-size.d.ts +29 -0
  155. package/dist/use-terminal-size.d.ts.map +1 -0
  156. package/dist/use-terminal-size.js +48 -0
  157. package/dist/use-terminal-size.js.map +1 -0
  158. package/dist/useTheme.d.ts +76 -0
  159. package/dist/useTheme.d.ts.map +1 -0
  160. package/dist/useTheme.js +136 -0
  161. package/dist/useTheme.js.map +1 -0
  162. package/package.json +58 -0
@@ -0,0 +1,654 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Tetris Game
4
+ *
5
+ * Classic Tetris game implemented with the game framework.
6
+ * Features: 7 standard pieces, rotation, line clearing, scoring, next piece preview.
7
+ */
8
+ import { Box, Text, useInput } from "ink";
9
+ import { useState, useCallback, useRef, useEffect } from "react";
10
+ import { useGameLoop } from "../use-game-loop.js";
11
+ import { useHighScores } from "../use-high-scores.js";
12
+ import { useGameSession } from "../use-stats.js";
13
+ import { Leaderboard, NewHighScoreBanner } from "../Leaderboard.js";
14
+ import { LoopAlertOverlay } from "../LoopAlertOverlay.js";
15
+ /** Tetris game metadata */
16
+ export const tetrisGameInfo = {
17
+ id: "tetris",
18
+ name: "Tetris",
19
+ description: "Classic Tetris - clear lines with falling blocks!",
20
+ controls: "Arrow keys to move/rotate, Space to drop, P to pause",
21
+ minWidth: 30,
22
+ minHeight: 20,
23
+ };
24
+ /** Board dimensions */
25
+ const BOARD_WIDTH = 10;
26
+ const BOARD_HEIGHT = 20;
27
+ /** Piece definitions with their rotations */
28
+ const PIECE_DEFINITIONS = {
29
+ I: {
30
+ blocks: [
31
+ { x: 0, y: 1 },
32
+ { x: 1, y: 1 },
33
+ { x: 2, y: 1 },
34
+ { x: 3, y: 1 },
35
+ ],
36
+ color: "cyan",
37
+ },
38
+ O: {
39
+ blocks: [
40
+ { x: 0, y: 0 },
41
+ { x: 1, y: 0 },
42
+ { x: 0, y: 1 },
43
+ { x: 1, y: 1 },
44
+ ],
45
+ color: "yellow",
46
+ },
47
+ T: {
48
+ blocks: [
49
+ { x: 1, y: 0 },
50
+ { x: 0, y: 1 },
51
+ { x: 1, y: 1 },
52
+ { x: 2, y: 1 },
53
+ ],
54
+ color: "magenta",
55
+ },
56
+ S: {
57
+ blocks: [
58
+ { x: 1, y: 0 },
59
+ { x: 2, y: 0 },
60
+ { x: 0, y: 1 },
61
+ { x: 1, y: 1 },
62
+ ],
63
+ color: "green",
64
+ },
65
+ Z: {
66
+ blocks: [
67
+ { x: 0, y: 0 },
68
+ { x: 1, y: 0 },
69
+ { x: 1, y: 1 },
70
+ { x: 2, y: 1 },
71
+ ],
72
+ color: "red",
73
+ },
74
+ J: {
75
+ blocks: [
76
+ { x: 0, y: 0 },
77
+ { x: 0, y: 1 },
78
+ { x: 1, y: 1 },
79
+ { x: 2, y: 1 },
80
+ ],
81
+ color: "blue",
82
+ },
83
+ L: {
84
+ blocks: [
85
+ { x: 2, y: 0 },
86
+ { x: 0, y: 1 },
87
+ { x: 1, y: 1 },
88
+ { x: 2, y: 1 },
89
+ ],
90
+ color: "#FFA500", // Orange
91
+ },
92
+ };
93
+ const PIECE_TYPES = ["I", "O", "T", "S", "Z", "J", "L"];
94
+ /** Create a new piece */
95
+ function createPiece(type) {
96
+ const def = PIECE_DEFINITIONS[type];
97
+ return {
98
+ type,
99
+ blocks: [...def.blocks],
100
+ color: def.color,
101
+ };
102
+ }
103
+ /** Get a random piece type */
104
+ function randomPieceType() {
105
+ return PIECE_TYPES[Math.floor(Math.random() * PIECE_TYPES.length)];
106
+ }
107
+ /** Create empty board */
108
+ function createEmptyBoard() {
109
+ return Array(BOARD_HEIGHT)
110
+ .fill(null)
111
+ .map(() => Array(BOARD_WIDTH)
112
+ .fill(null)
113
+ .map(() => ({ filled: false })));
114
+ }
115
+ /** Rotate piece 90 degrees clockwise */
116
+ function rotatePiece(piece) {
117
+ if (piece.type === "O")
118
+ return piece; // O doesn't rotate
119
+ // Find bounding box
120
+ const minX = Math.min(...piece.blocks.map((b) => b.x));
121
+ const minY = Math.min(...piece.blocks.map((b) => b.y));
122
+ const maxY = Math.max(...piece.blocks.map((b) => b.y));
123
+ const height = maxY - minY + 1;
124
+ // Rotate around center
125
+ const newBlocks = piece.blocks.map((block) => {
126
+ const relX = block.x - minX;
127
+ const relY = block.y - minY;
128
+ return {
129
+ x: height - 1 - relY + minX,
130
+ y: relX + minY,
131
+ };
132
+ });
133
+ // Normalize to start from 0
134
+ const newMinX = Math.min(...newBlocks.map((b) => b.x));
135
+ const newMinY = Math.min(...newBlocks.map((b) => b.y));
136
+ return {
137
+ ...piece,
138
+ blocks: newBlocks.map((b) => ({
139
+ x: b.x - newMinX,
140
+ y: b.y - newMinY,
141
+ })),
142
+ };
143
+ }
144
+ /** Check if piece position is valid */
145
+ function isValidPosition(board, piece, position) {
146
+ for (const block of piece.blocks) {
147
+ const x = position.x + block.x;
148
+ const y = position.y + block.y;
149
+ // Check bounds
150
+ if (x < 0 || x >= BOARD_WIDTH || y >= BOARD_HEIGHT) {
151
+ return false;
152
+ }
153
+ // Allow blocks above the board
154
+ if (y < 0)
155
+ continue;
156
+ // Check collision with placed blocks
157
+ if (board[y][x].filled) {
158
+ return false;
159
+ }
160
+ }
161
+ return true;
162
+ }
163
+ /** Place piece on board */
164
+ function placePiece(board, piece, position) {
165
+ const newBoard = board.map((row) => row.map((cell) => ({ ...cell })));
166
+ for (const block of piece.blocks) {
167
+ const x = position.x + block.x;
168
+ const y = position.y + block.y;
169
+ if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) {
170
+ newBoard[y][x] = { filled: true, color: piece.color };
171
+ }
172
+ }
173
+ return newBoard;
174
+ }
175
+ /** Find completed lines */
176
+ function findCompletedLines(board) {
177
+ const lines = [];
178
+ for (let y = 0; y < BOARD_HEIGHT; y++) {
179
+ if (board[y].every((cell) => cell.filled)) {
180
+ lines.push(y);
181
+ }
182
+ }
183
+ return lines;
184
+ }
185
+ /** Clear completed lines */
186
+ function clearLines(board, lines) {
187
+ if (lines.length === 0)
188
+ return board;
189
+ const newBoard = board.filter((_, y) => !lines.includes(y));
190
+ // Add empty rows at top
191
+ while (newBoard.length < BOARD_HEIGHT) {
192
+ newBoard.unshift(Array(BOARD_WIDTH)
193
+ .fill(null)
194
+ .map(() => ({ filled: false })));
195
+ }
196
+ return newBoard;
197
+ }
198
+ /** Calculate score for cleared lines */
199
+ function calculateLineScore(lines, level) {
200
+ const baseScores = {
201
+ 1: 100,
202
+ 2: 300,
203
+ 3: 500,
204
+ 4: 800, // Tetris!
205
+ };
206
+ return (baseScores[lines] || 0) * (level + 1);
207
+ }
208
+ /** Calculate drop speed based on level */
209
+ function getDropSpeed(level) {
210
+ // Starts at 1000ms, decreases with level
211
+ return Math.max(100, 1000 - level * 80);
212
+ }
213
+ /** Create initial game state */
214
+ function createInitialState() {
215
+ const firstPiece = randomPieceType();
216
+ const nextPiece = randomPieceType();
217
+ return {
218
+ board: createEmptyBoard(),
219
+ currentPiece: createPiece(firstPiece),
220
+ piecePosition: { x: Math.floor((BOARD_WIDTH - 4) / 2), y: 0 },
221
+ nextPiece,
222
+ score: 0,
223
+ level: 0,
224
+ lines: 0,
225
+ status: "playing",
226
+ clearingLines: [],
227
+ clearAnimFrame: 0,
228
+ leaderboardPosition: 0,
229
+ };
230
+ }
231
+ /** Next piece preview component */
232
+ function NextPiecePreview({ pieceType }) {
233
+ const piece = createPiece(pieceType);
234
+ const grid = Array(4)
235
+ .fill(null)
236
+ .map(() => Array(4).fill(" "));
237
+ // Center the piece in the preview
238
+ const offsetX = piece.type === "I" ? 0 : 1;
239
+ const offsetY = piece.type === "I" ? 1 : 1;
240
+ for (const block of piece.blocks) {
241
+ const y = block.y + offsetY;
242
+ const x = block.x + offsetX;
243
+ if (y >= 0 && y < 4 && x >= 0 && x < 4) {
244
+ grid[y][x] = "█";
245
+ }
246
+ }
247
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Next:" }), grid.map((row, y) => (_jsx(Box, { children: row.map((cell, x) => (_jsx(Text, { color: cell === "█" ? piece.color : undefined, children: cell }, x))) }, y)))] }));
248
+ }
249
+ /** Game board component */
250
+ function GameBoard({ board, currentPiece, piecePosition, clearingLines, clearAnimFrame, }) {
251
+ // Create display board
252
+ const display = board.map((row, _y) => row.map((cell) => {
253
+ if (cell.filled) {
254
+ return { char: "█", color: cell.color };
255
+ }
256
+ return { char: " " };
257
+ }));
258
+ // Add current piece
259
+ if (currentPiece) {
260
+ for (const block of currentPiece.blocks) {
261
+ const x = piecePosition.x + block.x;
262
+ const y = piecePosition.y + block.y;
263
+ if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) {
264
+ display[y][x] = { char: "█", color: currentPiece.color };
265
+ }
266
+ }
267
+ // Add ghost piece (drop preview)
268
+ let ghostY = piecePosition.y;
269
+ while (isValidPosition(board, currentPiece, { x: piecePosition.x, y: ghostY + 1 })) {
270
+ ghostY++;
271
+ }
272
+ if (ghostY !== piecePosition.y) {
273
+ for (const block of currentPiece.blocks) {
274
+ const x = piecePosition.x + block.x;
275
+ const y = ghostY + block.y;
276
+ if (y >= 0 &&
277
+ y < BOARD_HEIGHT &&
278
+ x >= 0 &&
279
+ x < BOARD_WIDTH &&
280
+ !display[y][x].color) {
281
+ display[y][x] = { char: "░", color: "gray" };
282
+ }
283
+ }
284
+ }
285
+ }
286
+ // Apply line clearing animation
287
+ if (clearingLines.length > 0) {
288
+ const flashOn = clearAnimFrame % 2 === 0;
289
+ for (const lineY of clearingLines) {
290
+ for (let x = 0; x < BOARD_WIDTH; x++) {
291
+ display[lineY][x] = {
292
+ char: flashOn ? "█" : " ",
293
+ color: flashOn ? "white" : undefined,
294
+ };
295
+ }
296
+ }
297
+ }
298
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "┌" + "──".repeat(BOARD_WIDTH) + "┐" }), display.map((row, y) => (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "\u2502" }), row.map((cell, x) => (_jsxs(Text, { color: cell.color, children: [cell.char, cell.char] }, x))), _jsx(Text, { color: "gray", children: "\u2502" })] }, y))), _jsx(Text, { color: "gray", children: "└" + "──".repeat(BOARD_WIDTH) + "┘" })] }));
299
+ }
300
+ /** Game over overlay */
301
+ function GameOverOverlay({ score, leaderboardPosition, }) {
302
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 1, children: [_jsx(Text, { bold: true, color: "red", children: "GAME OVER" }), _jsxs(Text, { children: ["Score: ", _jsx(Text, { color: "yellow", children: score })] }), leaderboardPosition > 0 && (_jsx(NewHighScoreBanner, { position: leaderboardPosition, score: score })), _jsx(Text, { dimColor: true, children: "Press R to restart | H for leaderboard" })] }));
303
+ }
304
+ /** Paused overlay */
305
+ function PausedOverlay() {
306
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "PAUSED" }), _jsx(Text, { dimColor: true, children: "Press P to resume" })] }));
307
+ }
308
+ /** HUD component */
309
+ function GameHUD({ score, highScore, level, lines, fps, }) {
310
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Score" }), _jsx(Text, { color: "yellow", children: score })] }), _jsxs(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Level" }), _jsx(Text, { color: "cyan", children: level })] }), _jsxs(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Lines" }), _jsx(Text, { color: "green", children: lines })] }), _jsxs(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { dimColor: true, children: "High" }), _jsx(Text, { dimColor: true, children: highScore })] }), _jsxs(Text, { dimColor: true, children: [fps, " FPS"] })] }));
311
+ }
312
+ /**
313
+ * Tetris game component
314
+ */
315
+ export function TetrisGame({ hasFocus, onExit, loopAttention, onLoopAlertDismiss, onGameStateChange }) {
316
+ const [state, setState] = useState(() => createInitialState());
317
+ const lastDropTime = useRef(0);
318
+ const clearAnimTimer = useRef(0);
319
+ const scoreSubmittedRef = useRef(false);
320
+ const statsSubmittedRef = useRef(false);
321
+ const [showLoopAlert, setShowLoopAlert] = useState(false);
322
+ const [wasPlayingBeforeAlert, setWasPlayingBeforeAlert] = useState(false);
323
+ // High score persistence
324
+ const { highScore, leaderboard, submitScore } = useHighScores("tetris");
325
+ // Stats tracking
326
+ const { startSession, endSession, isSessionActive } = useGameSession("tetris");
327
+ // Report game state changes to status bar
328
+ useEffect(() => {
329
+ const mappedStatus = state.status === "leaderboard" ? "game_over" : state.status;
330
+ onGameStateChange?.({
331
+ score: state.score,
332
+ status: mappedStatus,
333
+ highScore,
334
+ });
335
+ }, [state.score, state.status, highScore, onGameStateChange]);
336
+ // Auto-pause when loop needs attention
337
+ useEffect(() => {
338
+ if (loopAttention?.needsAttention && state.status === "playing") {
339
+ setWasPlayingBeforeAlert(true);
340
+ setShowLoopAlert(true);
341
+ setState((prev) => ({ ...prev, status: "paused" }));
342
+ }
343
+ else if (!loopAttention?.needsAttention && showLoopAlert) {
344
+ setShowLoopAlert(false);
345
+ // Auto-resume if we were playing before the alert
346
+ if (wasPlayingBeforeAlert) {
347
+ setState((prev) => {
348
+ if (prev.status === "paused") {
349
+ return { ...prev, status: "playing" };
350
+ }
351
+ return prev;
352
+ });
353
+ setWasPlayingBeforeAlert(false);
354
+ }
355
+ }
356
+ }, [loopAttention?.needsAttention, state.status, showLoopAlert, wasPlayingBeforeAlert]);
357
+ // Start session when game starts
358
+ useEffect(() => {
359
+ if (state.status === "playing" && !isSessionActive) {
360
+ startSession();
361
+ }
362
+ }, [state.status, isSessionActive, startSession]);
363
+ // Submit score when game ends
364
+ useEffect(() => {
365
+ if (state.status === "game_over" && !scoreSubmittedRef.current && state.score > 0) {
366
+ scoreSubmittedRef.current = true;
367
+ submitScore(state.score, { level: state.level, lines: state.lines }).then((position) => {
368
+ if (position > 0) {
369
+ setState((prev) => ({ ...prev, leaderboardPosition: position }));
370
+ }
371
+ });
372
+ }
373
+ // Record stats when game ends
374
+ if (state.status === "game_over" && !statsSubmittedRef.current) {
375
+ statsSubmittedRef.current = true;
376
+ void endSession(state.score, undefined, { linesCleared: state.lines });
377
+ }
378
+ }, [state.status, state.score, state.level, state.lines, submitScore, endSession]);
379
+ // Lock piece and check for lines
380
+ const lockPiece = useCallback(() => {
381
+ setState((prev) => {
382
+ if (!prev.currentPiece)
383
+ return prev;
384
+ const newBoard = placePiece(prev.board, prev.currentPiece, prev.piecePosition);
385
+ const completedLines = findCompletedLines(newBoard);
386
+ if (completedLines.length > 0) {
387
+ // Start line clearing animation
388
+ return {
389
+ ...prev,
390
+ board: newBoard,
391
+ currentPiece: null,
392
+ clearingLines: completedLines,
393
+ clearAnimFrame: 0,
394
+ };
395
+ }
396
+ // No lines to clear, spawn new piece immediately
397
+ const newPiece = createPiece(prev.nextPiece);
398
+ const startPosition = { x: Math.floor((BOARD_WIDTH - 4) / 2), y: 0 };
399
+ if (!isValidPosition(newBoard, newPiece, startPosition)) {
400
+ return {
401
+ ...prev,
402
+ board: newBoard,
403
+ status: "game_over",
404
+ };
405
+ }
406
+ return {
407
+ ...prev,
408
+ board: newBoard,
409
+ currentPiece: newPiece,
410
+ piecePosition: startPosition,
411
+ nextPiece: randomPieceType(),
412
+ };
413
+ });
414
+ }, []);
415
+ // Move piece
416
+ const movePiece = useCallback((dx, dy) => {
417
+ setState((prev) => {
418
+ if (!prev.currentPiece || prev.status !== "playing")
419
+ return prev;
420
+ const newPosition = {
421
+ x: prev.piecePosition.x + dx,
422
+ y: prev.piecePosition.y + dy,
423
+ };
424
+ if (isValidPosition(prev.board, prev.currentPiece, newPosition)) {
425
+ return { ...prev, piecePosition: newPosition };
426
+ }
427
+ // If moving down and invalid, lock piece
428
+ if (dy > 0) {
429
+ lockPiece();
430
+ }
431
+ return prev;
432
+ });
433
+ }, [lockPiece]);
434
+ // Rotate piece
435
+ const rotate = useCallback(() => {
436
+ setState((prev) => {
437
+ if (!prev.currentPiece || prev.status !== "playing")
438
+ return prev;
439
+ const rotated = rotatePiece(prev.currentPiece);
440
+ // Try normal rotation
441
+ if (isValidPosition(prev.board, rotated, prev.piecePosition)) {
442
+ return { ...prev, currentPiece: rotated };
443
+ }
444
+ // Wall kick attempts
445
+ const kicks = [
446
+ { x: -1, y: 0 },
447
+ { x: 1, y: 0 },
448
+ { x: -2, y: 0 },
449
+ { x: 2, y: 0 },
450
+ { x: 0, y: -1 },
451
+ ];
452
+ for (const kick of kicks) {
453
+ const kickedPosition = {
454
+ x: prev.piecePosition.x + kick.x,
455
+ y: prev.piecePosition.y + kick.y,
456
+ };
457
+ if (isValidPosition(prev.board, rotated, kickedPosition)) {
458
+ return {
459
+ ...prev,
460
+ currentPiece: rotated,
461
+ piecePosition: kickedPosition,
462
+ };
463
+ }
464
+ }
465
+ return prev;
466
+ });
467
+ }, []);
468
+ // Hard drop
469
+ const hardDrop = useCallback(() => {
470
+ setState((prev) => {
471
+ if (!prev.currentPiece || prev.status !== "playing")
472
+ return prev;
473
+ let dropY = prev.piecePosition.y;
474
+ while (isValidPosition(prev.board, prev.currentPiece, {
475
+ x: prev.piecePosition.x,
476
+ y: dropY + 1,
477
+ })) {
478
+ dropY++;
479
+ }
480
+ // Add points for hard drop (2 per cell)
481
+ const dropDistance = dropY - prev.piecePosition.y;
482
+ const newScore = prev.score + dropDistance * 2;
483
+ const newBoard = placePiece(prev.board, prev.currentPiece, {
484
+ x: prev.piecePosition.x,
485
+ y: dropY,
486
+ });
487
+ const completedLines = findCompletedLines(newBoard);
488
+ if (completedLines.length > 0) {
489
+ return {
490
+ ...prev,
491
+ board: newBoard,
492
+ currentPiece: null,
493
+ score: newScore,
494
+ clearingLines: completedLines,
495
+ clearAnimFrame: 0,
496
+ };
497
+ }
498
+ const newPiece = createPiece(prev.nextPiece);
499
+ const startPosition = { x: Math.floor((BOARD_WIDTH - 4) / 2), y: 0 };
500
+ if (!isValidPosition(newBoard, newPiece, startPosition)) {
501
+ return {
502
+ ...prev,
503
+ board: newBoard,
504
+ score: newScore,
505
+ status: "game_over",
506
+ };
507
+ }
508
+ return {
509
+ ...prev,
510
+ board: newBoard,
511
+ currentPiece: newPiece,
512
+ piecePosition: startPosition,
513
+ nextPiece: randomPieceType(),
514
+ score: newScore,
515
+ };
516
+ });
517
+ }, []);
518
+ // Game loop
519
+ const { loopInfo } = useGameLoop({
520
+ targetFps: 30,
521
+ isActive: hasFocus && state.status === "playing",
522
+ onTick: (info) => {
523
+ // Handle line clearing animation
524
+ if (state.clearingLines.length > 0) {
525
+ clearAnimTimer.current += info.deltaTime;
526
+ if (clearAnimTimer.current >= 100) {
527
+ // 100ms per frame
528
+ clearAnimTimer.current = 0;
529
+ setState((prev) => {
530
+ const newFrame = prev.clearAnimFrame + 1;
531
+ if (newFrame >= 4) {
532
+ // Animation complete, clear lines
533
+ const clearedBoard = clearLines(prev.board, prev.clearingLines);
534
+ const lineCount = prev.clearingLines.length;
535
+ const lineScore = calculateLineScore(lineCount, prev.level);
536
+ const newLines = prev.lines + lineCount;
537
+ const newLevel = Math.floor(newLines / 10);
538
+ // Spawn new piece
539
+ const newPiece = createPiece(prev.nextPiece);
540
+ const startPosition = {
541
+ x: Math.floor((BOARD_WIDTH - 4) / 2),
542
+ y: 0,
543
+ };
544
+ if (!isValidPosition(clearedBoard, newPiece, startPosition)) {
545
+ return {
546
+ ...prev,
547
+ board: clearedBoard,
548
+ score: prev.score + lineScore,
549
+ lines: newLines,
550
+ level: newLevel,
551
+ clearingLines: [],
552
+ clearAnimFrame: 0,
553
+ status: "game_over",
554
+ };
555
+ }
556
+ return {
557
+ ...prev,
558
+ board: clearedBoard,
559
+ currentPiece: newPiece,
560
+ piecePosition: startPosition,
561
+ nextPiece: randomPieceType(),
562
+ score: prev.score + lineScore,
563
+ lines: newLines,
564
+ level: newLevel,
565
+ clearingLines: [],
566
+ clearAnimFrame: 0,
567
+ };
568
+ }
569
+ return { ...prev, clearAnimFrame: newFrame };
570
+ });
571
+ }
572
+ return;
573
+ }
574
+ // Auto drop
575
+ const dropSpeed = getDropSpeed(state.level);
576
+ if (info.elapsedTime - lastDropTime.current >= dropSpeed) {
577
+ movePiece(0, 1);
578
+ lastDropTime.current = info.elapsedTime;
579
+ }
580
+ },
581
+ });
582
+ // Handle input
583
+ useInput((input, key) => {
584
+ if (!hasFocus)
585
+ return;
586
+ // Exit
587
+ if (input === "q" || input === "Q" || key.escape) {
588
+ onExit();
589
+ return;
590
+ }
591
+ // Restart
592
+ if (input === "r" || input === "R") {
593
+ scoreSubmittedRef.current = false;
594
+ statsSubmittedRef.current = false;
595
+ setState(createInitialState());
596
+ lastDropTime.current = 0;
597
+ clearAnimTimer.current = 0;
598
+ return;
599
+ }
600
+ // Show leaderboard (when game over or paused)
601
+ if ((input === "h" || input === "H") && state.status !== "playing") {
602
+ setState((prev) => ({
603
+ ...prev,
604
+ status: prev.status === "leaderboard" ? "game_over" : "leaderboard",
605
+ }));
606
+ return;
607
+ }
608
+ // Dismiss loop alert with Enter key
609
+ if (key.return && showLoopAlert) {
610
+ setShowLoopAlert(false);
611
+ onLoopAlertDismiss?.();
612
+ return;
613
+ }
614
+ // Pause
615
+ if (input === "p" || input === "P") {
616
+ // If we're showing loop alert, dismiss it and resume
617
+ if (showLoopAlert) {
618
+ setShowLoopAlert(false);
619
+ setWasPlayingBeforeAlert(false);
620
+ }
621
+ setState((prev) => ({
622
+ ...prev,
623
+ status: prev.status === "playing" ? "paused" : "playing",
624
+ }));
625
+ return;
626
+ }
627
+ if (state.status !== "playing" || state.clearingLines.length > 0)
628
+ return;
629
+ // Movement
630
+ if (key.leftArrow || input === "a" || input === "A") {
631
+ movePiece(-1, 0);
632
+ }
633
+ else if (key.rightArrow || input === "d" || input === "D") {
634
+ movePiece(1, 0);
635
+ }
636
+ else if (key.downArrow || input === "s" || input === "S") {
637
+ movePiece(0, 1);
638
+ setState((prev) => ({ ...prev, score: prev.score + 1 })); // Soft drop bonus
639
+ }
640
+ else if (key.upArrow || input === "w" || input === "W") {
641
+ rotate();
642
+ }
643
+ else if (input === " ") {
644
+ hardDrop();
645
+ }
646
+ }, { isActive: hasFocus });
647
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [state.status === "leaderboard" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(Leaderboard, { title: "Tetris High Scores", scores: leaderboard, highlightPosition: state.leaderboardPosition }) })) : state.status === "game_over" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(GameOverOverlay, { score: state.score, leaderboardPosition: state.leaderboardPosition }) })) : state.status === "paused" && showLoopAlert && loopAttention ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(LoopAlertOverlay, { attention: loopAttention, onDismiss: onLoopAlertDismiss }) })) : state.status === "paused" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx(PausedOverlay, {}) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(GameBoard, { board: state.board, currentPiece: state.currentPiece, piecePosition: state.piecePosition, clearingLines: state.clearingLines, clearAnimFrame: state.clearAnimFrame }), _jsxs(Box, { flexDirection: "column", children: [_jsx(NextPiecePreview, { pieceType: state.nextPiece }), _jsx(GameHUD, { score: state.score, highScore: highScore, level: state.level, lines: state.lines, fps: loopInfo.fps })] })] })), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: hasFocus
648
+ ? state.status === "leaderboard"
649
+ ? "H: Back | R: Restart | Q: Exit"
650
+ : "←→: Move | ↑: Rotate | ↓: Soft | Space: Drop | P: Pause | R: Restart | H: Scores | Q: Exit"
651
+ : "Press Tab to focus" }) })] }));
652
+ }
653
+ export default TetrisGame;
654
+ //# sourceMappingURL=TetrisGame.js.map