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.
- package/README.md +372 -0
- package/dist/AchievementNotification.d.ts +28 -0
- package/dist/AchievementNotification.d.ts.map +1 -0
- package/dist/AchievementNotification.js +74 -0
- package/dist/AchievementNotification.js.map +1 -0
- package/dist/GameSelector.d.ts +25 -0
- package/dist/GameSelector.d.ts.map +1 -0
- package/dist/GameSelector.js +105 -0
- package/dist/GameSelector.js.map +1 -0
- package/dist/HelpOverlay.d.ts +15 -0
- package/dist/HelpOverlay.d.ts.map +1 -0
- package/dist/HelpOverlay.js +134 -0
- package/dist/HelpOverlay.js.map +1 -0
- package/dist/Layout.d.ts +49 -0
- package/dist/Layout.d.ts.map +1 -0
- package/dist/Layout.js +83 -0
- package/dist/Layout.js.map +1 -0
- package/dist/Leaderboard.d.ts +46 -0
- package/dist/Leaderboard.d.ts.map +1 -0
- package/dist/Leaderboard.js +68 -0
- package/dist/Leaderboard.js.map +1 -0
- package/dist/LogViewer.d.ts +33 -0
- package/dist/LogViewer.d.ts.map +1 -0
- package/dist/LogViewer.js +179 -0
- package/dist/LogViewer.js.map +1 -0
- package/dist/LoopAlertOverlay.d.ts +15 -0
- package/dist/LoopAlertOverlay.d.ts.map +1 -0
- package/dist/LoopAlertOverlay.js +17 -0
- package/dist/LoopAlertOverlay.js.map +1 -0
- package/dist/LoopManagementPanel.d.ts +44 -0
- package/dist/LoopManagementPanel.d.ts.map +1 -0
- package/dist/LoopManagementPanel.js +220 -0
- package/dist/LoopManagementPanel.js.map +1 -0
- package/dist/SettingsMenu.d.ts +22 -0
- package/dist/SettingsMenu.d.ts.map +1 -0
- package/dist/SettingsMenu.js +367 -0
- package/dist/SettingsMenu.js.map +1 -0
- package/dist/SplitPane.d.ts +63 -0
- package/dist/SplitPane.d.ts.map +1 -0
- package/dist/SplitPane.js +104 -0
- package/dist/SplitPane.js.map +1 -0
- package/dist/StatsMenu.d.ts +15 -0
- package/dist/StatsMenu.d.ts.map +1 -0
- package/dist/StatsMenu.js +230 -0
- package/dist/StatsMenu.js.map +1 -0
- package/dist/StatusBar.d.ts +58 -0
- package/dist/StatusBar.d.ts.map +1 -0
- package/dist/StatusBar.js +106 -0
- package/dist/StatusBar.js.map +1 -0
- package/dist/__tests__/ralph-loop-parser.test.d.ts +2 -0
- package/dist/__tests__/ralph-loop-parser.test.d.ts.map +1 -0
- package/dist/__tests__/ralph-loop-parser.test.js +143 -0
- package/dist/__tests__/ralph-loop-parser.test.js.map +1 -0
- package/dist/claude-code-process.d.ts +76 -0
- package/dist/claude-code-process.d.ts.map +1 -0
- package/dist/claude-code-process.js +221 -0
- package/dist/claude-code-process.js.map +1 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +265 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +206 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +270 -0
- package/dist/config.js.map +1 -0
- package/dist/game-types.d.ts +177 -0
- package/dist/game-types.d.ts.map +1 -0
- package/dist/game-types.js +55 -0
- package/dist/game-types.js.map +1 -0
- package/dist/games/MinesweeperGame.d.ts +15 -0
- package/dist/games/MinesweeperGame.d.ts.map +1 -0
- package/dist/games/MinesweeperGame.js +555 -0
- package/dist/games/MinesweeperGame.js.map +1 -0
- package/dist/games/PongGame.d.ts +15 -0
- package/dist/games/PongGame.d.ts.map +1 -0
- package/dist/games/PongGame.js +379 -0
- package/dist/games/PongGame.js.map +1 -0
- package/dist/games/SnakeGame.d.ts +15 -0
- package/dist/games/SnakeGame.d.ts.map +1 -0
- package/dist/games/SnakeGame.js +333 -0
- package/dist/games/SnakeGame.js.map +1 -0
- package/dist/games/TetrisGame.d.ts +15 -0
- package/dist/games/TetrisGame.d.ts.map +1 -0
- package/dist/games/TetrisGame.js +654 -0
- package/dist/games/TetrisGame.js.map +1 -0
- package/dist/games/index.d.ts +23 -0
- package/dist/games/index.d.ts.map +1 -0
- package/dist/games/index.js +47 -0
- package/dist/games/index.js.map +1 -0
- package/dist/high-scores.d.ts +57 -0
- package/dist/high-scores.d.ts.map +1 -0
- package/dist/high-scores.js +230 -0
- package/dist/high-scores.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +264 -0
- package/dist/index.js.map +1 -0
- package/dist/ralph-loop-parser.d.ts +58 -0
- package/dist/ralph-loop-parser.d.ts.map +1 -0
- package/dist/ralph-loop-parser.js +315 -0
- package/dist/ralph-loop-parser.js.map +1 -0
- package/dist/stats.d.ts +142 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +521 -0
- package/dist/stats.js.map +1 -0
- package/dist/styled-components.d.ts +231 -0
- package/dist/styled-components.d.ts.map +1 -0
- package/dist/styled-components.js +192 -0
- package/dist/styled-components.js.map +1 -0
- package/dist/theme.d.ts +301 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +372 -0
- package/dist/theme.js.map +1 -0
- package/dist/themes.d.ts +117 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +296 -0
- package/dist/themes.js.map +1 -0
- package/dist/ui/index.d.ts +13 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +29 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/use-claude-code.d.ts +30 -0
- package/dist/use-claude-code.d.ts.map +1 -0
- package/dist/use-claude-code.js +84 -0
- package/dist/use-claude-code.js.map +1 -0
- package/dist/use-config.d.ts +58 -0
- package/dist/use-config.d.ts.map +1 -0
- package/dist/use-config.js +113 -0
- package/dist/use-config.js.map +1 -0
- package/dist/use-game-loop.d.ts +47 -0
- package/dist/use-game-loop.d.ts.map +1 -0
- package/dist/use-game-loop.js +136 -0
- package/dist/use-game-loop.js.map +1 -0
- package/dist/use-high-scores.d.ts +41 -0
- package/dist/use-high-scores.d.ts.map +1 -0
- package/dist/use-high-scores.js +94 -0
- package/dist/use-high-scores.js.map +1 -0
- package/dist/use-layout-state.d.ts +77 -0
- package/dist/use-layout-state.d.ts.map +1 -0
- package/dist/use-layout-state.js +160 -0
- package/dist/use-layout-state.js.map +1 -0
- package/dist/use-ralph-loop.d.ts +41 -0
- package/dist/use-ralph-loop.d.ts.map +1 -0
- package/dist/use-ralph-loop.js +106 -0
- package/dist/use-ralph-loop.js.map +1 -0
- package/dist/use-spinner.d.ts +46 -0
- package/dist/use-spinner.d.ts.map +1 -0
- package/dist/use-spinner.js +71 -0
- package/dist/use-spinner.js.map +1 -0
- package/dist/use-stats.d.ts +59 -0
- package/dist/use-stats.d.ts.map +1 -0
- package/dist/use-stats.js +150 -0
- package/dist/use-stats.js.map +1 -0
- package/dist/use-terminal-size.d.ts +29 -0
- package/dist/use-terminal-size.d.ts.map +1 -0
- package/dist/use-terminal-size.js +48 -0
- package/dist/use-terminal-size.js.map +1 -0
- package/dist/useTheme.d.ts +76 -0
- package/dist/useTheme.d.ts.map +1 -0
- package/dist/useTheme.js +136 -0
- package/dist/useTheme.js.map +1 -0
- 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
|