@hypersocial/cli-games 0.2.1 → 0.2.2

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/dist/cli.js CHANGED
@@ -537,6 +537,15 @@ function navigateMenu(currentSelection, itemCount, key, domEvent) {
537
537
  }
538
538
  return { newSelection, confirmed };
539
539
  }
540
+ function checkShortcut(items, key) {
541
+ for (let i = 0; i < items.length; i++) {
542
+ const shortcut = items[i].shortcut;
543
+ if (shortcut && key === shortcut.toLowerCase()) {
544
+ return i;
545
+ }
546
+ }
547
+ return -1;
548
+ }
540
549
  function renderSimpleMenu(items, selection, options) {
541
550
  const themeColor = getCurrentThemeColor();
542
551
  const { centerX, startY, showShortcuts = true } = options;
@@ -683,8 +692,8 @@ var TILE_BG_COLORS = {
683
692
  };
684
693
  function run2048Game(terminal) {
685
694
  const themeColor = getCurrentThemeColor();
686
- const MIN_COLS = 36;
687
- const MIN_ROWS = 16;
695
+ const MIN_COLS2 = 36;
696
+ const MIN_ROWS2 = 16;
688
697
  const GRID_SIZE = 4;
689
698
  const TILE_WIDTH = 8;
690
699
  const TILE_HEIGHT = 3;
@@ -748,7 +757,7 @@ function run2048Game(terminal) {
748
757
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
749
758
  scorePopups.push({ x, y, text, frames: 20, color });
750
759
  }
751
- function triggerShake2(frames, intensity) {
760
+ function triggerShake3(frames, intensity) {
752
761
  shakeFrames = frames;
753
762
  shakeIntensity = intensity;
754
763
  }
@@ -872,13 +881,13 @@ function run2048Game(terminal) {
872
881
  if (anyMoved) {
873
882
  score += totalMergeScore;
874
883
  if (totalMergeScore > 0) {
875
- triggerShake2(4, 1);
884
+ triggerShake3(4, 1);
876
885
  }
877
886
  spawnTile();
878
887
  if (!won && !continuedAfterWin && has2048()) {
879
888
  won = true;
880
889
  if (score > highScore) highScore = score;
881
- triggerShake2(8, 2);
890
+ triggerShake3(8, 2);
882
891
  for (let y = 0; y < GRID_SIZE; y++) {
883
892
  for (let x = 0; x < GRID_SIZE; x++) {
884
893
  if (grid[y][x] === 2048) {
@@ -964,12 +973,12 @@ function run2048Game(terminal) {
964
973
  if (shakeFrames > 0) shakeFrames--;
965
974
  const cols = terminal.cols;
966
975
  const rows = terminal.rows;
967
- if (cols < MIN_COLS || rows < MIN_ROWS) {
976
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
968
977
  const msg1 = "Terminal too small!";
969
- const needWidth = cols < MIN_COLS;
970
- const needHeight = rows < MIN_ROWS;
978
+ const needWidth = cols < MIN_COLS2;
979
+ const needHeight = rows < MIN_ROWS2;
971
980
  let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider ->" : "Make pane taller v";
972
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
981
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
973
982
  const centerX = Math.floor(cols / 2);
974
983
  const centerY = Math.floor(rows / 2);
975
984
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -1281,8 +1290,8 @@ function run2048Game(terminal) {
1281
1290
  // src/games/asteroids/index.ts
1282
1291
  function runAsteroidsGame(terminal) {
1283
1292
  const themeColor = getCurrentThemeColor();
1284
- const MIN_COLS = 35;
1285
- const MIN_ROWS = 16;
1293
+ const MIN_COLS2 = 35;
1294
+ const MIN_ROWS2 = 16;
1286
1295
  let GAME_WIDTH = 30;
1287
1296
  let GAME_HEIGHT = 12;
1288
1297
  const SHIP_TURN_SPEED = 0.2;
@@ -1380,7 +1389,7 @@ function runAsteroidsGame(terminal) {
1380
1389
  if (value >= max) return value - max;
1381
1390
  return value;
1382
1391
  }
1383
- function triggerShake2(frames, intensity) {
1392
+ function triggerShake3(frames, intensity) {
1384
1393
  shakeFrames = frames;
1385
1394
  shakeIntensity = intensity;
1386
1395
  }
@@ -1417,7 +1426,7 @@ function runAsteroidsGame(terminal) {
1417
1426
  if (invincibilityFrames > 0) return;
1418
1427
  lives--;
1419
1428
  spawnParticles3(ship.x, ship.y, 15, "\x1B[91m");
1420
- triggerShake2(10, 3);
1429
+ triggerShake3(10, 3);
1421
1430
  if (lives <= 0) {
1422
1431
  gameOver = true;
1423
1432
  if (score > highScore) highScore = score;
@@ -1498,7 +1507,7 @@ function runAsteroidsGame(terminal) {
1498
1507
  bullets.splice(j, 1);
1499
1508
  splitAsteroid(ast);
1500
1509
  asteroids.splice(i, 1);
1501
- triggerShake2(4, 1);
1510
+ triggerShake3(4, 1);
1502
1511
  break;
1503
1512
  }
1504
1513
  }
@@ -1528,9 +1537,9 @@ function runAsteroidsGame(terminal) {
1528
1537
  if (shakeFrames > 0) shakeFrames--;
1529
1538
  const cols = terminal.cols;
1530
1539
  const rows = terminal.rows;
1531
- if (cols < MIN_COLS || rows < MIN_ROWS) {
1540
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
1532
1541
  const msg = "Terminal too small!";
1533
- const need = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
1542
+ const need = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
1534
1543
  output += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor((cols - msg.length) / 2))}H${themeColor}${msg}\x1B[0m`;
1535
1544
  output += `\x1B[${Math.floor(rows / 2) + 2};${Math.max(1, Math.floor((cols - need.length) / 2))}H\x1B[2m${need}\x1B[0m`;
1536
1545
  terminal.write(output);
@@ -1721,8 +1730,8 @@ function runAsteroidsGame(terminal) {
1721
1730
  // src/games/breakout/index.ts
1722
1731
  function runBreakoutGame(terminal) {
1723
1732
  const themeColor = getCurrentThemeColor();
1724
- const MIN_COLS = 40;
1725
- const MIN_ROWS = 18;
1733
+ const MIN_COLS2 = 40;
1734
+ const MIN_ROWS2 = 18;
1726
1735
  const GAME_WIDTH = 46;
1727
1736
  const GAME_HEIGHT = 20;
1728
1737
  const PADDLE_WIDTH_NORMAL = 7;
@@ -1791,7 +1800,7 @@ function runBreakoutGame(terminal) {
1791
1800
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
1792
1801
  scorePopups.push({ x, y, text, frames: 18, color });
1793
1802
  }
1794
- function triggerShake2(frames, intensity) {
1803
+ function triggerShake3(frames, intensity) {
1795
1804
  shakeFrames = frames;
1796
1805
  shakeIntensity = intensity;
1797
1806
  }
@@ -1848,7 +1857,7 @@ function runBreakoutGame(terminal) {
1848
1857
  powerUps.push({ x, y, type: types[Math.floor(Math.random() * types.length)], vy: 0.15 });
1849
1858
  }
1850
1859
  function applyPowerUp(type) {
1851
- triggerShake2(8, 2);
1860
+ triggerShake3(8, 2);
1852
1861
  borderFlash = 15;
1853
1862
  switch (type) {
1854
1863
  case "multiball": {
@@ -2024,7 +2033,7 @@ function runBreakoutGame(terminal) {
2024
2033
  const totalPts = basePts + comboBonus;
2025
2034
  score += totalPts;
2026
2035
  const intensity = Math.min(comboCount, 8);
2027
- triggerShake2(3 + intensity, 1 + Math.floor(intensity / 3));
2036
+ triggerShake3(3 + intensity, 1 + Math.floor(intensity / 3));
2028
2037
  spawnParticles3(brick.x + brick.width / 2, brick.y, 6 + intensity, brickColors[brick.type]);
2029
2038
  addScorePopup2(brick.x + 1, brick.y - 1, comboCount > 1 ? `+${totalPts}!` : `+${totalPts}`, brickColors[brick.type]);
2030
2039
  spawnPowerUp(brick.x + brick.width / 2, brick.y);
@@ -2042,12 +2051,12 @@ function runBreakoutGame(terminal) {
2042
2051
  if (lives <= 0) {
2043
2052
  gameOver = true;
2044
2053
  if (score > highScore) highScore = score;
2045
- triggerShake2(20, 4);
2054
+ triggerShake3(20, 4);
2046
2055
  spawnParticles3(paddleX, GAME_HEIGHT - 2, 15, "\x1B[1;91m", ["\u2717", "\u2620", "\xD7", "\u2593"]);
2047
2056
  } else {
2048
2057
  ballAttached = true;
2049
2058
  balls = [{ x: paddleX, y: GAME_HEIGHT - 3, vx: 0, vy: 0, active: true }];
2050
- triggerShake2(10, 2);
2059
+ triggerShake3(10, 2);
2051
2060
  }
2052
2061
  }
2053
2062
  const aliveBricks = bricks.filter((b) => b.alive);
@@ -2056,7 +2065,7 @@ function runBreakoutGame(terminal) {
2056
2065
  gameOver = true;
2057
2066
  level++;
2058
2067
  if (score > highScore) highScore = score;
2059
- triggerShake2(12, 2);
2068
+ triggerShake3(12, 2);
2060
2069
  for (let i = 0; i < 5; i++) setTimeout(() => spawnParticles3(Math.random() * GAME_WIDTH, Math.random() * GAME_HEIGHT / 2, 15, brickColors[Math.floor(Math.random() * brickColors.length)], ["\u2605", "\u2726", "\u25C6", "\u25CF", "\u2727"]), i * 100);
2061
2070
  }
2062
2071
  }
@@ -2065,11 +2074,11 @@ function runBreakoutGame(terminal) {
2065
2074
  if (shakeFrames > 0) shakeFrames--;
2066
2075
  if (borderFlash > 0) borderFlash--;
2067
2076
  const cols = terminal.cols, rows = terminal.rows;
2068
- if (cols < MIN_COLS || rows < MIN_ROWS) {
2077
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
2069
2078
  const msg1 = "Terminal too small!";
2070
- const needW = cols < MIN_COLS, needH = rows < MIN_ROWS;
2079
+ const needW = cols < MIN_COLS2, needH = rows < MIN_ROWS2;
2071
2080
  const hint2 = needW && needH ? "Make pane larger" : needW ? "Make pane wider \u2192" : "Make pane taller \u2193";
2072
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
2081
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
2073
2082
  const cX = Math.floor(cols / 2), cY = Math.floor(rows / 2);
2074
2083
  output += `\x1B[${cY - 1};${Math.max(1, cX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
2075
2084
  output += `\x1B[${cY + 1};${Math.max(1, cX - Math.floor(msg2.length / 2))}H\x1B[2m${msg2}\x1B[0m`;
@@ -2631,8 +2640,8 @@ var ROPE_RETRACT_SPEED = 0.15;
2631
2640
  var ROPE_FAST_RETRACT_SPEED = 0.3;
2632
2641
  function runCourierGame(terminal) {
2633
2642
  const themeColor = getCurrentThemeColor();
2634
- const MIN_COLS = 40;
2635
- const MIN_ROWS = 16;
2643
+ const MIN_COLS2 = 40;
2644
+ const MIN_ROWS2 = 16;
2636
2645
  let running = true;
2637
2646
  let gameStarted = false;
2638
2647
  let gameOver = false;
@@ -3261,8 +3270,8 @@ function runCourierGame(terminal) {
3261
3270
  output += "\x1B[2J\x1B[H";
3262
3271
  const cols = terminal.cols;
3263
3272
  const rows = terminal.rows;
3264
- if (cols < MIN_COLS || rows < MIN_ROWS) {
3265
- const msg = `Need ${MIN_COLS}\xD7${MIN_ROWS}, have ${cols}\xD7${rows}`;
3273
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
3274
+ const msg = `Need ${MIN_COLS2}\xD7${MIN_ROWS2}, have ${cols}\xD7${rows}`;
3266
3275
  const centerX = Math.floor(cols / 2);
3267
3276
  const centerY = Math.floor(rows / 2);
3268
3277
  output += `\x1B[${centerY};${Math.max(1, centerX - msg.length / 2)}H${themeColor}${msg}\x1B[0m`;
@@ -4021,8 +4030,8 @@ var LOG_MESSAGES = {
4021
4030
  };
4022
4031
  function runCrackGame(terminal) {
4023
4032
  const themeColor = getCurrentThemeColor();
4024
- const MIN_COLS = 40;
4025
- const MIN_ROWS = 16;
4033
+ const MIN_COLS2 = 40;
4034
+ const MIN_ROWS2 = 16;
4026
4035
  let cols = terminal.cols;
4027
4036
  let rows = terminal.rows;
4028
4037
  const updateDimensions = () => {
@@ -4234,9 +4243,9 @@ function runCrackGame(terminal) {
4234
4243
  function render() {
4235
4244
  let output = "";
4236
4245
  output += "\x1B[2J\x1B[H";
4237
- if (cols < MIN_COLS || rows < MIN_ROWS) {
4246
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
4238
4247
  const msg1 = "Terminal too small!";
4239
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
4248
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
4240
4249
  const centerX = Math.floor(cols / 2);
4241
4250
  const centerY = Math.floor(rows / 2);
4242
4251
  output += `\x1B[${centerY};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -4689,8 +4698,8 @@ function runCrackGame(terminal) {
4689
4698
  // src/games/frogger/index.ts
4690
4699
  function runFroggerGame(terminal) {
4691
4700
  const themeColor = getCurrentThemeColor();
4692
- const MIN_COLS = 40;
4693
- const MIN_ROWS = 16;
4701
+ const MIN_COLS2 = 40;
4702
+ const MIN_ROWS2 = 16;
4694
4703
  const LILY_PAD_COUNT = 5;
4695
4704
  const TIME_LIMIT = 45;
4696
4705
  const MOVE_COOLDOWN = 100;
@@ -5075,10 +5084,10 @@ function runFroggerGame(terminal) {
5075
5084
  output += "\x1B[2J\x1B[H";
5076
5085
  const cols = terminal.cols;
5077
5086
  const rows = terminal.rows;
5078
- if (cols < MIN_COLS || rows < MIN_ROWS) {
5087
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
5079
5088
  const msg1 = "Terminal too small!";
5080
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
5081
- const hint2 = cols < MIN_COLS && rows < MIN_ROWS ? "Make pane larger" : cols < MIN_COLS ? "Make pane wider \u2192" : "Make pane taller \u2193";
5089
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
5090
+ const hint2 = cols < MIN_COLS2 && rows < MIN_ROWS2 ? "Make pane larger" : cols < MIN_COLS2 ? "Make pane wider \u2192" : "Make pane taller \u2193";
5082
5091
  const cx = Math.floor(cols / 2);
5083
5092
  const cy = Math.floor(rows / 2);
5084
5093
  output += `\x1B[${cy - 1};${Math.max(1, cx - msg1.length / 2)}H${themeColor}${msg1}\x1B[0m`;
@@ -5516,8 +5525,8 @@ var HANGMAN_STAGES = [
5516
5525
  function runHangmanGame(terminal) {
5517
5526
  const themeColor = getCurrentThemeColor();
5518
5527
  const GAME_WIDTH = 40;
5519
- const MIN_COLS = 32;
5520
- const MIN_ROWS = 16;
5528
+ const MIN_COLS2 = 32;
5529
+ const MIN_ROWS2 = 16;
5521
5530
  let running = true;
5522
5531
  let gameStarted = false;
5523
5532
  let gameOver = false;
@@ -5681,10 +5690,10 @@ function runHangmanGame(terminal) {
5681
5690
  }
5682
5691
  const cols = terminal.cols;
5683
5692
  const rows = terminal.rows;
5684
- if (cols < MIN_COLS || rows < MIN_ROWS) {
5693
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
5685
5694
  const msg1 = "Terminal too small!";
5686
- const needWidth = cols < MIN_COLS;
5687
- const needHeight = rows < MIN_ROWS;
5695
+ const needWidth = cols < MIN_COLS2;
5696
+ const needHeight = rows < MIN_ROWS2;
5688
5697
  let hint2 = "";
5689
5698
  if (needWidth && needHeight) {
5690
5699
  hint2 = "Make pane larger";
@@ -5693,7 +5702,7 @@ function runHangmanGame(terminal) {
5693
5702
  } else {
5694
5703
  hint2 = "Make pane taller \u2193";
5695
5704
  }
5696
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
5705
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
5697
5706
  const centerX = Math.floor(cols / 2);
5698
5707
  const centerY = Math.floor(rows / 2);
5699
5708
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -6048,7 +6057,7 @@ function runMinesweeperGame(terminal) {
6048
6057
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
6049
6058
  scorePopups.push({ x, y, text, frames: 20, color });
6050
6059
  }
6051
- function triggerShake2(frames, intensity) {
6060
+ function triggerShake3(frames, intensity) {
6052
6061
  shakeFrames = frames;
6053
6062
  shakeIntensity = intensity;
6054
6063
  }
@@ -6135,7 +6144,7 @@ function runMinesweeperGame(terminal) {
6135
6144
  if (cell.isMine) {
6136
6145
  gameOver = true;
6137
6146
  won = false;
6138
- triggerShake2(25, 4);
6147
+ triggerShake3(25, 4);
6139
6148
  spawnExplosion(x * 2 + 1, y);
6140
6149
  addScorePopup2(x * 2, y - 1, "MALWARE!", "\x1B[1;91m");
6141
6150
  revealAllMines();
@@ -6184,7 +6193,7 @@ function runMinesweeperGame(terminal) {
6184
6193
  if (cellsRevealed >= totalNonMines) {
6185
6194
  gameOver = true;
6186
6195
  won = true;
6187
- triggerShake2(10, 2);
6196
+ triggerShake3(10, 2);
6188
6197
  for (let i = 0; i < 5; i++) {
6189
6198
  const x = Math.floor(Math.random() * difficulty.width);
6190
6199
  const y = Math.floor(Math.random() * difficulty.height);
@@ -6582,8 +6591,8 @@ function runMinesweeperGame(terminal) {
6582
6591
  // src/games/pong/index.ts
6583
6592
  function runPongGame(terminal) {
6584
6593
  const themeColor = getCurrentThemeColor();
6585
- const MIN_COLS = 40;
6586
- const MIN_ROWS = 16;
6594
+ const MIN_COLS2 = 40;
6595
+ const MIN_ROWS2 = 16;
6587
6596
  const getGameDimensions = () => {
6588
6597
  const cols = terminal.cols;
6589
6598
  const rows = terminal.rows;
@@ -6723,10 +6732,10 @@ function runPongGame(terminal) {
6723
6732
  }
6724
6733
  const cols = terminal.cols;
6725
6734
  const rows = terminal.rows;
6726
- if (cols < MIN_COLS || rows < MIN_ROWS) {
6735
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
6727
6736
  const msg1 = "Terminal too small!";
6728
- const needWidth = cols < MIN_COLS;
6729
- const needHeight = rows < MIN_ROWS;
6737
+ const needWidth = cols < MIN_COLS2;
6738
+ const needHeight = rows < MIN_ROWS2;
6730
6739
  let hint2 = "";
6731
6740
  if (needWidth && needHeight) {
6732
6741
  hint2 = "Make pane larger";
@@ -6735,7 +6744,7 @@ function runPongGame(terminal) {
6735
6744
  } else {
6736
6745
  hint2 = "Make pane taller \u2193";
6737
6746
  }
6738
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
6747
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
6739
6748
  const centerX2 = Math.floor(cols / 2);
6740
6749
  const centerY = Math.floor(rows / 2);
6741
6750
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -7171,8 +7180,8 @@ function runRunnerGame(terminal) {
7171
7180
  const GAME_WIDTH = ROAD_WIDTH_BOTTOM + SCENERY_WIDTH * 2;
7172
7181
  let gameTop = 5;
7173
7182
  let gameLeft = 4;
7174
- const MIN_COLS = GAME_WIDTH + 2;
7175
- const MIN_ROWS = TRACK_HEIGHT + 8;
7183
+ const MIN_COLS2 = GAME_WIDTH + 2;
7184
+ const MIN_ROWS2 = TRACK_HEIGHT + 8;
7176
7185
  let running = true;
7177
7186
  let gameStarted = false;
7178
7187
  let gameOver = false;
@@ -7403,12 +7412,12 @@ function runRunnerGame(terminal) {
7403
7412
  }
7404
7413
  const cols = terminal.cols;
7405
7414
  const rows = terminal.rows;
7406
- if (cols < MIN_COLS || rows < MIN_ROWS) {
7415
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
7407
7416
  const msg1 = "Terminal too small!";
7408
- const needWidth = cols < MIN_COLS;
7409
- const needHeight = rows < MIN_ROWS;
7417
+ const needWidth = cols < MIN_COLS2;
7418
+ const needHeight = rows < MIN_ROWS2;
7410
7419
  let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
7411
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
7420
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
7412
7421
  const centerX2 = Math.floor(cols / 2);
7413
7422
  const centerY = Math.floor(rows / 2);
7414
7423
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -7935,8 +7944,8 @@ function runRunnerGame(terminal) {
7935
7944
  // src/games/simon/index.ts
7936
7945
  function runSimonGame(terminal) {
7937
7946
  const themeColor = getCurrentThemeColor();
7938
- const MIN_COLS = 40;
7939
- const MIN_ROWS = 18;
7947
+ const MIN_COLS2 = 40;
7948
+ const MIN_ROWS2 = 18;
7940
7949
  const GAME_WIDTH = 40;
7941
7950
  const GAME_HEIGHT = 18;
7942
7951
  const QUADRANT_COLORS = [
@@ -8050,7 +8059,7 @@ function runSimonGame(terminal) {
8050
8059
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
8051
8060
  scorePopups.push({ x, y, text, frames: 20, color });
8052
8061
  }
8053
- function triggerShake2(frames, intensity) {
8062
+ function triggerShake3(frames, intensity) {
8054
8063
  shakeFrames = frames;
8055
8064
  shakeIntensity = intensity;
8056
8065
  }
@@ -8112,7 +8121,7 @@ function runSimonGame(terminal) {
8112
8121
  statusMessage = "PATTERN ACCEPTED";
8113
8122
  statusColor = "\x1B[1;92m";
8114
8123
  statusBlink = true;
8115
- triggerShake2(6, 1);
8124
+ triggerShake3(6, 1);
8116
8125
  }
8117
8126
  } else {
8118
8127
  phase = "failure";
@@ -8124,7 +8133,7 @@ function runSimonGame(terminal) {
8124
8133
  statusMessage = "ACCESS DENIED";
8125
8134
  statusColor = "\x1B[1;91m";
8126
8135
  statusBlink = true;
8127
- triggerShake2(15, 3);
8136
+ triggerShake3(15, 3);
8128
8137
  const centerX = GAME_WIDTH / 2;
8129
8138
  const centerY = GAME_HEIGHT / 2;
8130
8139
  spawnParticles3(centerX, centerY, 15, "\x1B[1;91m", ["\u2717", "\u2716", "\xD7", "\u2573"]);
@@ -8250,12 +8259,12 @@ function runSimonGame(terminal) {
8250
8259
  if (shakeFrames > 0) shakeFrames--;
8251
8260
  const cols = terminal.cols;
8252
8261
  const rows = terminal.rows;
8253
- if (cols < MIN_COLS || rows < MIN_ROWS) {
8262
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
8254
8263
  const msg1 = "Terminal too small!";
8255
- const needWidth = cols < MIN_COLS;
8256
- const needHeight = rows < MIN_ROWS;
8264
+ const needWidth = cols < MIN_COLS2;
8265
+ const needHeight = rows < MIN_ROWS2;
8257
8266
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
8258
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
8267
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
8259
8268
  const centerX = Math.floor(cols / 2);
8260
8269
  const centerY = Math.floor(rows / 2);
8261
8270
  outputParts.push(`\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`);
@@ -8517,8 +8526,8 @@ function runSimonGame(terminal) {
8517
8526
  // src/games/snake/index.ts
8518
8527
  function runSnakeGame(terminal) {
8519
8528
  const themeColor = getCurrentThemeColor();
8520
- const MIN_COLS = 36;
8521
- const MIN_ROWS = 16;
8529
+ const MIN_COLS2 = 36;
8530
+ const MIN_ROWS2 = 16;
8522
8531
  let cols = terminal.cols;
8523
8532
  let rows = terminal.rows;
8524
8533
  let gameTop = 6;
@@ -8621,7 +8630,7 @@ function runSnakeGame(terminal) {
8621
8630
  });
8622
8631
  }
8623
8632
  }
8624
- function updateParticles3() {
8633
+ function updateParticles4() {
8625
8634
  for (let i = particles.length - 1; i >= 0; i--) {
8626
8635
  const p = particles[i];
8627
8636
  p.x += p.vx;
@@ -8646,7 +8655,7 @@ function runSnakeGame(terminal) {
8646
8655
  scorePopup.y -= 0.3;
8647
8656
  if (scorePopup.frames <= 0) scorePopup = null;
8648
8657
  }
8649
- updateParticles3();
8658
+ updateParticles4();
8650
8659
  let renderGameLeft = gameLeft;
8651
8660
  let renderGameTop = gameTop;
8652
8661
  if (shakeFrames > 0) {
@@ -8655,10 +8664,10 @@ function runSnakeGame(terminal) {
8655
8664
  renderGameLeft = Math.max(1, gameLeft + shakeX);
8656
8665
  renderGameTop = Math.max(3, gameTop + shakeY);
8657
8666
  }
8658
- if (cols < MIN_COLS || rows < MIN_ROWS) {
8667
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
8659
8668
  const msg1 = "Terminal too small!";
8660
- const needWidth = cols < MIN_COLS;
8661
- const needHeight = rows < MIN_ROWS;
8669
+ const needWidth = cols < MIN_COLS2;
8670
+ const needHeight = rows < MIN_ROWS2;
8662
8671
  let hint2 = "";
8663
8672
  if (needWidth && needHeight) {
8664
8673
  hint2 = "Make pane larger";
@@ -8667,7 +8676,7 @@ function runSnakeGame(terminal) {
8667
8676
  } else {
8668
8677
  hint2 = "Make pane taller \u2193";
8669
8678
  }
8670
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
8679
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
8671
8680
  const centerX = Math.floor(cols / 2);
8672
8681
  const centerY = Math.floor(rows / 2);
8673
8682
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -8989,8 +8998,8 @@ function runSnakeGame(terminal) {
8989
8998
  // src/games/spaceinvaders/index.ts
8990
8999
  function runSpaceInvadersGame(terminal) {
8991
9000
  const themeColor = getCurrentThemeColor();
8992
- const MIN_COLS = 40;
8993
- const MIN_ROWS = 16;
9001
+ const MIN_COLS2 = 40;
9002
+ const MIN_ROWS2 = 16;
8994
9003
  const getGameDimensions = () => {
8995
9004
  const cols = terminal.cols;
8996
9005
  const rows = terminal.rows;
@@ -9168,10 +9177,10 @@ function runSpaceInvadersGame(terminal) {
9168
9177
  }
9169
9178
  const cols = terminal.cols;
9170
9179
  const rows = terminal.rows;
9171
- if (cols < MIN_COLS || rows < MIN_ROWS) {
9180
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
9172
9181
  const msg1 = "Terminal too small!";
9173
- const needWidth = cols < MIN_COLS;
9174
- const needHeight = rows < MIN_ROWS;
9182
+ const needWidth = cols < MIN_COLS2;
9183
+ const needHeight = rows < MIN_ROWS2;
9175
9184
  let hint2 = "";
9176
9185
  if (needWidth && needHeight) {
9177
9186
  hint2 = "Make pane larger";
@@ -9180,7 +9189,7 @@ function runSpaceInvadersGame(terminal) {
9180
9189
  } else {
9181
9190
  hint2 = "Make pane taller \u2193";
9182
9191
  }
9183
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
9192
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
9184
9193
  const centerX = Math.floor(cols / 2);
9185
9194
  const centerY = Math.floor(rows / 2);
9186
9195
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -9653,13 +9662,13 @@ function runTetrisGame(terminal) {
9653
9662
  const themeColor = getCurrentThemeColor();
9654
9663
  const BOARD_WIDTH = 12;
9655
9664
  const BOARD_HEIGHT = 20;
9656
- const SIDE_PANEL_WIDTH = 14;
9665
+ const SIDE_PANEL_WIDTH2 = 14;
9657
9666
  const BOARD_ONLY_WIDTH = BOARD_WIDTH * 2 + 2;
9658
- const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH + 4;
9659
- const TICK_MS = 25;
9667
+ const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH2 + 4;
9668
+ const TICK_MS2 = 25;
9660
9669
  const SPRINT_TARGET_LINES = 40;
9661
- const MIN_COLS = BOARD_ONLY_WIDTH + 2;
9662
- const MIN_ROWS = BOARD_HEIGHT + 3;
9670
+ const MIN_COLS2 = BOARD_ONLY_WIDTH + 2;
9671
+ const MIN_ROWS2 = BOARD_HEIGHT + 3;
9663
9672
  const MIN_ROWS_WITH_TITLE = BOARD_HEIGHT + 4;
9664
9673
  let running = true;
9665
9674
  let gameStarted = false;
@@ -9742,7 +9751,7 @@ function runTetrisGame(terminal) {
9742
9751
  const shape = TETROMINOES[currentPiece][0];
9743
9752
  pieceX = Math.floor((BOARD_WIDTH - shape[0].length) / 2);
9744
9753
  pieceY = 0;
9745
- if (!isValidPosition(pieceX, pieceY, currentRotation)) {
9754
+ if (!isValidPosition2(pieceX, pieceY, currentRotation)) {
9746
9755
  gameOver = true;
9747
9756
  if (score > highScore) highScore = score;
9748
9757
  }
@@ -9751,7 +9760,7 @@ function runTetrisGame(terminal) {
9751
9760
  const rotations = TETROMINOES[currentPiece];
9752
9761
  return rotations[rotation % rotations.length];
9753
9762
  }
9754
- function isValidPosition(x, y, rotation) {
9763
+ function isValidPosition2(x, y, rotation) {
9755
9764
  const shape = TETROMINOES[currentPiece][rotation % TETROMINOES[currentPiece].length];
9756
9765
  for (let row = 0; row < shape.length; row++) {
9757
9766
  for (let col = 0; col < shape[row].length; col++) {
@@ -9868,23 +9877,23 @@ function runTetrisGame(terminal) {
9868
9877
  }
9869
9878
  }
9870
9879
  function moveLeft() {
9871
- if (isValidPosition(pieceX - 1, pieceY, currentRotation)) {
9880
+ if (isValidPosition2(pieceX - 1, pieceY, currentRotation)) {
9872
9881
  pieceX--;
9873
9882
  }
9874
9883
  }
9875
9884
  function moveRight() {
9876
- if (isValidPosition(pieceX + 1, pieceY, currentRotation)) {
9885
+ if (isValidPosition2(pieceX + 1, pieceY, currentRotation)) {
9877
9886
  pieceX++;
9878
9887
  }
9879
9888
  }
9880
9889
  function moveDown() {
9881
- if (isValidPosition(pieceX, pieceY + 1, currentRotation)) {
9890
+ if (isValidPosition2(pieceX, pieceY + 1, currentRotation)) {
9882
9891
  pieceY++;
9883
9892
  return true;
9884
9893
  }
9885
9894
  return false;
9886
9895
  }
9887
- function hardDrop() {
9896
+ function hardDrop2() {
9888
9897
  if (hardDropping) return;
9889
9898
  if (hardDropConsumed) return;
9890
9899
  hardDropping = true;
@@ -9892,13 +9901,13 @@ function runTetrisGame(terminal) {
9892
9901
  }
9893
9902
  function rotate() {
9894
9903
  const newRotation = (currentRotation + 1) % TETROMINOES[currentPiece].length;
9895
- if (isValidPosition(pieceX, pieceY, newRotation)) {
9904
+ if (isValidPosition2(pieceX, pieceY, newRotation)) {
9896
9905
  currentRotation = newRotation;
9897
9906
  return;
9898
9907
  }
9899
9908
  const kicks = [-1, 1, -2, 2];
9900
9909
  for (const kick of kicks) {
9901
- if (isValidPosition(pieceX + kick, pieceY, newRotation)) {
9910
+ if (isValidPosition2(pieceX + kick, pieceY, newRotation)) {
9902
9911
  pieceX += kick;
9903
9912
  currentRotation = newRotation;
9904
9913
  return;
@@ -9910,10 +9919,10 @@ function runTetrisGame(terminal) {
9910
9919
  output += "\x1B[2J\x1B[H";
9911
9920
  const cols = terminal.cols;
9912
9921
  const rows = terminal.rows;
9913
- if (cols < MIN_COLS || rows < MIN_ROWS) {
9922
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
9914
9923
  const msg1 = "Terminal too small!";
9915
- const needWidth = cols < MIN_COLS;
9916
- const needHeight = rows < MIN_ROWS;
9924
+ const needWidth = cols < MIN_COLS2;
9925
+ const needHeight = rows < MIN_ROWS2;
9917
9926
  let hint = "";
9918
9927
  if (needWidth && needHeight) {
9919
9928
  hint = "Make pane larger";
@@ -9922,7 +9931,7 @@ function runTetrisGame(terminal) {
9922
9931
  } else {
9923
9932
  hint = "Make pane taller \u2193";
9924
9933
  }
9925
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
9934
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
9926
9935
  const centerX = Math.floor(cols / 2);
9927
9936
  const centerY = Math.floor(rows / 2);
9928
9937
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -10056,7 +10065,7 @@ function runTetrisGame(terminal) {
10056
10065
  }
10057
10066
  }
10058
10067
  let ghostY = pieceY;
10059
- while (isValidPosition(pieceX, ghostY + 1, currentRotation)) {
10068
+ while (isValidPosition2(pieceX, ghostY + 1, currentRotation)) {
10060
10069
  ghostY++;
10061
10070
  }
10062
10071
  if (ghostY !== pieceY) {
@@ -10156,7 +10165,7 @@ function runTetrisGame(terminal) {
10156
10165
  return;
10157
10166
  }
10158
10167
  dropCounter++;
10159
- if (dropCounter * TICK_MS >= dropInterval) {
10168
+ if (dropCounter * TICK_MS2 >= dropInterval) {
10160
10169
  dropCounter = 0;
10161
10170
  if (!moveDown()) {
10162
10171
  lockPiece();
@@ -10187,14 +10196,14 @@ function runTetrisGame(terminal) {
10187
10196
  return;
10188
10197
  }
10189
10198
  render();
10190
- }, TICK_MS);
10199
+ }, TICK_MS2);
10191
10200
  const gameInterval = setInterval(() => {
10192
10201
  if (!running) {
10193
10202
  clearInterval(gameInterval);
10194
10203
  return;
10195
10204
  }
10196
10205
  update();
10197
- }, TICK_MS);
10206
+ }, TICK_MS2);
10198
10207
  const handleKeyUp = (e) => {
10199
10208
  if (e.key === " ") {
10200
10209
  hardDropConsumed = false;
@@ -10331,7 +10340,7 @@ function runTetrisGame(terminal) {
10331
10340
  rotate();
10332
10341
  break;
10333
10342
  case " ":
10334
- hardDrop();
10343
+ hardDrop2();
10335
10344
  break;
10336
10345
  }
10337
10346
  });
@@ -10366,8 +10375,8 @@ var BLOCK_COLORS = [
10366
10375
  ];
10367
10376
  function runTowerGame(terminal) {
10368
10377
  const themeColor = getCurrentThemeColor();
10369
- const MIN_COLS = 30;
10370
- const MIN_ROWS = 16;
10378
+ const MIN_COLS2 = 30;
10379
+ const MIN_ROWS2 = 16;
10371
10380
  const INITIAL_BLOCK_WIDTH = 8;
10372
10381
  const MIN_BLOCK_WIDTH = 2;
10373
10382
  const PERFECT_THRESHOLD = 0.5;
@@ -10446,7 +10455,7 @@ function runTowerGame(terminal) {
10446
10455
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
10447
10456
  scorePopups.push({ x, y, text, frames: 24, color });
10448
10457
  }
10449
- function triggerShake2(frames, intensity) {
10458
+ function triggerShake3(frames, intensity) {
10450
10459
  shakeFrames = frames;
10451
10460
  shakeIntensity = intensity;
10452
10461
  }
@@ -10507,7 +10516,7 @@ function runTowerGame(terminal) {
10507
10516
  if (score > highScore) highScore = score;
10508
10517
  if (perfectCombo > maxCombo) maxCombo = perfectCombo;
10509
10518
  spawnParticles3(dropX + dropWidth / 2, landY, 15, "\x1B[91m");
10510
- triggerShake2(10, 3);
10519
+ triggerShake3(10, 3);
10511
10520
  return;
10512
10521
  }
10513
10522
  const isPerfect = Math.abs(dropLeft - topLeft) < PERFECT_THRESHOLD && Math.abs(dropRight - topRight) < PERFECT_THRESHOLD;
@@ -10525,7 +10534,7 @@ function runTowerGame(terminal) {
10525
10534
  popupText,
10526
10535
  perfectCombo > 3 ? "\x1B[1;91m" : "\x1B[1;93m"
10527
10536
  );
10528
- triggerShake2(4, 1);
10537
+ triggerShake3(4, 1);
10529
10538
  } else {
10530
10539
  if (perfectCombo > maxCombo) maxCombo = perfectCombo;
10531
10540
  perfectCombo = 0;
@@ -10539,7 +10548,7 @@ function runTowerGame(terminal) {
10539
10548
  spawnFallingPiece(topRight, landY, overhangWidth, dropColor);
10540
10549
  spawnParticles3(topRight + overhangWidth / 2, landY, 5, dropColor);
10541
10550
  }
10542
- triggerShake2(3, 1);
10551
+ triggerShake3(3, 1);
10543
10552
  }
10544
10553
  score += points;
10545
10554
  height++;
@@ -10607,12 +10616,12 @@ function runTowerGame(terminal) {
10607
10616
  if (shakeFrames > 0) shakeFrames--;
10608
10617
  const cols = terminal.cols;
10609
10618
  const rows = terminal.rows;
10610
- if (cols < MIN_COLS || rows < MIN_ROWS) {
10619
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
10611
10620
  const msg1 = "Terminal too small!";
10612
- const needWidth = cols < MIN_COLS;
10613
- const needHeight = rows < MIN_ROWS;
10621
+ const needWidth = cols < MIN_COLS2;
10622
+ const needHeight = rows < MIN_ROWS2;
10614
10623
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
10615
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
10624
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
10616
10625
  const centerX = Math.floor(cols / 2);
10617
10626
  const centerY = Math.floor(rows / 2);
10618
10627
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -10857,8 +10866,8 @@ function runTowerGame(terminal) {
10857
10866
  // src/games/tron/index.ts
10858
10867
  function runTronGame(terminal) {
10859
10868
  const themeColor = getCurrentThemeColor();
10860
- const MIN_COLS = 40;
10861
- const MIN_ROWS = 18;
10869
+ const MIN_COLS2 = 40;
10870
+ const MIN_ROWS2 = 18;
10862
10871
  const GAME_WIDTH = 46;
10863
10872
  const GAME_HEIGHT = 18;
10864
10873
  const ROUNDS_TO_WIN = 3;
@@ -10927,9 +10936,9 @@ function runTronGame(terminal) {
10927
10936
  }
10928
10937
  function spawnExplosion(x, y, color) {
10929
10938
  spawnParticles3(x, y, 20, color, ["\u2717", "\xD7", "\u2591", "\u2592", "\u2593", "\u2588"]);
10930
- triggerShake2(12, 3);
10939
+ triggerShake3(12, 3);
10931
10940
  }
10932
- function triggerShake2(frames, intensity) {
10941
+ function triggerShake3(frames, intensity) {
10933
10942
  shakeFrames = frames;
10934
10943
  shakeIntensity = intensity;
10935
10944
  }
@@ -11144,7 +11153,7 @@ function runTronGame(terminal) {
11144
11153
  arenaMinY += SHRINK_AMOUNT;
11145
11154
  arenaMaxY -= SHRINK_AMOUNT;
11146
11155
  }
11147
- triggerShake2(4, 1);
11156
+ triggerShake3(4, 1);
11148
11157
  }
11149
11158
  }
11150
11159
  }
@@ -11211,12 +11220,12 @@ function runTronGame(terminal) {
11211
11220
  if (shakeFrames > 0) shakeFrames--;
11212
11221
  const cols = terminal.cols;
11213
11222
  const rows = terminal.rows;
11214
- if (cols < MIN_COLS || rows < MIN_ROWS) {
11223
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
11215
11224
  const msg1 = "Terminal too small!";
11216
- const needWidth = cols < MIN_COLS;
11217
- const needHeight = rows < MIN_ROWS;
11225
+ const needWidth = cols < MIN_COLS2;
11226
+ const needHeight = rows < MIN_ROWS2;
11218
11227
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
11219
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
11228
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
11220
11229
  const centerX = Math.floor(cols / 2);
11221
11230
  const centerY = Math.floor(rows / 2);
11222
11231
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -11305,7 +11314,7 @@ function runTronGame(terminal) {
11305
11314
  const nextMsg = "Press SPACE for next round";
11306
11315
  const nextX = gameLeft + Math.floor((GAME_WIDTH - nextMsg.length) / 2) + 1;
11307
11316
  output += `\x1B[${msgY + 2};${nextX}H\x1B[2m${themeColor}${nextMsg}\x1B[0m`;
11308
- output = renderParticles(output, renderLeft, renderTop);
11317
+ output = renderParticles2(output, renderLeft, renderTop);
11309
11318
  } else if (gameOver) {
11310
11319
  output = renderTrailsAndCycles(output, renderLeft, renderTop);
11311
11320
  const overColor = playerWins >= ROUNDS_TO_WIN ? "\x1B[1;92m" : "\x1B[1;91m";
@@ -11319,7 +11328,7 @@ function runTronGame(terminal) {
11319
11328
  const restart = "\u255A [R] RESTART [Q] QUIT \u255D";
11320
11329
  const restartX = gameLeft + Math.floor((GAME_WIDTH - restart.length) / 2) + 1;
11321
11330
  output += `\x1B[${overY + 2};${restartX}H\x1B[2m${themeColor}${restart}\x1B[0m`;
11322
- output = renderParticles(output, renderLeft, renderTop);
11331
+ output = renderParticles2(output, renderLeft, renderTop);
11323
11332
  } else {
11324
11333
  for (const pos of player.trail) {
11325
11334
  if (pos.x > arenaMinX && pos.x < arenaMaxX && pos.y > arenaMinY && pos.y < arenaMaxY) {
@@ -11337,7 +11346,7 @@ function runTronGame(terminal) {
11337
11346
  if (ai.alive) {
11338
11347
  output += `\x1B[${renderTop + 1 + ai.y};${renderLeft + 1 + ai.x}H\x1B[1m${ai.color}${ai.char}\x1B[0m`;
11339
11348
  }
11340
- output = renderParticles(output, renderLeft, renderTop);
11349
+ output = renderParticles2(output, renderLeft, renderTop);
11341
11350
  }
11342
11351
  const hint = gameStarted && !gameOver && !paused ? `SPEED: ${Math.round((1 - gameSpeed / BASE_SPEED) * 100) + 100}% [ ESC ] MENU` : "";
11343
11352
  const hintX = Math.floor((cols - hint.length) / 2);
@@ -11359,7 +11368,7 @@ function runTronGame(terminal) {
11359
11368
  }
11360
11369
  return output;
11361
11370
  }
11362
- function renderParticles(output, renderLeft, renderTop) {
11371
+ function renderParticles2(output, renderLeft, renderTop) {
11363
11372
  for (const p of particles) {
11364
11373
  const screenX = Math.round(renderLeft + 1 + p.x);
11365
11374
  const screenY = Math.round(renderTop + 1 + p.y);
@@ -11694,8 +11703,8 @@ var SENTENCES = [
11694
11703
  function runTypingTest(terminal) {
11695
11704
  const themeColor = getCurrentThemeColor();
11696
11705
  const GAME_WIDTH = 60;
11697
- const MIN_COLS = 40;
11698
- const MIN_ROWS = 18;
11706
+ const MIN_COLS2 = 40;
11707
+ const MIN_ROWS2 = 18;
11699
11708
  let running = true;
11700
11709
  let gameStarted = false;
11701
11710
  let gameOver = false;
@@ -11894,10 +11903,10 @@ function runTypingTest(terminal) {
11894
11903
  }
11895
11904
  const cols = terminal.cols;
11896
11905
  const rows = terminal.rows;
11897
- if (cols < MIN_COLS || rows < MIN_ROWS) {
11906
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
11898
11907
  const msg1 = "Terminal too small!";
11899
- const needWidth = cols < MIN_COLS;
11900
- const needHeight = rows < MIN_ROWS;
11908
+ const needWidth = cols < MIN_COLS2;
11909
+ const needHeight = rows < MIN_ROWS2;
11901
11910
  let hint2 = "";
11902
11911
  if (needWidth && needHeight) {
11903
11912
  hint2 = "Make pane larger";
@@ -11906,7 +11915,7 @@ function runTypingTest(terminal) {
11906
11915
  } else {
11907
11916
  hint2 = "Make pane taller \u2193";
11908
11917
  }
11909
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
11918
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
11910
11919
  const centerX = Math.floor(cols / 2);
11911
11920
  const centerY = Math.floor(rows / 2);
11912
11921
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -12551,8 +12560,8 @@ var WORDS2 = [
12551
12560
  ];
12552
12561
  function runWordleGame(terminal) {
12553
12562
  const themeColor = getCurrentThemeColor();
12554
- const MIN_COLS = 36;
12555
- const MIN_ROWS = 20;
12563
+ const MIN_COLS2 = 36;
12564
+ const MIN_ROWS2 = 20;
12556
12565
  const MAX_GUESSES = 6;
12557
12566
  const WORD_LENGTH = 5;
12558
12567
  const KEYBOARD_ROWS = [
@@ -12617,7 +12626,7 @@ function runWordleGame(terminal) {
12617
12626
  });
12618
12627
  }
12619
12628
  }
12620
- function spawnFirework3(x, y) {
12629
+ function spawnFirework4(x, y) {
12621
12630
  const colors = ["\x1B[1;92m", "\x1B[1;93m", "\x1B[1;96m", "\x1B[1;95m"];
12622
12631
  const color = colors[Math.floor(Math.random() * colors.length)];
12623
12632
  const chars = ["*", "+", "o", ".", "`", "'"];
@@ -12638,7 +12647,7 @@ function runWordleGame(terminal) {
12638
12647
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
12639
12648
  scorePopups.push({ x, y, text, frames: 25, color });
12640
12649
  }
12641
- function triggerShake2(frames, intensity) {
12650
+ function triggerShake3(frames, intensity) {
12642
12651
  shakeFrames = frames;
12643
12652
  shakeIntensity = intensity;
12644
12653
  }
@@ -12696,7 +12705,7 @@ function runWordleGame(terminal) {
12696
12705
  }
12697
12706
  function submitGuess() {
12698
12707
  if (currentGuess.length !== WORD_LENGTH) {
12699
- triggerShake2(4, 2);
12708
+ triggerShake3(4, 2);
12700
12709
  addScorePopup2(Math.floor(cols / 2), 8, "NOT ENOUGH LETTERS", "\x1B[1;91m");
12701
12710
  return;
12702
12711
  }
@@ -12722,7 +12731,7 @@ function runWordleGame(terminal) {
12722
12731
  }
12723
12732
  stats.guessDistribution[guesses.length - 1]++;
12724
12733
  borderFlash = 20;
12725
- triggerShake2(10, 3);
12734
+ triggerShake3(10, 3);
12726
12735
  const messages = ["CIPHER BREACHED!", "CODE CRACKED!", "DECRYPTED!", "BRILLIANT!"];
12727
12736
  addScorePopup2(centerX, guessY - 2, messages[Math.floor(Math.random() * messages.length)], "\x1B[1;92m");
12728
12737
  setTimeout(() => {
@@ -12730,7 +12739,7 @@ function runWordleGame(terminal) {
12730
12739
  for (let i = 0; i < 5; i++) {
12731
12740
  setTimeout(() => {
12732
12741
  if (running && won) {
12733
- spawnFirework3(
12742
+ spawnFirework4(
12734
12743
  10 + Math.random() * (cols - 20),
12735
12744
  5 + Math.random() * 10
12736
12745
  );
@@ -12743,7 +12752,7 @@ function runWordleGame(terminal) {
12743
12752
  gameOver = true;
12744
12753
  stats.gamesPlayed++;
12745
12754
  stats.currentStreak = 0;
12746
- triggerShake2(8, 4);
12755
+ triggerShake3(8, 4);
12747
12756
  borderFlash = 15;
12748
12757
  addScorePopup2(centerX, guessY - 2, "DECRYPTION FAILED", "\x1B[1;91m");
12749
12758
  spawnParticles3(centerX, guessY, 10, "\x1B[1;91m", ["X", "x", ".", "*"]);
@@ -12814,12 +12823,12 @@ function runWordleGame(terminal) {
12814
12823
  rows = terminal.rows;
12815
12824
  if (shakeFrames > 0) shakeFrames--;
12816
12825
  if (borderFlash > 0) borderFlash--;
12817
- if (cols < MIN_COLS || rows < MIN_ROWS) {
12826
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
12818
12827
  const msg1 = "Terminal too small!";
12819
- const needWidth = cols < MIN_COLS;
12820
- const needHeight = rows < MIN_ROWS;
12828
+ const needWidth = cols < MIN_COLS2;
12829
+ const needHeight = rows < MIN_ROWS2;
12821
12830
  let hint = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider" : "Make pane taller";
12822
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
12831
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
12823
12832
  const centerX2 = Math.floor(cols / 2);
12824
12833
  const centerY = Math.floor(rows / 2);
12825
12834
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -13138,6 +13147,2492 @@ function runWordleGame(terminal) {
13138
13147
  return controller;
13139
13148
  }
13140
13149
 
13150
+ // src/games/hyper-fighter/engine.ts
13151
+ var BOARD_ROWS = 12;
13152
+ var BOARD_COLS = 6;
13153
+ var COLORS = ["red", "green", "blue", "yellow"];
13154
+ var CRASH_GEM_CHANCE = 0.25;
13155
+ var DROP_ALLEY_COL = 3;
13156
+ var DIAMOND_INTERVAL = 25;
13157
+ var nextPowerGemId = 1;
13158
+ function createBoard() {
13159
+ const board = [];
13160
+ for (let r = 0; r < BOARD_ROWS; r++) {
13161
+ board.push(new Array(BOARD_COLS).fill(null));
13162
+ }
13163
+ return board;
13164
+ }
13165
+ function randomColor() {
13166
+ return COLORS[Math.floor(Math.random() * COLORS.length)];
13167
+ }
13168
+ function randomGem() {
13169
+ const isCrash = Math.random() < CRASH_GEM_CHANCE;
13170
+ return {
13171
+ color: randomColor(),
13172
+ type: isCrash ? "crash" : "normal"
13173
+ };
13174
+ }
13175
+ function generatePair() {
13176
+ return {
13177
+ primary: randomGem(),
13178
+ secondary: randomGem(),
13179
+ col: DROP_ALLEY_COL,
13180
+ row: 0,
13181
+ orientation: 0
13182
+ };
13183
+ }
13184
+ function getSecondaryPos(pair) {
13185
+ switch (pair.orientation) {
13186
+ case 0:
13187
+ return { row: pair.row - 1, col: pair.col };
13188
+ // up
13189
+ case 1:
13190
+ return { row: pair.row, col: pair.col + 1 };
13191
+ // right
13192
+ case 2:
13193
+ return { row: pair.row + 1, col: pair.col };
13194
+ // down
13195
+ case 3:
13196
+ return { row: pair.row, col: pair.col - 1 };
13197
+ // left
13198
+ default:
13199
+ return { row: pair.row - 1, col: pair.col };
13200
+ }
13201
+ }
13202
+ function isValidPosition(pair, board) {
13203
+ const sec = getSecondaryPos(pair);
13204
+ if (pair.col < 0 || pair.col >= BOARD_COLS) return false;
13205
+ if (pair.row >= BOARD_ROWS) return false;
13206
+ if (sec.col < 0 || sec.col >= BOARD_COLS) return false;
13207
+ if (sec.row >= BOARD_ROWS) return false;
13208
+ if (pair.row >= 0 && board[pair.row][pair.col] !== null) return false;
13209
+ if (sec.row >= 0 && board[sec.row][sec.col] !== null) return false;
13210
+ return true;
13211
+ }
13212
+ function movePair(pair, board, dx) {
13213
+ const test = { ...pair, col: pair.col + dx };
13214
+ if (isValidPosition(test, board)) {
13215
+ pair.col += dx;
13216
+ return true;
13217
+ }
13218
+ return false;
13219
+ }
13220
+ function rotatePair(pair, board, clockwise) {
13221
+ const newOrientation = clockwise ? (pair.orientation + 1) % 4 : (pair.orientation + 3) % 4;
13222
+ const test = { ...pair, orientation: newOrientation };
13223
+ if (isValidPosition(test, board)) {
13224
+ pair.orientation = newOrientation;
13225
+ return true;
13226
+ }
13227
+ for (const kick of [1, -1, 2, -2]) {
13228
+ const kicked = { ...test, col: test.col + kick };
13229
+ if (isValidPosition(kicked, board)) {
13230
+ pair.orientation = newOrientation;
13231
+ pair.col += kick;
13232
+ return true;
13233
+ }
13234
+ }
13235
+ const upKick = { ...test, row: test.row - 1 };
13236
+ if (isValidPosition(upKick, board)) {
13237
+ pair.orientation = newOrientation;
13238
+ pair.row -= 1;
13239
+ return true;
13240
+ }
13241
+ return false;
13242
+ }
13243
+ function dropPair(pair, board) {
13244
+ const test = { ...pair, row: pair.row + 1 };
13245
+ if (isValidPosition(test, board)) {
13246
+ pair.row += 1;
13247
+ return true;
13248
+ }
13249
+ return false;
13250
+ }
13251
+ function hardDrop(pair, board) {
13252
+ let dropped = 0;
13253
+ while (dropPair(pair, board)) {
13254
+ dropped++;
13255
+ }
13256
+ return dropped;
13257
+ }
13258
+ function lockPair(pair, board) {
13259
+ if (pair.row >= 0 && pair.row < BOARD_ROWS) {
13260
+ board[pair.row][pair.col] = { ...pair.primary };
13261
+ }
13262
+ const sec = getSecondaryPos(pair);
13263
+ if (sec.row >= 0 && sec.row < BOARD_ROWS) {
13264
+ board[sec.row][sec.col] = { ...pair.secondary };
13265
+ }
13266
+ }
13267
+ function applyGravityFull(board, powerGems) {
13268
+ void powerGems;
13269
+ stripPowerGemIds(board);
13270
+ let moved = false;
13271
+ for (let col = 0; col < BOARD_COLS; col++) {
13272
+ let writeRow = BOARD_ROWS - 1;
13273
+ for (let row = BOARD_ROWS - 1; row >= 0; row--) {
13274
+ if (board[row][col] !== null) {
13275
+ if (row !== writeRow) {
13276
+ board[writeRow][col] = board[row][col];
13277
+ board[row][col] = null;
13278
+ moved = true;
13279
+ }
13280
+ writeRow--;
13281
+ }
13282
+ }
13283
+ }
13284
+ return moved;
13285
+ }
13286
+ function stripPowerGemIds(board) {
13287
+ for (let r = 0; r < BOARD_ROWS; r++) {
13288
+ for (let c = 0; c < BOARD_COLS; c++) {
13289
+ const gem = board[r][c];
13290
+ if (gem && gem.powerGemId !== void 0) {
13291
+ delete gem.powerGemId;
13292
+ }
13293
+ }
13294
+ }
13295
+ }
13296
+ function detectPowerGems(board) {
13297
+ const found = [];
13298
+ const used = /* @__PURE__ */ new Set();
13299
+ for (let r = 0; r < BOARD_ROWS; r++) {
13300
+ for (let c = 0; c < BOARD_COLS; c++) {
13301
+ const gem = board[r][c];
13302
+ if (!gem || gem.type !== "normal") continue;
13303
+ for (let h = 2; h <= BOARD_ROWS - r; h++) {
13304
+ for (let w = 2; w <= BOARD_COLS - c; w++) {
13305
+ if (isUniformRect(board, r, c, w, h, gem.color)) {
13306
+ const key = `${r},${c},${w},${h}`;
13307
+ if (!used.has(key)) {
13308
+ found.push({
13309
+ id: nextPowerGemId++,
13310
+ color: gem.color,
13311
+ x: c,
13312
+ y: r,
13313
+ width: w,
13314
+ height: h
13315
+ });
13316
+ }
13317
+ }
13318
+ }
13319
+ }
13320
+ }
13321
+ }
13322
+ found.sort((a, b) => b.width * b.height - a.width * a.height);
13323
+ const result = [];
13324
+ const claimed = /* @__PURE__ */ new Set();
13325
+ for (const pg of found) {
13326
+ let overlap = false;
13327
+ for (let r = pg.y; r < pg.y + pg.height; r++) {
13328
+ for (let c = pg.x; c < pg.x + pg.width; c++) {
13329
+ if (claimed.has(`${r},${c}`)) {
13330
+ overlap = true;
13331
+ break;
13332
+ }
13333
+ }
13334
+ if (overlap) break;
13335
+ }
13336
+ if (!overlap) {
13337
+ result.push(pg);
13338
+ for (let r = pg.y; r < pg.y + pg.height; r++) {
13339
+ for (let c = pg.x; c < pg.x + pg.width; c++) {
13340
+ claimed.add(`${r},${c}`);
13341
+ const gem = board[r][c];
13342
+ if (gem) gem.powerGemId = pg.id;
13343
+ }
13344
+ }
13345
+ }
13346
+ }
13347
+ return result;
13348
+ }
13349
+ function isUniformRect(board, startRow, startCol, width, height, color) {
13350
+ for (let r = startRow; r < startRow + height; r++) {
13351
+ for (let c = startCol; c < startCol + width; c++) {
13352
+ const gem = board[r][c];
13353
+ if (!gem || gem.type !== "normal" || gem.color !== color) return false;
13354
+ }
13355
+ }
13356
+ return true;
13357
+ }
13358
+ function findCrashTargets(board, _powerGems) {
13359
+ void _powerGems;
13360
+ const targets = findCrashTargetsCore(board);
13361
+ return targets;
13362
+ }
13363
+ function findCrashTargetsWithPowerInfo(board, powerGems) {
13364
+ const targets = findCrashTargetsCore(board);
13365
+ const destroyedPgIds = /* @__PURE__ */ new Set();
13366
+ for (const t of targets) {
13367
+ const gem = board[t.row][t.col];
13368
+ if (gem && gem.powerGemId !== void 0) {
13369
+ destroyedPgIds.add(gem.powerGemId);
13370
+ }
13371
+ }
13372
+ const destroyedPowerGemSizes = [];
13373
+ for (const pg of powerGems) {
13374
+ if (destroyedPgIds.has(pg.id)) {
13375
+ destroyedPowerGemSizes.push(pg.width * pg.height);
13376
+ }
13377
+ }
13378
+ return { targets, destroyedPowerGemSizes };
13379
+ }
13380
+ function findCrashTargetsCore(board) {
13381
+ const targetsByCell = /* @__PURE__ */ new Map();
13382
+ const visited = /* @__PURE__ */ new Set();
13383
+ for (let r = 0; r < BOARD_ROWS; r++) {
13384
+ for (let c = 0; c < BOARD_COLS; c++) {
13385
+ const gem = board[r][c];
13386
+ if (!gem || gem.type !== "crash") continue;
13387
+ const neighbors = getNeighbors(r, c);
13388
+ let hasTarget = false;
13389
+ for (const [nr, nc] of neighbors) {
13390
+ const ng = board[nr][nc];
13391
+ if (ng && ng.color === gem.color && ng.type === "normal") {
13392
+ hasTarget = true;
13393
+ break;
13394
+ }
13395
+ }
13396
+ if (hasTarget) {
13397
+ const connected = floodFillCrashGroup(board, r, c, gem.color, visited);
13398
+ for (const cell of connected) {
13399
+ targetsByCell.set(`${cell.row},${cell.col}`, { ...cell, color: gem.color });
13400
+ }
13401
+ }
13402
+ }
13403
+ }
13404
+ const targetSet = new Set(Array.from(targetsByCell.keys()));
13405
+ for (const key of Array.from(targetSet)) {
13406
+ const [rStr, cStr] = key.split(",");
13407
+ const r = parseInt(rStr, 10);
13408
+ const c = parseInt(cStr, 10);
13409
+ for (const [nr, nc] of getNeighbors(r, c)) {
13410
+ const nKey = `${nr},${nc}`;
13411
+ if (targetSet.has(nKey)) continue;
13412
+ const ng = board[nr][nc];
13413
+ if (ng && ng.type === "counter") {
13414
+ targetsByCell.set(nKey, { row: nr, col: nc, color: ng.color });
13415
+ targetSet.add(nKey);
13416
+ }
13417
+ }
13418
+ }
13419
+ return Array.from(targetsByCell.values());
13420
+ }
13421
+ function getNeighbors(row, col) {
13422
+ const n = [];
13423
+ if (row > 0) n.push([row - 1, col]);
13424
+ if (row < BOARD_ROWS - 1) n.push([row + 1, col]);
13425
+ if (col > 0) n.push([row, col - 1]);
13426
+ if (col < BOARD_COLS - 1) n.push([row, col + 1]);
13427
+ return n;
13428
+ }
13429
+ function floodFillCrashGroup(board, startRow, startCol, color, globalVisited) {
13430
+ const result = [];
13431
+ const stack = [[startRow, startCol]];
13432
+ const localVisited = /* @__PURE__ */ new Set();
13433
+ while (stack.length > 0) {
13434
+ const [r, c] = stack.pop();
13435
+ const key = `${r},${c}`;
13436
+ if (localVisited.has(key) || globalVisited.has(key)) continue;
13437
+ const gem = board[r][c];
13438
+ if (!gem) continue;
13439
+ if (gem.color !== color) continue;
13440
+ if (gem.type !== "normal" && gem.type !== "crash") continue;
13441
+ localVisited.add(key);
13442
+ globalVisited.add(key);
13443
+ result.push({ row: r, col: c });
13444
+ for (const [nr, nc] of getNeighbors(r, c)) {
13445
+ stack.push([nr, nc]);
13446
+ }
13447
+ }
13448
+ return result;
13449
+ }
13450
+ function clearGems(board, targets) {
13451
+ for (const t of targets) {
13452
+ board[t.row][t.col] = null;
13453
+ }
13454
+ }
13455
+ function resolveDiamond(board) {
13456
+ const cleared = [];
13457
+ for (let r = 0; r < BOARD_ROWS; r++) {
13458
+ for (let c = 0; c < BOARD_COLS; c++) {
13459
+ const gem = board[r][c];
13460
+ if (!gem || gem.type !== "diamond") continue;
13461
+ let targetColor = null;
13462
+ if (r + 1 < BOARD_ROWS && board[r + 1][c]) {
13463
+ targetColor = board[r + 1][c].color;
13464
+ }
13465
+ if (!targetColor) {
13466
+ for (const [nr, nc] of getNeighbors(r, c)) {
13467
+ if (board[nr][nc] && board[nr][nc].type !== "diamond") {
13468
+ targetColor = board[nr][nc].color;
13469
+ break;
13470
+ }
13471
+ }
13472
+ }
13473
+ board[r][c] = null;
13474
+ cleared.push({ row: r, col: c, color: targetColor || "red" });
13475
+ if (!targetColor) continue;
13476
+ for (let dr = 0; dr < BOARD_ROWS; dr++) {
13477
+ for (let dc = 0; dc < BOARD_COLS; dc++) {
13478
+ const g = board[dr][dc];
13479
+ if (g && g.color === targetColor) {
13480
+ cleared.push({ row: dr, col: dc, color: targetColor });
13481
+ board[dr][dc] = null;
13482
+ }
13483
+ }
13484
+ }
13485
+ }
13486
+ }
13487
+ return cleared;
13488
+ }
13489
+ function shouldSpawnDiamond(totalDrops) {
13490
+ return totalDrops > 0 && totalDrops % DIAMOND_INTERVAL === 0;
13491
+ }
13492
+ function generateDiamondPair() {
13493
+ return {
13494
+ primary: { color: "red", type: "diamond" },
13495
+ secondary: randomGem(),
13496
+ col: DROP_ALLEY_COL,
13497
+ row: 0,
13498
+ orientation: 0
13499
+ };
13500
+ }
13501
+ function calculateStepAttack(info) {
13502
+ let pgBonus = 0;
13503
+ for (const area of info.powerGemSizes) {
13504
+ pgBonus += Math.floor(area / 8);
13505
+ }
13506
+ return Math.floor((info.gemsCleared + pgBonus) * info.chainStep);
13507
+ }
13508
+ function applyAttackModifiers(total, damageModifier = 1, isDiamondClear = false) {
13509
+ let attack = Math.floor(total * damageModifier);
13510
+ if (isDiamondClear) attack = Math.floor(attack * 0.5);
13511
+ return attack;
13512
+ }
13513
+ function resolveCounterAttack(attack, pendingGarbage, ratio = 2) {
13514
+ if (attack <= 0 || pendingGarbage <= 0) {
13515
+ return {
13516
+ remainingAttack: attack,
13517
+ remainingPending: pendingGarbage,
13518
+ canceledGems: 0,
13519
+ pendingStartsAtThree: false
13520
+ };
13521
+ }
13522
+ const cancelable = Math.floor(attack / ratio);
13523
+ const canceledGems = Math.min(cancelable, pendingGarbage);
13524
+ const remainingPending = pendingGarbage - canceledGems;
13525
+ const remainingAttack = attack - canceledGems * ratio;
13526
+ return {
13527
+ remainingAttack,
13528
+ remainingPending,
13529
+ canceledGems,
13530
+ pendingStartsAtThree: canceledGems > 0 && remainingPending > 0
13531
+ };
13532
+ }
13533
+ function decrementCounters(player) {
13534
+ for (let r = 0; r < BOARD_ROWS; r++) {
13535
+ for (let c = 0; c < BOARD_COLS; c++) {
13536
+ const gem = player.board[r][c];
13537
+ if (gem && gem.type === "counter" && gem.counterTimer !== void 0) {
13538
+ gem.counterTimer--;
13539
+ if (gem.counterTimer <= 0) {
13540
+ gem.type = "normal";
13541
+ delete gem.counterTimer;
13542
+ }
13543
+ }
13544
+ }
13545
+ }
13546
+ }
13547
+ function checkGameOver(board) {
13548
+ return board[0][DROP_ALLEY_COL] !== null;
13549
+ }
13550
+ function getGhostPosition(pair, board) {
13551
+ const ghost = { ...pair };
13552
+ while (true) {
13553
+ const test = { ...ghost, row: ghost.row + 1 };
13554
+ if (!isValidPosition(test, board)) break;
13555
+ ghost.row++;
13556
+ }
13557
+ const sec = getSecondaryPos(ghost);
13558
+ return {
13559
+ primaryRow: ghost.row,
13560
+ primaryCol: ghost.col,
13561
+ secondaryRow: sec.row,
13562
+ secondaryCol: sec.col
13563
+ };
13564
+ }
13565
+ function createPlayerState() {
13566
+ return {
13567
+ board: createBoard(),
13568
+ currentPair: null,
13569
+ nextPair: generatePair(),
13570
+ powerGems: [],
13571
+ score: 0,
13572
+ pendingGarbage: 0,
13573
+ pendingCounteredGarbage: 0,
13574
+ alive: true,
13575
+ totalDrops: 0
13576
+ };
13577
+ }
13578
+ function spawnPair(player) {
13579
+ player.currentPair = player.nextPair;
13580
+ player.currentPair.col = DROP_ALLEY_COL;
13581
+ player.currentPair.row = 0;
13582
+ player.currentPair.orientation = 0;
13583
+ player.totalDrops++;
13584
+ if (shouldSpawnDiamond(player.totalDrops + 1)) {
13585
+ player.nextPair = generateDiamondPair();
13586
+ } else {
13587
+ player.nextPair = generatePair();
13588
+ }
13589
+ if (!isValidPosition(player.currentPair, player.board)) {
13590
+ player.alive = false;
13591
+ return false;
13592
+ }
13593
+ return true;
13594
+ }
13595
+
13596
+ // src/games/hyper-fighter/ai.ts
13597
+ var DIFFICULTIES2 = {
13598
+ easy: { name: "EASY", thinkFrames: 14, mistakeRate: 0.3, dropSpeedBoost: 1, simulateDepth: 0 },
13599
+ normal: { name: "NORMAL", thinkFrames: 8, mistakeRate: 0.1, dropSpeedBoost: 1.5, simulateDepth: 0 },
13600
+ hard: { name: "HARD", thinkFrames: 4, mistakeRate: 0.02, dropSpeedBoost: 2, simulateDepth: 1 }
13601
+ };
13602
+ function createAIState(difficulty) {
13603
+ return {
13604
+ targetCol: 2,
13605
+ targetOrientation: 0,
13606
+ thinkTimer: 0,
13607
+ moveTimer: 0,
13608
+ decided: false,
13609
+ difficulty
13610
+ };
13611
+ }
13612
+ function getColumnHeight(board, col) {
13613
+ for (let r = 0; r < BOARD_ROWS; r++) {
13614
+ if (board[r][col] !== null) return BOARD_ROWS - r;
13615
+ }
13616
+ return 0;
13617
+ }
13618
+ function evaluateBoard(board) {
13619
+ let score = 0;
13620
+ for (let c = 0; c < BOARD_COLS; c++) {
13621
+ const h = getColumnHeight(board, c);
13622
+ score -= h * h * 2;
13623
+ }
13624
+ const heights = [];
13625
+ for (let c = 0; c < BOARD_COLS; c++) {
13626
+ heights.push(getColumnHeight(board, c));
13627
+ }
13628
+ for (let c = 0; c < BOARD_COLS - 1; c++) {
13629
+ const diff = Math.abs(heights[c] - heights[c + 1]);
13630
+ score -= diff * 3;
13631
+ }
13632
+ for (let r = 0; r < BOARD_ROWS; r++) {
13633
+ for (let c = 0; c < BOARD_COLS; c++) {
13634
+ const gem = board[r][c];
13635
+ if (!gem || gem.type === "counter") continue;
13636
+ if (c + 1 < BOARD_COLS) {
13637
+ const right = board[r][c + 1];
13638
+ if (right && right.color === gem.color && right.type !== "counter") {
13639
+ score += 4;
13640
+ }
13641
+ }
13642
+ if (r + 1 < BOARD_ROWS) {
13643
+ const below = board[r + 1][c];
13644
+ if (below && below.color === gem.color && below.type !== "counter") {
13645
+ score += 4;
13646
+ }
13647
+ }
13648
+ }
13649
+ }
13650
+ for (let r = 0; r < BOARD_ROWS; r++) {
13651
+ for (let c = 0; c < BOARD_COLS; c++) {
13652
+ const gem = board[r][c];
13653
+ if (!gem || gem.type !== "crash") continue;
13654
+ const neighbors = [
13655
+ [r - 1, c],
13656
+ [r + 1, c],
13657
+ [r, c - 1],
13658
+ [r, c + 1]
13659
+ ];
13660
+ for (const [nr, nc] of neighbors) {
13661
+ if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) {
13662
+ const ng = board[nr][nc];
13663
+ if (ng && ng.color === gem.color && ng.type === "normal") {
13664
+ score += 15;
13665
+ }
13666
+ }
13667
+ }
13668
+ }
13669
+ }
13670
+ const powerGems = detectPowerGems(board);
13671
+ for (const pg of powerGems) {
13672
+ score += pg.width * pg.height * 8;
13673
+ }
13674
+ const targets = findCrashTargets(board, powerGems);
13675
+ score += targets.length * 10;
13676
+ const maxHeight = Math.max(...heights);
13677
+ if (maxHeight >= BOARD_ROWS - 2) score -= 200;
13678
+ if (maxHeight >= BOARD_ROWS - 1) score -= 500;
13679
+ const deathHeight = heights[DROP_ALLEY_COL];
13680
+ if (deathHeight >= BOARD_ROWS - 3) score -= 100;
13681
+ return score;
13682
+ }
13683
+ function cloneBoard(board) {
13684
+ return board.map((row) => row.map((gem) => gem ? { ...gem } : null));
13685
+ }
13686
+ function evaluatePlacement(pair, board, simulateDepth = 0) {
13687
+ const testBoard = cloneBoard(board);
13688
+ const testPair = {
13689
+ primary: { ...pair.primary },
13690
+ secondary: { ...pair.secondary },
13691
+ col: pair.col,
13692
+ row: pair.row,
13693
+ orientation: pair.orientation
13694
+ };
13695
+ while (true) {
13696
+ const next = { ...testPair, row: testPair.row + 1 };
13697
+ if (!isValidPosition(next, testBoard)) break;
13698
+ testPair.row++;
13699
+ }
13700
+ lockPair(testPair, testBoard);
13701
+ applyGravityFull(testBoard, []);
13702
+ if (simulateDepth > 0) {
13703
+ const pg = detectPowerGems(testBoard);
13704
+ const targets = findCrashTargets(testBoard, pg);
13705
+ if (targets.length > 0) {
13706
+ clearGems(testBoard, targets);
13707
+ applyGravityFull(testBoard, []);
13708
+ }
13709
+ }
13710
+ return evaluateBoard(testBoard);
13711
+ }
13712
+ function selectMove(player, ai) {
13713
+ const pair = player.currentPair;
13714
+ if (!pair) return;
13715
+ if (Math.random() < ai.difficulty.mistakeRate) {
13716
+ ai.targetCol = Math.floor(Math.random() * BOARD_COLS);
13717
+ ai.targetOrientation = Math.floor(Math.random() * 4);
13718
+ ai.decided = true;
13719
+ return;
13720
+ }
13721
+ const options = [];
13722
+ for (let orient = 0; orient < 4; orient++) {
13723
+ for (let col = 0; col < BOARD_COLS; col++) {
13724
+ const testPair = {
13725
+ primary: { ...pair.primary },
13726
+ secondary: { ...pair.secondary },
13727
+ col,
13728
+ row: 0,
13729
+ orientation: orient
13730
+ };
13731
+ if (!isValidPosition(testPair, player.board)) continue;
13732
+ const score = evaluatePlacement(testPair, player.board, ai.difficulty.simulateDepth);
13733
+ options.push({ col, orientation: orient, score });
13734
+ }
13735
+ }
13736
+ if (options.length === 0) {
13737
+ ai.targetCol = pair.col;
13738
+ ai.targetOrientation = pair.orientation;
13739
+ ai.decided = true;
13740
+ return;
13741
+ }
13742
+ options.sort((a, b) => b.score - a.score);
13743
+ const best = options[0];
13744
+ ai.targetCol = best.col;
13745
+ ai.targetOrientation = best.orientation;
13746
+ ai.decided = true;
13747
+ }
13748
+ function aiTick(player, ai) {
13749
+ if (!player.currentPair) return "none";
13750
+ ai.thinkTimer++;
13751
+ if (!ai.decided) {
13752
+ if (ai.thinkTimer >= ai.difficulty.thinkFrames) {
13753
+ selectMove(player, ai);
13754
+ ai.thinkTimer = 0;
13755
+ }
13756
+ return "none";
13757
+ }
13758
+ ai.moveTimer++;
13759
+ const moveInterval = Math.max(2, Math.floor(ai.difficulty.thinkFrames / 3));
13760
+ if (ai.moveTimer < moveInterval) return "none";
13761
+ ai.moveTimer = 0;
13762
+ const pair = player.currentPair;
13763
+ if (pair.orientation !== ai.targetOrientation) {
13764
+ const cwDist = (ai.targetOrientation - pair.orientation + 4) % 4;
13765
+ const ccwDist = (pair.orientation - ai.targetOrientation + 4) % 4;
13766
+ return cwDist <= ccwDist ? "rotate_cw" : "rotate_ccw";
13767
+ }
13768
+ if (pair.col < ai.targetCol) {
13769
+ return "move";
13770
+ } else if (pair.col > ai.targetCol) {
13771
+ return "move";
13772
+ }
13773
+ return "drop";
13774
+ }
13775
+ function getAIMoveDirection(player, ai) {
13776
+ if (!player.currentPair) return 0;
13777
+ if (player.currentPair.col < ai.targetCol) return 1;
13778
+ if (player.currentPair.col > ai.targetCol) return -1;
13779
+ return 0;
13780
+ }
13781
+
13782
+ // src/games/hyper-fighter/effects.ts
13783
+ var GEM_ANSI = {
13784
+ red: "\x1B[1;38;5;196m",
13785
+ green: "\x1B[1;38;5;46m",
13786
+ blue: "\x1B[1;38;5;27m",
13787
+ yellow: "\x1B[1;38;5;226m"
13788
+ };
13789
+ var PARTICLE_CHARS = ["\u2726", "\u2605", "\u25C6", "\u25CF", "\xB7", "*", "\u25AA"];
13790
+ function spawnClearParticles(x, y, color, count, particles) {
13791
+ const ansi = GEM_ANSI[color];
13792
+ for (let i = 0; i < count; i++) {
13793
+ particles.push({
13794
+ x: x + (Math.random() - 0.5) * 2,
13795
+ y: y + (Math.random() - 0.5),
13796
+ char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
13797
+ color: ansi,
13798
+ vx: (Math.random() - 0.5) * 3,
13799
+ vy: (Math.random() - 0.8) * 2,
13800
+ life: 8 + Math.floor(Math.random() * 8),
13801
+ maxLife: 16
13802
+ });
13803
+ }
13804
+ }
13805
+ function spawnFirework2(x, y, particles) {
13806
+ const colors = ["\x1B[91m", "\x1B[93m", "\x1B[92m", "\x1B[96m", "\x1B[95m"];
13807
+ for (let i = 0; i < 20; i++) {
13808
+ const angle = Math.PI * 2 * i / 20;
13809
+ const speed = 1.5 + Math.random();
13810
+ particles.push({
13811
+ x,
13812
+ y,
13813
+ char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
13814
+ color: colors[Math.floor(Math.random() * colors.length)],
13815
+ vx: Math.cos(angle) * speed,
13816
+ vy: Math.sin(angle) * speed * 0.5,
13817
+ life: 12 + Math.floor(Math.random() * 8),
13818
+ maxLife: 20
13819
+ });
13820
+ }
13821
+ }
13822
+ function spawnCollapse(startX, startY, width, height, particles) {
13823
+ for (let r = 0; r < height; r++) {
13824
+ for (let c = 0; c < width; c++) {
13825
+ if (Math.random() < 0.4) {
13826
+ particles.push({
13827
+ x: startX + c * 2,
13828
+ y: startY + r,
13829
+ char: "\u2593",
13830
+ color: "\x1B[90m",
13831
+ vx: (Math.random() - 0.5) * 0.5,
13832
+ vy: 0.3 + Math.random() * 0.5,
13833
+ life: 10 + Math.floor(Math.random() * 15),
13834
+ maxLife: 25
13835
+ });
13836
+ }
13837
+ }
13838
+ }
13839
+ }
13840
+ function updateParticles2(particles) {
13841
+ for (let i = particles.length - 1; i >= 0; i--) {
13842
+ const p = particles[i];
13843
+ p.x += p.vx;
13844
+ p.y += p.vy;
13845
+ p.vy += 0.08;
13846
+ p.life--;
13847
+ if (p.life <= 0) {
13848
+ particles.splice(i, 1);
13849
+ }
13850
+ }
13851
+ }
13852
+ function renderParticles(particles, minX, minY, maxX, maxY) {
13853
+ let output = "";
13854
+ for (const p of particles) {
13855
+ const px = Math.round(p.x);
13856
+ const py = Math.round(p.y);
13857
+ if (px < minX || px > maxX || py < minY || py > maxY) continue;
13858
+ const fade = p.life > p.maxLife * 0.5 ? "\x1B[1m" : "\x1B[2m";
13859
+ output += `\x1B[${py};${px}H${fade}${p.color}${p.char}\x1B[0m`;
13860
+ }
13861
+ return output;
13862
+ }
13863
+ var COMBO_MESSAGES = [
13864
+ { text: "NICE!", color: "\x1B[92m" },
13865
+ { text: "GREAT!", color: "\x1B[93m" },
13866
+ { text: "AMAZING!", color: "\x1B[96m" },
13867
+ { text: "UNSTOPPABLE!", color: "\x1B[95m" },
13868
+ { text: "GODLIKE!!", color: "\x1B[91m" },
13869
+ { text: "GODLIKE!!", color: "\x1B[1;91m" }
13870
+ ];
13871
+ function spawnComboText(chains, x, y, texts) {
13872
+ const idx = Math.min(chains - 1, COMBO_MESSAGES.length - 1);
13873
+ const msg = COMBO_MESSAGES[idx];
13874
+ texts.push({
13875
+ text: `${msg.text} \xD7${chains}`,
13876
+ x: x - Math.floor(msg.text.length / 2),
13877
+ y,
13878
+ color: msg.color,
13879
+ frames: 20,
13880
+ maxFrames: 20
13881
+ });
13882
+ }
13883
+ function spawnChainCounter(chains, x, y, texts) {
13884
+ texts.push({
13885
+ text: `${chains} CHAIN`,
13886
+ x: x - 3,
13887
+ y: y + 1,
13888
+ color: "\x1B[1;97m",
13889
+ frames: 16,
13890
+ maxFrames: 16
13891
+ });
13892
+ }
13893
+ function updateFloatingTexts(texts) {
13894
+ for (let i = texts.length - 1; i >= 0; i--) {
13895
+ const t = texts[i];
13896
+ t.frames--;
13897
+ if (t.frames % 3 === 0) t.y -= 1;
13898
+ if (t.frames <= 0) {
13899
+ texts.splice(i, 1);
13900
+ }
13901
+ }
13902
+ }
13903
+ function renderFloatingTexts(texts) {
13904
+ let output = "";
13905
+ for (const t of texts) {
13906
+ if (t.y < 1) continue;
13907
+ const fade = t.frames > t.maxFrames * 0.4 ? "\x1B[1m" : "\x1B[2m";
13908
+ output += `\x1B[${t.y};${Math.max(1, t.x)}H${fade}${t.color}${t.text}\x1B[0m`;
13909
+ }
13910
+ return output;
13911
+ }
13912
+ function spawnProjectile(fromX, toX, y, count, projectiles) {
13913
+ let char;
13914
+ let color;
13915
+ if (count >= 8) {
13916
+ char = "\u25C8";
13917
+ color = "\x1B[1;91m";
13918
+ } else if (count >= 4) {
13919
+ char = "\u25C6";
13920
+ color = "\x1B[1;93m";
13921
+ } else {
13922
+ char = "\u25CF";
13923
+ color = "\x1B[1;97m";
13924
+ }
13925
+ projectiles.push({ fromX, toX, y, progress: 0, char, color, count });
13926
+ }
13927
+ function updateProjectiles(projectiles) {
13928
+ for (let i = projectiles.length - 1; i >= 0; i--) {
13929
+ const p = projectiles[i];
13930
+ p.progress += 1 / 7;
13931
+ if (p.progress >= 1) {
13932
+ projectiles.splice(i, 1);
13933
+ }
13934
+ }
13935
+ }
13936
+ function renderProjectiles(projectiles) {
13937
+ let output = "";
13938
+ for (const p of projectiles) {
13939
+ const x = Math.round(p.fromX + (p.toX - p.fromX) * p.progress);
13940
+ if (x < 1) continue;
13941
+ output += `\x1B[${p.y};${x}H${p.color}${p.char}\x1B[0m`;
13942
+ }
13943
+ return output;
13944
+ }
13945
+ function renderPortrait(lines, x, y, color) {
13946
+ let output = "";
13947
+ for (let i = 0; i < lines.length; i++) {
13948
+ output += `\x1B[${y + i};${x}H${color}${lines[i]}\x1B[0m`;
13949
+ }
13950
+ return output;
13951
+ }
13952
+ function triggerShake(shake, intensity, frames) {
13953
+ shake.intensity = Math.max(shake.intensity, intensity);
13954
+ shake.frames = Math.max(shake.frames, frames);
13955
+ }
13956
+ function updateShake(shake) {
13957
+ if (shake.frames <= 0) return { dx: 0, dy: 0 };
13958
+ shake.frames--;
13959
+ if (shake.frames <= 0) {
13960
+ shake.intensity = 0;
13961
+ return { dx: 0, dy: 0 };
13962
+ }
13963
+ return {
13964
+ dx: Math.round((Math.random() - 0.5) * shake.intensity * 2),
13965
+ dy: Math.round((Math.random() - 0.5) * shake.intensity)
13966
+ };
13967
+ }
13968
+ function triggerFlash(flash, color, frames) {
13969
+ flash.color = color;
13970
+ flash.frames = frames;
13971
+ }
13972
+ function updateFlash(flash) {
13973
+ if (flash.frames <= 0) return null;
13974
+ flash.frames--;
13975
+ return flash.color;
13976
+ }
13977
+ function renderEnergyBar(x, y, level, maxLevel) {
13978
+ const barHeight = 6;
13979
+ const filled = Math.min(barHeight, Math.round(level / Math.max(1, maxLevel) * barHeight));
13980
+ let output = "";
13981
+ for (let i = 0; i < barHeight; i++) {
13982
+ const isFilled = i >= barHeight - filled;
13983
+ const char = isFilled ? "\u2588" : "\u2591";
13984
+ const color = isFilled ? filled >= barHeight - 1 ? "\x1B[91m" : filled >= barHeight / 2 ? "\x1B[93m" : "\x1B[92m" : "\x1B[90m";
13985
+ output += `\x1B[${y + i};${x}H${color}${char}\x1B[0m`;
13986
+ }
13987
+ return output;
13988
+ }
13989
+
13990
+ // src/games/hyper-fighter/characters.ts
13991
+ var R = "red";
13992
+ var G = "green";
13993
+ var B = "blue";
13994
+ var Y = "yellow";
13995
+ var ryu = {
13996
+ id: "ryu",
13997
+ name: "Ryu",
13998
+ description: "Vertical columns",
13999
+ damageModifier: 1,
14000
+ dropPattern: [
14001
+ [R, G, B, Y, R, G],
14002
+ [R, G, B, Y, R, G],
14003
+ [R, G, B, Y, R, G],
14004
+ [R, G, B, Y, R, G]
14005
+ ],
14006
+ portraits: {
14007
+ idle: [" __ ", " (-_-)", " /| "],
14008
+ attack: [" _\\__ ", " (>o<)", " =|/ "],
14009
+ hit: [" __ ", " (x_x)", " /| "],
14010
+ win: [" \\__/ ", " (^o^)", " /\\ "],
14011
+ lose: [" __ ", " (;_;)", " | "]
14012
+ }
14013
+ };
14014
+ var ken = {
14015
+ id: "ken",
14016
+ name: "Ken",
14017
+ description: "Horizontal rows",
14018
+ damageModifier: 1,
14019
+ dropPattern: [
14020
+ [Y, Y, Y, Y, Y, Y],
14021
+ [G, G, G, G, G, G],
14022
+ [B, B, B, B, B, B],
14023
+ [R, R, R, R, R, R]
14024
+ ],
14025
+ portraits: {
14026
+ idle: [" ^^^ ", " [>_>]", " /| "],
14027
+ attack: [" ^^^/ ", " [>o<]", " /=| "],
14028
+ hit: [" ^^^ ", " [x_x]", " /| "],
14029
+ win: [" \\^^/ ", " [^o^]", " /\\ "],
14030
+ lose: [" ^^^ ", " [;_;]", " | "]
14031
+ }
14032
+ };
14033
+ var chunLi = {
14034
+ id: "chunli",
14035
+ name: "Chun-Li",
14036
+ description: "2x2 color blocks",
14037
+ damageModifier: 1.2,
14038
+ dropPattern: [
14039
+ [R, R, G, G, B, B],
14040
+ [R, R, G, G, B, B],
14041
+ [Y, Y, R, R, G, G],
14042
+ [Y, Y, R, R, G, G]
14043
+ ],
14044
+ portraits: {
14045
+ idle: [" @ @ ", " {^.^}", " /| "],
14046
+ attack: [" @ @\\", " {>.<}", " /|= "],
14047
+ hit: [" @ @ ", " {x.x}", " /| "],
14048
+ win: ["\\@ @/", " {^o^}", " /\\ "],
14049
+ lose: [" @ @ ", " {;.;}", " | "]
14050
+ }
14051
+ };
14052
+ var sakura = {
14053
+ id: "sakura",
14054
+ name: "Sakura",
14055
+ description: "Fixed edges, alt middle",
14056
+ damageModifier: 1,
14057
+ dropPattern: [
14058
+ [G, R, B, R, B, Y],
14059
+ [G, B, R, B, R, Y],
14060
+ [G, R, B, R, B, Y],
14061
+ [G, B, R, B, R, Y]
14062
+ ],
14063
+ portraits: {
14064
+ idle: [" >> ", " <*_*>", " /| "],
14065
+ attack: [" >>/ ", " <*o*>", " /|= "],
14066
+ hit: [" >> ", " <x_x>", " /| "],
14067
+ win: [" \\>>/ ", " <^o^>", " /\\ "],
14068
+ lose: [" >> ", " <;_;>", " | "]
14069
+ }
14070
+ };
14071
+ var morrigan = {
14072
+ id: "morrigan",
14073
+ name: "Morrigan",
14074
+ description: "Symmetric mirrored",
14075
+ damageModifier: 1,
14076
+ dropPattern: [
14077
+ [B, G, R, R, G, B],
14078
+ [G, B, R, R, B, G],
14079
+ [R, G, B, B, G, R],
14080
+ [G, R, B, B, R, G]
14081
+ ],
14082
+ portraits: {
14083
+ idle: [" ~ ~ ", " ~^_^~", " /| "],
14084
+ attack: [" ~/~\\ ", " ~>_<~", " /| "],
14085
+ hit: [" ~ ~ ", " ~x_x~", " /| "],
14086
+ win: ["\\~ ~/", " ~^o~ ", " /\\ "],
14087
+ lose: [" ~ ~ ", " ~;_;~", " | "]
14088
+ }
14089
+ };
14090
+ var hsienKo = {
14091
+ id: "hsienKo",
14092
+ name: "Hsien-Ko",
14093
+ description: "Diagonal staircase",
14094
+ damageModifier: 1,
14095
+ dropPattern: [
14096
+ [R, G, B, Y, R, G],
14097
+ [G, B, Y, R, G, B],
14098
+ [B, Y, R, G, B, Y],
14099
+ [Y, R, G, B, Y, R]
14100
+ ],
14101
+ portraits: {
14102
+ idle: [" == ", " |o_o|", " /| "],
14103
+ attack: [" ==\\ ", " |o_o|", " /|= "],
14104
+ hit: [" == ", " |x_x|", " /| "],
14105
+ win: [" \\==/ ", " |^o^|", " /\\ "],
14106
+ lose: [" == ", " |;_;|", " | "]
14107
+ }
14108
+ };
14109
+ var felicia = {
14110
+ id: "felicia",
14111
+ name: "Felicia",
14112
+ description: "Fixed edges, swap mid",
14113
+ damageModifier: 1,
14114
+ dropPattern: [
14115
+ [G, R, B, R, B, Y],
14116
+ [G, B, R, B, R, Y],
14117
+ [Y, R, B, R, B, G],
14118
+ [Y, B, R, B, R, G]
14119
+ ],
14120
+ portraits: {
14121
+ idle: [" /\\/\\ ", " =^w^=", " /| "],
14122
+ attack: [" /\\/\\\\", " =^o^=", " /|= "],
14123
+ hit: [" /\\/\\ ", " =x_x=", " /| "],
14124
+ win: ["\\/\\/\\/", " =^w^=", " /\\ "],
14125
+ lose: [" /\\/\\ ", " =;w;=", " | "]
14126
+ }
14127
+ };
14128
+ var donovan = {
14129
+ id: "donovan",
14130
+ name: "Donovan",
14131
+ description: "3-col halves + alt base",
14132
+ damageModifier: 1,
14133
+ dropPattern: [
14134
+ [R, R, R, G, G, G],
14135
+ [R, R, R, G, G, G],
14136
+ [B, B, B, Y, Y, Y],
14137
+ [R, G, B, R, G, B]
14138
+ ],
14139
+ portraits: {
14140
+ idle: [" || ", " #-_-#", " /| "],
14141
+ attack: [" ||/ ", " #>_<#", " /|= "],
14142
+ hit: [" || ", " #x_x#", " /| "],
14143
+ win: [" \\||/ ", " #^o^#", " /\\ "],
14144
+ lose: [" || ", " #;_;#", " | "]
14145
+ }
14146
+ };
14147
+ var dan = {
14148
+ id: "dan",
14149
+ name: "Dan",
14150
+ description: "ALL RED (joke char)",
14151
+ damageModifier: 1,
14152
+ dropPattern: [
14153
+ [R, R, R, R, R, R],
14154
+ [R, R, R, R, R, R],
14155
+ [R, R, R, R, R, R],
14156
+ [R, R, R, R, R, R]
14157
+ ],
14158
+ portraits: {
14159
+ idle: [" ^^ ", " (?_?)", " /| "],
14160
+ attack: [" ^^! ", " (!o!)", " /|~ "],
14161
+ hit: [" ^^ ", " (x_x)", " /| "],
14162
+ win: [" \\^^/ ", " (^o^)", " /\\ "],
14163
+ lose: [" ^^ ", " (T_T)", " | "]
14164
+ }
14165
+ };
14166
+ var akuma = {
14167
+ id: "akuma",
14168
+ name: "Akuma",
14169
+ description: "Diagonal rainbow cycle",
14170
+ damageModifier: 0.7,
14171
+ dropPattern: [
14172
+ [R, Y, B, G, R, Y],
14173
+ [Y, B, G, R, Y, B],
14174
+ [B, G, R, Y, B, G],
14175
+ [G, R, Y, B, G, R]
14176
+ ],
14177
+ portraits: {
14178
+ idle: [" /MM\\ ", " !>_<!", " /| "],
14179
+ attack: [" /MM\\|", " !>o<!", " =/| "],
14180
+ hit: [" /MM\\ ", " !x_x!", " /| "],
14181
+ win: ["\\/MM\\/", " !^_^!", " /\\ "],
14182
+ lose: [" /MM\\ ", " !;_;!", " | "]
14183
+ }
14184
+ };
14185
+ var devilotte = {
14186
+ id: "devilotte",
14187
+ name: "Devilotte",
14188
+ description: "Reverse diagonal rainbow",
14189
+ damageModifier: 0.7,
14190
+ dropPattern: [
14191
+ [G, B, Y, R, G, B],
14192
+ [B, Y, R, G, B, Y],
14193
+ [Y, R, G, B, Y, R],
14194
+ [R, G, B, Y, R, G]
14195
+ ],
14196
+ portraits: {
14197
+ idle: [" vVv ", " $v_v$", " /| "],
14198
+ attack: [" vVv\\", " $>_<$", " /|= "],
14199
+ hit: [" vVv ", " $x_x$", " /| "],
14200
+ win: [" \\vVv/", " $^_^$", " /\\ "],
14201
+ lose: [" vVv ", " $;_;$", " | "]
14202
+ }
14203
+ };
14204
+ var CHARACTERS = [
14205
+ ryu,
14206
+ ken,
14207
+ chunLi,
14208
+ sakura,
14209
+ morrigan,
14210
+ hsienKo,
14211
+ felicia,
14212
+ donovan,
14213
+ dan,
14214
+ akuma,
14215
+ devilotte
14216
+ ];
14217
+ var CHAR_GRID = [
14218
+ [0, 1, 2, 3],
14219
+ [4, 5, 6, 7],
14220
+ [8, 9, 10]
14221
+ ];
14222
+ function getRandomCharacter() {
14223
+ return CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
14224
+ }
14225
+
14226
+ // src/games/hyper-fighter/index.ts
14227
+ var TICK_MS = 50;
14228
+ var MIN_COLS = 60;
14229
+ var MIN_ROWS = 36;
14230
+ var VS_COL_WIDTH = 12;
14231
+ var SIDE_PANEL_WIDTH = 14;
14232
+ var HEADER_HEIGHT = 2;
14233
+ var FOOTER_HEIGHT = 2;
14234
+ var MIN_CELL_WIDTH = 3;
14235
+ var MAX_CELL_WIDTH = 6;
14236
+ var MIN_CELL_HEIGHT = 2;
14237
+ var MAX_CELL_HEIGHT = 4;
14238
+ var GEM_COLORS = {
14239
+ red: "\x1B[1;38;5;196m",
14240
+ green: "\x1B[1;38;5;46m",
14241
+ blue: "\x1B[1;38;5;27m",
14242
+ yellow: "\x1B[1;38;5;226m"
14243
+ };
14244
+ var COUNTER_BG_COLORS = {
14245
+ red: "\x1B[48;5;196m",
14246
+ green: "\x1B[48;5;34m",
14247
+ blue: "\x1B[48;5;27m",
14248
+ yellow: "\x1B[48;5;178m"
14249
+ };
14250
+ var BASE_DROP_SPEED = 16;
14251
+ var MIN_DROP_SPEED = 3;
14252
+ var SPEED_RAMP_DROPS = 8;
14253
+ var PHASE_NONE = 0;
14254
+ var PHASE_FLASH = 1;
14255
+ var PHASE_DISSOLVE = 2;
14256
+ var PHASE_GRAVITY = 3;
14257
+ var PHASE_CHECK = 4;
14258
+ var PHASE_GARBAGE = 5;
14259
+ var FLASH_FRAMES = 6;
14260
+ var DISSOLVE_FRAMES = 4;
14261
+ var GRAVITY_FRAMES = 1;
14262
+ var COUNTER_TIMER_NORMAL = 5;
14263
+ var COUNTER_TIMER_DEFENDED = 3;
14264
+ var GARBAGE_DROP_STEP_FRAMES = 2;
14265
+ function runHyperFighterGame(terminal) {
14266
+ const themeColor = getCurrentThemeColor();
14267
+ let running = true;
14268
+ let gameState = "difficulty";
14269
+ let difficultySelection = 1;
14270
+ let pauseMenuSelection = 0;
14271
+ let p1;
14272
+ let p2;
14273
+ let aiState;
14274
+ let selectedDifficulty = DIFFICULTIES2.normal;
14275
+ let p1Character = CHARACTERS[0];
14276
+ let p2Character = getRandomCharacter();
14277
+ let charGridRow = 0;
14278
+ let charGridCol = 0;
14279
+ let p1DropTimer = 0;
14280
+ let p2DropTimer = 0;
14281
+ let p1DropSpeed = BASE_DROP_SPEED;
14282
+ let p2DropSpeed = BASE_DROP_SPEED;
14283
+ let p1Phase = PHASE_NONE;
14284
+ let p2Phase = PHASE_NONE;
14285
+ let p1PhaseTimer = 0;
14286
+ let p2PhaseTimer = 0;
14287
+ let p1ClearedCells = [];
14288
+ let p2ClearedCells = [];
14289
+ let p1ChainCount = 0;
14290
+ let p2ChainCount = 0;
14291
+ let p1TotalCleared = 0;
14292
+ let p2TotalCleared = 0;
14293
+ let p1GarbageDrop = null;
14294
+ let p2GarbageDrop = null;
14295
+ let p1GarbagePatternCursor = 0;
14296
+ let p2GarbagePatternCursor = 0;
14297
+ let p1IsDiamondClear = false;
14298
+ let p2IsDiamondClear = false;
14299
+ let p1AttackAccum = 0;
14300
+ let p2AttackAccum = 0;
14301
+ let particles = [];
14302
+ let floatingTexts = [];
14303
+ let projectiles = [];
14304
+ let p1Shake = { intensity: 0, frames: 0 };
14305
+ let p2Shake = { intensity: 0, frames: 0 };
14306
+ let p1Flash = { color: "", frames: 0 };
14307
+ let p2Flash = { color: "", frames: 0 };
14308
+ let p1Pose = "idle";
14309
+ let p2Pose = "idle";
14310
+ let p1PoseTimer = 0;
14311
+ let p2PoseTimer = 0;
14312
+ let winner = 0;
14313
+ let gameOverTimer = 0;
14314
+ let glitchFrame = 0;
14315
+ let cellWidth = 3;
14316
+ let cellHeight = 2;
14317
+ let boardDisplayWidth = BOARD_COLS * cellWidth + 2;
14318
+ let boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
14319
+ let cellEmpty = " ".repeat(cellWidth);
14320
+ let cellSolid = "\u2588".repeat(cellWidth);
14321
+ let cellPower = "\u2593".repeat(cellWidth);
14322
+ let cellGhost = "\u2591".repeat(cellWidth);
14323
+ let cellCrash = "\u25C6".repeat(cellWidth);
14324
+ let cellDiamond = "\u25C7".repeat(cellWidth);
14325
+ let cellEmptyDot = " ".repeat(Math.floor((cellWidth - 1) / 2)) + "\x1B[38;5;238m\xB7\x1B[0m" + " ".repeat(cellWidth - Math.floor((cellWidth - 1) / 2) - 1);
14326
+ let showSidePanels = false;
14327
+ let sidePanel1X = 0;
14328
+ let sidePanel2X = 0;
14329
+ let boardLeft1 = 2;
14330
+ let boardLeft2 = 28;
14331
+ let vsColX = 16;
14332
+ let boardTop = 3;
14333
+ function recalcCellStrings() {
14334
+ boardDisplayWidth = BOARD_COLS * cellWidth + 2;
14335
+ boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
14336
+ cellEmpty = " ".repeat(cellWidth);
14337
+ cellSolid = "\u2588".repeat(cellWidth);
14338
+ cellPower = "\u2593".repeat(cellWidth);
14339
+ cellGhost = "\u2591".repeat(cellWidth);
14340
+ cellCrash = "\u25C6".repeat(cellWidth);
14341
+ cellDiamond = "\u25C7".repeat(cellWidth);
14342
+ const padLeft = Math.floor((cellWidth - 1) / 2);
14343
+ cellEmptyDot = " ".repeat(padLeft) + "\xB7" + " ".repeat(cellWidth - padLeft - 1);
14344
+ }
14345
+ const controller = {
14346
+ stop: () => {
14347
+ if (!running) return;
14348
+ running = false;
14349
+ },
14350
+ get isRunning() {
14351
+ return running;
14352
+ }
14353
+ };
14354
+ const title = [
14355
+ "\u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588",
14356
+ "\u2588\u2580\u2588 \u2580\u2584\u2580 \u2588\u2580\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584",
14357
+ "\u2588\u2580\u2580 \u2588 \u2588\u2580\u2580 \u2588 \u2588 \u2580\u2588\u2580 \u2588\u2580\u2580 \u2588\u2580\u2588",
14358
+ "\u2588\u2580 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588 \u2588\u2588\u2584 \u2588\u2580\u2584"
14359
+ ];
14360
+ const GLITCH_CHARS2 = "!@#$%^&*\u2591\u2592\u2593\u2588\u2580\u2584";
14361
+ function initGame() {
14362
+ p1 = createPlayerState();
14363
+ p2 = createPlayerState();
14364
+ aiState = createAIState(selectedDifficulty);
14365
+ p1DropTimer = 0;
14366
+ p2DropTimer = 0;
14367
+ p1DropSpeed = BASE_DROP_SPEED;
14368
+ p2DropSpeed = BASE_DROP_SPEED;
14369
+ p1Phase = PHASE_NONE;
14370
+ p2Phase = PHASE_NONE;
14371
+ p1PhaseTimer = 0;
14372
+ p2PhaseTimer = 0;
14373
+ p1ClearedCells = [];
14374
+ p2ClearedCells = [];
14375
+ p1ChainCount = 0;
14376
+ p2ChainCount = 0;
14377
+ p1TotalCleared = 0;
14378
+ p2TotalCleared = 0;
14379
+ p1GarbageDrop = null;
14380
+ p2GarbageDrop = null;
14381
+ p1GarbagePatternCursor = 0;
14382
+ p2GarbagePatternCursor = 0;
14383
+ p1IsDiamondClear = false;
14384
+ p2IsDiamondClear = false;
14385
+ p1AttackAccum = 0;
14386
+ p2AttackAccum = 0;
14387
+ particles = [];
14388
+ floatingTexts = [];
14389
+ projectiles = [];
14390
+ p1Shake = { intensity: 0, frames: 0 };
14391
+ p2Shake = { intensity: 0, frames: 0 };
14392
+ p1Flash = { color: "", frames: 0 };
14393
+ p2Flash = { color: "", frames: 0 };
14394
+ p1Pose = "idle";
14395
+ p2Pose = "idle";
14396
+ p1PoseTimer = 0;
14397
+ p2PoseTimer = 0;
14398
+ winner = 0;
14399
+ gameOverTimer = 0;
14400
+ pauseMenuSelection = 0;
14401
+ spawnPair(p1);
14402
+ spawnPair(p2);
14403
+ }
14404
+ function calculateLayout() {
14405
+ const cols = terminal.cols;
14406
+ const rows = terminal.rows;
14407
+ cellHeight = Math.max(MIN_CELL_HEIGHT, Math.min(
14408
+ MAX_CELL_HEIGHT,
14409
+ Math.floor((rows - HEADER_HEIGHT - FOOTER_HEIGHT - 2 - 2) / BOARD_ROWS)
14410
+ ));
14411
+ const availWidthWithPanels = cols - 2 * SIDE_PANEL_WIDTH - VS_COL_WIDTH - 4;
14412
+ const availWidthWithout = cols - VS_COL_WIDTH - 4;
14413
+ const cwWithPanels = Math.floor(availWidthWithPanels / (2 * BOARD_COLS));
14414
+ const cwWithout = Math.floor(availWidthWithout / (2 * BOARD_COLS));
14415
+ if (cwWithPanels >= MIN_CELL_WIDTH) {
14416
+ showSidePanels = true;
14417
+ cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithPanels));
14418
+ } else {
14419
+ showSidePanels = false;
14420
+ cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithout));
14421
+ }
14422
+ const maxAspectWidth = Math.floor(cellHeight * 1.5);
14423
+ if (cellWidth > maxAspectWidth && maxAspectWidth >= MIN_CELL_WIDTH) {
14424
+ cellWidth = maxAspectWidth;
14425
+ }
14426
+ recalcCellStrings();
14427
+ if (showSidePanels) {
14428
+ const totalWidth = SIDE_PANEL_WIDTH + boardDisplayWidth + VS_COL_WIDTH + boardDisplayWidth + SIDE_PANEL_WIDTH;
14429
+ const startX = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
14430
+ sidePanel1X = startX;
14431
+ boardLeft1 = startX + SIDE_PANEL_WIDTH;
14432
+ vsColX = boardLeft1 + boardDisplayWidth + 1;
14433
+ boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
14434
+ sidePanel2X = boardLeft2 + boardDisplayWidth;
14435
+ } else {
14436
+ const totalWidth = boardDisplayWidth * 2 + VS_COL_WIDTH;
14437
+ boardLeft1 = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
14438
+ boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
14439
+ vsColX = boardLeft1 + boardDisplayWidth + 1;
14440
+ sidePanel1X = 0;
14441
+ sidePanel2X = 0;
14442
+ }
14443
+ const availHeight = rows - HEADER_HEIGHT - FOOTER_HEIGHT;
14444
+ boardTop = HEADER_HEIGHT + Math.max(1, Math.floor((availHeight - boardDisplayHeight) / 2));
14445
+ }
14446
+ function getDropSpeed(player) {
14447
+ const ramp = Math.floor(player.totalDrops / SPEED_RAMP_DROPS);
14448
+ return Math.max(MIN_DROP_SPEED, BASE_DROP_SPEED - ramp);
14449
+ }
14450
+ function startResolution(player, isP1) {
14451
+ if (isP1) p1AttackAccum = 0;
14452
+ else p2AttackAccum = 0;
14453
+ const diamondCleared = resolveDiamond(player.board);
14454
+ if (diamondCleared.length > 0) {
14455
+ applyGravityFull(player.board, player.powerGems);
14456
+ if (isP1) p1IsDiamondClear = true;
14457
+ else p2IsDiamondClear = true;
14458
+ } else {
14459
+ if (isP1) p1IsDiamondClear = false;
14460
+ else p2IsDiamondClear = false;
14461
+ }
14462
+ player.powerGems = detectPowerGems(player.board);
14463
+ const { cleared, clearedCells: crashCells, powerGemSizes } = resolveOneStep(player);
14464
+ const clearedCells = [...diamondCleared, ...crashCells];
14465
+ const totalStepCleared = cleared + diamondCleared.length;
14466
+ if (clearedCells.length === 0) {
14467
+ finishResolution(player, isP1);
14468
+ return;
14469
+ }
14470
+ if (isP1) {
14471
+ p1ClearedCells = clearedCells;
14472
+ p1ChainCount++;
14473
+ p1TotalCleared += totalStepCleared;
14474
+ p1Phase = PHASE_FLASH;
14475
+ p1PhaseTimer = FLASH_FRAMES;
14476
+ const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p1ChainCount };
14477
+ p1AttackAccum += calculateStepAttack(stepInfo);
14478
+ } else {
14479
+ p2ClearedCells = clearedCells;
14480
+ p2ChainCount++;
14481
+ p2TotalCleared += totalStepCleared;
14482
+ p2Phase = PHASE_FLASH;
14483
+ p2PhaseTimer = FLASH_FRAMES;
14484
+ const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p2ChainCount };
14485
+ p2AttackAccum += calculateStepAttack(stepInfo);
14486
+ }
14487
+ const bLeft = isP1 ? boardLeft1 : boardLeft2;
14488
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14489
+ if (chainCount >= 1) {
14490
+ spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
14491
+ if (chainCount >= 2) {
14492
+ spawnChainCounter(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 3, floatingTexts);
14493
+ }
14494
+ }
14495
+ for (const cell of clearedCells) {
14496
+ const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
14497
+ const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
14498
+ spawnClearParticles(px, py, cell.color, 3, particles);
14499
+ }
14500
+ const shakeAmount = Math.min(3, Math.ceil(clearedCells.length / 4));
14501
+ if (isP1) {
14502
+ triggerShake(p1Shake, shakeAmount, 6);
14503
+ triggerFlash(p1Flash, "\x1B[97m", 4);
14504
+ p1Pose = "attack";
14505
+ p1PoseTimer = 12;
14506
+ } else {
14507
+ triggerShake(p2Shake, shakeAmount, 6);
14508
+ triggerFlash(p2Flash, "\x1B[97m", 4);
14509
+ p2Pose = "attack";
14510
+ p2PoseTimer = 12;
14511
+ }
14512
+ }
14513
+ function resolveOneStep(player) {
14514
+ player.powerGems = detectPowerGems(player.board);
14515
+ const { targets, destroyedPowerGemSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14516
+ if (targets.length === 0) return { cleared: 0, chains: 0, clearedCells: [], powerGemSizes: [] };
14517
+ for (const t of targets) {
14518
+ player.board[t.row][t.col] = null;
14519
+ }
14520
+ applyGravityFull(player.board, player.powerGems);
14521
+ player.powerGems = [];
14522
+ return { cleared: targets.length, chains: 1, clearedCells: targets, powerGemSizes: destroyedPowerGemSizes };
14523
+ }
14524
+ function buildGarbageDropState(player, defendedCount, normalCount, startCursor, dropPattern) {
14525
+ const occupied = player.board.map((row) => row.map((cell) => cell !== null));
14526
+ const timers = [];
14527
+ for (let i = 0; i < defendedCount; i++) timers.push(COUNTER_TIMER_DEFENDED);
14528
+ for (let i = 0; i < normalCount; i++) timers.push(COUNTER_TIMER_NORMAL);
14529
+ const gems = [];
14530
+ const patternRows = dropPattern.length;
14531
+ const patternCols = dropPattern[0].length;
14532
+ const patternLen = patternRows * patternCols;
14533
+ let placedCount = 0;
14534
+ function findLandingRow(col) {
14535
+ for (let r = BOARD_ROWS - 1; r >= 0; r--) {
14536
+ if (!occupied[r][col]) return r;
14537
+ }
14538
+ return -1;
14539
+ }
14540
+ for (let i = 0; i < timers.length; i++) {
14541
+ const patternIdx = (startCursor + i) % patternLen;
14542
+ const pRow = Math.floor(patternIdx / patternCols);
14543
+ const pCol = patternIdx % patternCols;
14544
+ const color = dropPattern[pRow][pCol];
14545
+ let placeCol = -1;
14546
+ let placeRow = -1;
14547
+ const landing = findLandingRow(pCol);
14548
+ if (landing >= 0) {
14549
+ placeCol = pCol;
14550
+ placeRow = landing;
14551
+ } else {
14552
+ for (let offset = 1; offset < BOARD_COLS; offset++) {
14553
+ for (const dir of [1, -1]) {
14554
+ const adjCol = pCol + offset * dir;
14555
+ if (adjCol >= 0 && adjCol < BOARD_COLS) {
14556
+ const adjLanding = findLandingRow(adjCol);
14557
+ if (adjLanding >= 0) {
14558
+ placeCol = adjCol;
14559
+ placeRow = adjLanding;
14560
+ break;
14561
+ }
14562
+ }
14563
+ }
14564
+ if (placeRow >= 0) break;
14565
+ }
14566
+ }
14567
+ if (placeRow < 0 || placeCol < 0) continue;
14568
+ occupied[placeRow][placeCol] = true;
14569
+ gems.push({
14570
+ col: placeCol,
14571
+ targetRow: placeRow,
14572
+ currentRow: 0,
14573
+ timer: timers[i],
14574
+ color,
14575
+ delayFrames: i
14576
+ });
14577
+ placedCount++;
14578
+ }
14579
+ if (gems.length === 0) {
14580
+ return { dropState: null, nextCursor: startCursor };
14581
+ }
14582
+ const nextCursor = (startCursor + placedCount) % patternLen;
14583
+ return { dropState: { gems, frameTick: 0 }, nextCursor };
14584
+ }
14585
+ function triggerLoss(isP1) {
14586
+ const loser = isP1 ? p1 : p2;
14587
+ loser.alive = false;
14588
+ gameState = "gameOver";
14589
+ winner = isP1 ? 2 : 1;
14590
+ gameOverTimer = 0;
14591
+ pauseMenuSelection = 0;
14592
+ if (isP1) {
14593
+ p1Pose = "lose";
14594
+ p2Pose = "win";
14595
+ } else {
14596
+ p2Pose = "lose";
14597
+ p1Pose = "win";
14598
+ }
14599
+ const loserLeft = isP1 ? boardLeft1 : boardLeft2;
14600
+ const winnerLeft = isP1 ? boardLeft2 : boardLeft1;
14601
+ spawnCollapse(loserLeft, boardTop, boardDisplayWidth, BOARD_ROWS * cellHeight, particles);
14602
+ spawnFirework2(winnerLeft + boardDisplayWidth / 2, boardTop + 3, particles);
14603
+ }
14604
+ function finalizePostResolution(player, isP1) {
14605
+ if (checkGameOver(player.board)) {
14606
+ triggerLoss(isP1);
14607
+ return;
14608
+ }
14609
+ player.powerGems = detectPowerGems(player.board);
14610
+ if (!spawnPair(player)) {
14611
+ triggerLoss(isP1);
14612
+ }
14613
+ }
14614
+ function tickGarbageDrop(player, isP1) {
14615
+ const dropState = isP1 ? p1GarbageDrop : p2GarbageDrop;
14616
+ if (!dropState) {
14617
+ if (isP1) p1Phase = PHASE_NONE;
14618
+ else p2Phase = PHASE_NONE;
14619
+ finalizePostResolution(player, isP1);
14620
+ return;
14621
+ }
14622
+ dropState.frameTick++;
14623
+ if (dropState.frameTick < GARBAGE_DROP_STEP_FRAMES) return;
14624
+ dropState.frameTick = 0;
14625
+ let stillDropping = false;
14626
+ for (const gem of dropState.gems) {
14627
+ if (gem.delayFrames > 0) {
14628
+ gem.delayFrames--;
14629
+ stillDropping = true;
14630
+ continue;
14631
+ }
14632
+ if (gem.currentRow < gem.targetRow) {
14633
+ gem.currentRow++;
14634
+ stillDropping = true;
14635
+ }
14636
+ }
14637
+ if (stillDropping) return;
14638
+ for (const gem of dropState.gems) {
14639
+ if (player.board[gem.targetRow][gem.col] !== null) continue;
14640
+ player.board[gem.targetRow][gem.col] = {
14641
+ color: gem.color,
14642
+ type: "counter",
14643
+ counterTimer: gem.timer
14644
+ };
14645
+ }
14646
+ if (isP1) {
14647
+ p1GarbageDrop = null;
14648
+ p1Phase = PHASE_NONE;
14649
+ } else {
14650
+ p2GarbageDrop = null;
14651
+ p2Phase = PHASE_NONE;
14652
+ }
14653
+ finalizePostResolution(player, isP1);
14654
+ }
14655
+ function tickResolution(isP1) {
14656
+ const phase = isP1 ? p1Phase : p2Phase;
14657
+ const timer = isP1 ? p1PhaseTimer : p2PhaseTimer;
14658
+ const player = isP1 ? p1 : p2;
14659
+ if (phase === PHASE_NONE) return;
14660
+ if (timer > 0) {
14661
+ if (isP1) p1PhaseTimer--;
14662
+ else p2PhaseTimer--;
14663
+ return;
14664
+ }
14665
+ switch (phase) {
14666
+ case PHASE_FLASH:
14667
+ if (isP1) {
14668
+ p1Phase = PHASE_DISSOLVE;
14669
+ p1PhaseTimer = DISSOLVE_FRAMES;
14670
+ } else {
14671
+ p2Phase = PHASE_DISSOLVE;
14672
+ p2PhaseTimer = DISSOLVE_FRAMES;
14673
+ }
14674
+ break;
14675
+ case PHASE_DISSOLVE:
14676
+ if (isP1) {
14677
+ p1Phase = PHASE_GRAVITY;
14678
+ p1PhaseTimer = GRAVITY_FRAMES;
14679
+ } else {
14680
+ p2Phase = PHASE_GRAVITY;
14681
+ p2PhaseTimer = GRAVITY_FRAMES;
14682
+ }
14683
+ break;
14684
+ case PHASE_GRAVITY:
14685
+ if (isP1) {
14686
+ p1Phase = PHASE_CHECK;
14687
+ p1PhaseTimer = 0;
14688
+ } else {
14689
+ p2Phase = PHASE_CHECK;
14690
+ p2PhaseTimer = 0;
14691
+ }
14692
+ break;
14693
+ case PHASE_GARBAGE:
14694
+ tickGarbageDrop(player, isP1);
14695
+ break;
14696
+ case PHASE_CHECK: {
14697
+ player.powerGems = detectPowerGems(player.board);
14698
+ const { targets: more, destroyedPowerGemSizes: pgSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14699
+ if (more.length > 0) {
14700
+ for (const t of more) {
14701
+ player.board[t.row][t.col] = null;
14702
+ }
14703
+ applyGravityFull(player.board, player.powerGems);
14704
+ player.powerGems = [];
14705
+ if (isP1) {
14706
+ p1ClearedCells = more;
14707
+ p1ChainCount++;
14708
+ p1TotalCleared += more.length;
14709
+ p1Phase = PHASE_FLASH;
14710
+ p1PhaseTimer = FLASH_FRAMES;
14711
+ const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p1ChainCount };
14712
+ p1AttackAccum += calculateStepAttack(stepInfo);
14713
+ } else {
14714
+ p2ClearedCells = more;
14715
+ p2ChainCount++;
14716
+ p2TotalCleared += more.length;
14717
+ p2Phase = PHASE_FLASH;
14718
+ p2PhaseTimer = FLASH_FRAMES;
14719
+ const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p2ChainCount };
14720
+ p2AttackAccum += calculateStepAttack(stepInfo);
14721
+ }
14722
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14723
+ const bLeft = isP1 ? boardLeft1 : boardLeft2;
14724
+ spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
14725
+ for (const cell of more) {
14726
+ const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
14727
+ const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
14728
+ spawnClearParticles(px, py, cell.color, 4 + chainCount, particles);
14729
+ }
14730
+ triggerShake(isP1 ? p1Shake : p2Shake, Math.min(4, chainCount), 8);
14731
+ } else {
14732
+ const totalCleared = isP1 ? p1TotalCleared : p2TotalCleared;
14733
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14734
+ const dmgMod = isP1 ? p1Character.damageModifier : p2Character.damageModifier;
14735
+ const isDiamond = isP1 ? p1IsDiamondClear : p2IsDiamondClear;
14736
+ const rawAttack = isP1 ? p1AttackAccum : p2AttackAccum;
14737
+ let attack = applyAttackModifiers(rawAttack, dmgMod, isDiamond);
14738
+ if (attack > 0 && player.pendingGarbage > 0) {
14739
+ const defense = resolveCounterAttack(attack, player.pendingGarbage);
14740
+ attack = defense.remainingAttack;
14741
+ player.pendingGarbage = defense.remainingPending;
14742
+ player.pendingCounteredGarbage = defense.pendingStartsAtThree ? defense.remainingPending : 0;
14743
+ if (defense.canceledGems > 0) {
14744
+ const left = isP1 ? boardLeft1 : boardLeft2;
14745
+ floatingTexts.push({
14746
+ text: `DEFENSE -${defense.canceledGems}`,
14747
+ x: left + 2,
14748
+ y: Math.max(1, boardTop - 1),
14749
+ color: "\x1B[96m",
14750
+ frames: 18,
14751
+ maxFrames: 18
14752
+ });
14753
+ }
14754
+ }
14755
+ if (attack > 0) {
14756
+ const opponent = isP1 ? p2 : p1;
14757
+ opponent.pendingGarbage += attack;
14758
+ const fromBLeft = isP1 ? boardLeft1 : boardLeft2;
14759
+ const toBLeft = isP1 ? boardLeft2 : boardLeft1;
14760
+ spawnProjectile(
14761
+ fromBLeft + boardDisplayWidth / 2,
14762
+ toBLeft + boardDisplayWidth / 2,
14763
+ boardTop + Math.floor(boardDisplayHeight / 2),
14764
+ attack,
14765
+ projectiles
14766
+ );
14767
+ if (isP1) {
14768
+ p2Pose = "hit";
14769
+ p2PoseTimer = 10;
14770
+ } else {
14771
+ p1Pose = "hit";
14772
+ p1PoseTimer = 10;
14773
+ }
14774
+ triggerFlash(isP1 ? p2Flash : p1Flash, "\x1B[91m", 6);
14775
+ triggerShake(isP1 ? p2Shake : p1Shake, Math.min(3, Math.ceil(attack / 2)), 6);
14776
+ }
14777
+ player.score += totalCleared * 10 + (chainCount > 1 ? chainCount * 50 : 0);
14778
+ finishResolution(player, isP1);
14779
+ }
14780
+ break;
14781
+ }
14782
+ }
14783
+ }
14784
+ function finishResolution(player, isP1) {
14785
+ if (isP1) {
14786
+ p1Phase = PHASE_NONE;
14787
+ p1PhaseTimer = 0;
14788
+ p1ChainCount = 0;
14789
+ p1TotalCleared = 0;
14790
+ p1ClearedCells = [];
14791
+ p1AttackAccum = 0;
14792
+ } else {
14793
+ p2Phase = PHASE_NONE;
14794
+ p2PhaseTimer = 0;
14795
+ p2ChainCount = 0;
14796
+ p2TotalCleared = 0;
14797
+ p2ClearedCells = [];
14798
+ p2AttackAccum = 0;
14799
+ }
14800
+ decrementCounters(player);
14801
+ player.powerGems = detectPowerGems(player.board);
14802
+ const { targets: postCounterTargets } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14803
+ if (postCounterTargets.length > 0) {
14804
+ startResolution(player, isP1);
14805
+ return;
14806
+ }
14807
+ if (player.pendingGarbage > 0) {
14808
+ const incomingCount = player.pendingGarbage;
14809
+ const defendedCount = Math.min(player.pendingCounteredGarbage, player.pendingGarbage);
14810
+ const normalCount = player.pendingGarbage - defendedCount;
14811
+ const cursor = isP1 ? p1GarbagePatternCursor : p2GarbagePatternCursor;
14812
+ const attackerPattern = isP1 ? p2Character.dropPattern : p1Character.dropPattern;
14813
+ const { dropState, nextCursor } = buildGarbageDropState(player, defendedCount, normalCount, cursor, attackerPattern);
14814
+ if (isP1) p1GarbagePatternCursor = nextCursor;
14815
+ else p2GarbagePatternCursor = nextCursor;
14816
+ player.pendingGarbage = 0;
14817
+ player.pendingCounteredGarbage = 0;
14818
+ const left = isP1 ? boardLeft1 : boardLeft2;
14819
+ floatingTexts.push({
14820
+ text: `DROP +${incomingCount}`,
14821
+ x: left + 2,
14822
+ y: Math.max(1, boardTop - 1),
14823
+ color: "\x1B[1;91m",
14824
+ frames: 20,
14825
+ maxFrames: 20
14826
+ });
14827
+ if (dropState) {
14828
+ if (isP1) {
14829
+ p1GarbageDrop = dropState;
14830
+ p1Phase = PHASE_GARBAGE;
14831
+ p1PhaseTimer = 0;
14832
+ } else {
14833
+ p2GarbageDrop = dropState;
14834
+ p2Phase = PHASE_GARBAGE;
14835
+ p2PhaseTimer = 0;
14836
+ }
14837
+ return;
14838
+ }
14839
+ }
14840
+ finalizePostResolution(player, isP1);
14841
+ }
14842
+ function update() {
14843
+ if (gameState !== "running") return;
14844
+ glitchFrame++;
14845
+ updateParticles2(particles);
14846
+ updateFloatingTexts(floatingTexts);
14847
+ updateProjectiles(projectiles);
14848
+ if (p1PoseTimer > 0) {
14849
+ p1PoseTimer--;
14850
+ if (p1PoseTimer <= 0) p1Pose = "idle";
14851
+ }
14852
+ if (p2PoseTimer > 0) {
14853
+ p2PoseTimer--;
14854
+ if (p2PoseTimer <= 0) p2Pose = "idle";
14855
+ }
14856
+ if (p1Phase !== PHASE_NONE) {
14857
+ tickResolution(true);
14858
+ }
14859
+ if (p2Phase !== PHASE_NONE) {
14860
+ tickResolution(false);
14861
+ }
14862
+ if (p1Phase === PHASE_NONE && p1.currentPair && p1.alive) {
14863
+ p1DropSpeed = getDropSpeed(p1);
14864
+ p1DropTimer++;
14865
+ if (p1DropTimer >= p1DropSpeed) {
14866
+ p1DropTimer = 0;
14867
+ if (!dropPair(p1.currentPair, p1.board)) {
14868
+ lockAndResolve(p1, true);
14869
+ }
14870
+ }
14871
+ }
14872
+ if (p2Phase === PHASE_NONE && p2.currentPair && p2.alive) {
14873
+ p2DropSpeed = Math.max(MIN_DROP_SPEED, Math.floor(getDropSpeed(p2) / aiState.difficulty.dropSpeedBoost));
14874
+ const action = aiTick(p2, aiState);
14875
+ switch (action) {
14876
+ case "rotate_cw":
14877
+ rotatePair(p2.currentPair, p2.board, true);
14878
+ break;
14879
+ case "rotate_ccw":
14880
+ rotatePair(p2.currentPair, p2.board, false);
14881
+ break;
14882
+ case "move": {
14883
+ const dir = getAIMoveDirection(p2, aiState);
14884
+ if (dir !== 0) movePair(p2.currentPair, p2.board, dir);
14885
+ break;
14886
+ }
14887
+ case "drop":
14888
+ if (!dropPair(p2.currentPair, p2.board)) {
14889
+ lockAndResolve(p2, false);
14890
+ }
14891
+ break;
14892
+ }
14893
+ p2DropTimer++;
14894
+ if (p2DropTimer >= p2DropSpeed) {
14895
+ p2DropTimer = 0;
14896
+ if (p2.currentPair && !dropPair(p2.currentPair, p2.board)) {
14897
+ lockAndResolve(p2, false);
14898
+ }
14899
+ }
14900
+ }
14901
+ }
14902
+ function lockAndResolve(player, isP1) {
14903
+ if (!player.currentPair) return;
14904
+ lockPair(player.currentPair, player.board);
14905
+ player.currentPair = null;
14906
+ applyGravityFull(player.board, player.powerGems);
14907
+ if (!isP1) {
14908
+ aiState.decided = false;
14909
+ aiState.thinkTimer = 0;
14910
+ aiState.moveTimer = 0;
14911
+ }
14912
+ startResolution(player, isP1);
14913
+ }
14914
+ function render() {
14915
+ if (!running) return;
14916
+ const cols = terminal.cols;
14917
+ const rows = terminal.rows;
14918
+ if (cols < MIN_COLS || rows < MIN_ROWS) {
14919
+ let output2 = "\x1B[2J\x1B[H";
14920
+ output2 += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor(cols / 2) - 10)}H`;
14921
+ output2 += `${themeColor}Need ${MIN_COLS}\xD7${MIN_ROWS} (have ${cols}\xD7${rows})\x1B[0m`;
14922
+ terminal.write(output2);
14923
+ return;
14924
+ }
14925
+ calculateLayout();
14926
+ let output = "\x1B[2J\x1B[H";
14927
+ switch (gameState) {
14928
+ case "difficulty":
14929
+ output += renderDifficultyScreen();
14930
+ break;
14931
+ case "characterSelect":
14932
+ output += renderCharacterSelectScreen();
14933
+ break;
14934
+ case "running":
14935
+ output += renderGame();
14936
+ break;
14937
+ case "paused":
14938
+ output += renderGame();
14939
+ output += renderPauseOverlay();
14940
+ break;
14941
+ case "gameOver":
14942
+ output += renderGame({
14943
+ showEffects: false,
14944
+ showHud: false,
14945
+ showControls: false,
14946
+ showVs: false
14947
+ });
14948
+ output += renderGameOverOverlay();
14949
+ break;
14950
+ }
14951
+ terminal.write(output);
14952
+ }
14953
+ function renderDifficultyScreen() {
14954
+ const cols = terminal.cols;
14955
+ const rows = terminal.rows;
14956
+ const centerX = Math.floor(cols / 2);
14957
+ let output = "";
14958
+ const titleY = Math.max(2, Math.floor(rows / 2) - 8);
14959
+ for (let i = 0; i < title.length; i++) {
14960
+ let line = title[i];
14961
+ if (Math.random() < 0.15) {
14962
+ const pos = Math.floor(Math.random() * line.length);
14963
+ const glitchChar = GLITCH_CHARS2[Math.floor(Math.random() * GLITCH_CHARS2.length)];
14964
+ line = line.substring(0, pos) + glitchChar + line.substring(pos + 1);
14965
+ }
14966
+ const x = Math.max(1, centerX - Math.floor(line.length / 2));
14967
+ const color = i < 2 ? themeColor : "\x1B[93m";
14968
+ output += `\x1B[${titleY + i};${x}H${color}\x1B[1m${line}\x1B[0m`;
14969
+ }
14970
+ const subtitle = "\u2554\u2550\u2550 GEM BATTLE VS AI \u2550\u2550\u2557";
14971
+ const subX = Math.max(1, centerX - Math.floor(subtitle.length / 2));
14972
+ output += `\x1B[${titleY + title.length + 1};${subX}H${themeColor}${subtitle}\x1B[0m`;
14973
+ const diffY = titleY + title.length + 4;
14974
+ const diffs = ["easy", "normal", "hard"];
14975
+ const diffLabels = ["EASY", "NORMAL", "HARD"];
14976
+ const diffDescs = ["Relaxed pace, AI makes mistakes", "Balanced challenge", "Fast & ruthless AI"];
14977
+ for (let i = 0; i < diffs.length; i++) {
14978
+ const isSelected = i === difficultySelection;
14979
+ const label = `[${i + 1}] ${diffLabels[i]}`;
14980
+ const text = isSelected ? `\u25BA ${label} \u25C4` : ` ${label} `;
14981
+ const style = isSelected ? "\x1B[1;93m" : `\x1B[2m${themeColor}`;
14982
+ const x = Math.max(1, centerX - Math.floor(text.length / 2));
14983
+ output += `\x1B[${diffY + i * 2};${x}H${style}${text}\x1B[0m`;
14984
+ if (isSelected) {
14985
+ const descX = Math.max(1, centerX - Math.floor(diffDescs[i].length / 2));
14986
+ output += `\x1B[${diffY + i * 2 + 1};${descX}H\x1B[2m${themeColor}${diffDescs[i]}\x1B[0m`;
14987
+ }
14988
+ }
14989
+ const controlsY = diffY + 8;
14990
+ const controls = "\u2191\u2193 Select Enter Confirm Q Quit";
14991
+ const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
14992
+ output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
14993
+ const refY = controlsY + 2;
14994
+ const refLines = [
14995
+ "\u2190\u2192/AD Move \u2191/W Rotate Z Counter-rotate",
14996
+ "\u2193/S Soft drop Space Hard drop ESC Pause"
14997
+ ];
14998
+ for (let i = 0; i < refLines.length; i++) {
14999
+ const rx = Math.max(1, centerX - Math.floor(refLines[i].length / 2));
15000
+ output += `\x1B[${refY + i};${rx}H\x1B[2m\x1B[90m${refLines[i]}\x1B[0m`;
15001
+ }
15002
+ return output;
15003
+ }
15004
+ const PATTERN_PREVIEW_COLORS = {
15005
+ red: "\x1B[1;38;5;196m",
15006
+ green: "\x1B[1;38;5;46m",
15007
+ blue: "\x1B[1;38;5;27m",
15008
+ yellow: "\x1B[1;38;5;226m"
15009
+ };
15010
+ function renderCharacterSelectScreen() {
15011
+ const cols = terminal.cols;
15012
+ const rows = terminal.rows;
15013
+ const centerX = Math.floor(cols / 2);
15014
+ let output = "";
15015
+ const titleY = Math.max(2, Math.floor(rows / 2) - 14);
15016
+ const selectTitle = "\u2554\u2550\u2550 SELECT YOUR FIGHTER \u2550\u2550\u2557";
15017
+ const stX = Math.max(1, centerX - Math.floor(selectTitle.length / 2));
15018
+ output += `\x1B[${titleY};${stX}H${themeColor}\x1B[1m${selectTitle}\x1B[0m`;
15019
+ const badge = `[${selectedDifficulty.name}]`;
15020
+ const bX = Math.max(1, centerX - Math.floor(badge.length / 2));
15021
+ output += `\x1B[${titleY + 1};${bX}H\x1B[2m${themeColor}${badge}\x1B[0m`;
15022
+ const gridY = titleY + 3;
15023
+ const cellW = 11;
15024
+ const selectedIdx = CHAR_GRID[charGridRow][charGridCol];
15025
+ for (let row = 0; row < CHAR_GRID.length; row++) {
15026
+ const rowChars = CHAR_GRID[row];
15027
+ const rowWidth = rowChars.length * cellW;
15028
+ const rowStartX = Math.max(1, centerX - Math.floor(rowWidth / 2));
15029
+ for (let col = 0; col < rowChars.length; col++) {
15030
+ const charIdx = rowChars[col];
15031
+ const char2 = CHARACTERS[charIdx];
15032
+ const isSelected = charIdx === selectedIdx;
15033
+ const x = rowStartX + col * cellW;
15034
+ const y = gridY + row * 3;
15035
+ const name = char2.name.slice(0, cellW - 2).padEnd(cellW - 2);
15036
+ if (isSelected) {
15037
+ output += `\x1B[${y};${x}H\x1B[1;93m\u25BA${name}\u25C4\x1B[0m`;
15038
+ } else {
15039
+ output += `\x1B[${y};${x}H${themeColor} ${name} \x1B[0m`;
15040
+ }
15041
+ const dmgStr = char2.damageModifier !== 1 ? `${Math.round(char2.damageModifier * 100)}%` : " ";
15042
+ const dmgColor = char2.damageModifier > 1 ? "\x1B[92m" : char2.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
15043
+ output += `\x1B[${y + 1};${x + 1}H${dmgColor}${dmgStr}\x1B[0m`;
15044
+ }
15045
+ }
15046
+ const char = CHARACTERS[selectedIdx];
15047
+ const detailY = gridY + CHAR_GRID.length * 3 + 1;
15048
+ const charTitle = `${char.name} \u2014 ${char.description}`;
15049
+ const ctX = Math.max(1, centerX - Math.floor(charTitle.length / 2));
15050
+ output += `\x1B[${detailY};${ctX}H\x1B[1;97m${charTitle}\x1B[0m`;
15051
+ const dmgLabel = char.damageModifier === 1 ? "Damage: 100% (standard)" : char.damageModifier > 1 ? `Damage: ${Math.round(char.damageModifier * 100)}% (bonus)` : `Damage: ${Math.round(char.damageModifier * 100)}% (reduced)`;
15052
+ const dmgLabelColor = char.damageModifier > 1 ? "\x1B[92m" : char.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
15053
+ const dlX = Math.max(1, centerX - Math.floor(dmgLabel.length / 2));
15054
+ output += `\x1B[${detailY + 1};${dlX}H${dmgLabelColor}${dmgLabel}\x1B[0m`;
15055
+ const previewWidth = 6 * 2;
15056
+ const previewX = Math.max(1, centerX - Math.floor(previewWidth / 2));
15057
+ const previewY = detailY + 3;
15058
+ output += `\x1B[${previewY - 1};${previewX}H\x1B[2m${themeColor}Drop Pattern:\x1B[0m`;
15059
+ for (let pr = 0; pr < char.dropPattern.length; pr++) {
15060
+ let rowStr = "";
15061
+ for (let pc = 0; pc < char.dropPattern[pr].length; pc++) {
15062
+ const color = char.dropPattern[pr][pc];
15063
+ rowStr += `${PATTERN_PREVIEW_COLORS[color]}\u2588\u2588\x1B[0m`;
15064
+ }
15065
+ output += `\x1B[${previewY + pr};${previewX}H${rowStr}`;
15066
+ }
15067
+ const portraitX = previewX + previewWidth + 3;
15068
+ const portraitLines = char.portraits.idle;
15069
+ output += renderPortrait(portraitLines, portraitX, previewY, themeColor);
15070
+ const controlsY = previewY + char.dropPattern.length + 2;
15071
+ const controls = "\u2190\u2192\u2191\u2193 Select Enter Confirm Esc Back";
15072
+ const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
15073
+ output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
15074
+ return output;
15075
+ }
15076
+ function renderGame(options) {
15077
+ const config = {
15078
+ showEffects: options?.showEffects ?? true,
15079
+ showHud: options?.showHud ?? true,
15080
+ showControls: options?.showControls ?? true,
15081
+ showVs: options?.showVs ?? true
15082
+ };
15083
+ let output = "";
15084
+ output += renderHeaderBar();
15085
+ const s1 = updateShake(p1Shake);
15086
+ const s2 = updateShake(p2Shake);
15087
+ output += renderBoard(p1, boardLeft1 + s1.dx, boardTop + s1.dy, true, true, p1Phase, p1PhaseTimer, p1ClearedCells, p1Flash);
15088
+ output += renderBoard(p2, boardLeft2 + s2.dx, boardTop + s2.dy, false, false, p2Phase, p2PhaseTimer, p2ClearedCells, p2Flash);
15089
+ if (config.showVs) {
15090
+ output += renderVSColumn();
15091
+ }
15092
+ if (showSidePanels && config.showHud) {
15093
+ output += renderSidePanel(true);
15094
+ output += renderSidePanel(false);
15095
+ }
15096
+ if (!showSidePanels) {
15097
+ output += `\x1B[${boardTop - 1};${boardLeft1 + 2}H${themeColor}${p1Character.name}\x1B[0m`;
15098
+ output += `\x1B[${boardTop - 1};${boardLeft2 + 2}H\x1B[91m${p2Character.name}\x1B[0m`;
15099
+ output += renderNextStrip(p1, boardLeft1, Math.max(1, boardTop - 2), themeColor);
15100
+ output += renderNextStrip(p2, boardLeft2, Math.max(1, boardTop - 2), "\x1B[91m");
15101
+ if (config.showHud) {
15102
+ const panelY = boardTop + boardDisplayHeight + 1;
15103
+ output += renderScorePanel(
15104
+ boardLeft1,
15105
+ panelY,
15106
+ p1.score,
15107
+ p1.pendingGarbage,
15108
+ themeColor,
15109
+ `S${Math.round(BASE_DROP_SPEED / Math.max(1, p1DropSpeed) * 100)}%`
15110
+ );
15111
+ output += renderScorePanel(
15112
+ boardLeft2,
15113
+ panelY,
15114
+ p2.score,
15115
+ p2.pendingGarbage,
15116
+ "\x1B[1;38;5;203m",
15117
+ "AI"
15118
+ );
15119
+ }
15120
+ }
15121
+ if (config.showEffects) {
15122
+ output += renderParticles(particles, 1, 1, terminal.cols, terminal.rows);
15123
+ output += renderFloatingTexts(floatingTexts);
15124
+ output += renderProjectiles(projectiles);
15125
+ }
15126
+ if (config.showControls) {
15127
+ output += renderFooterBar();
15128
+ }
15129
+ return output;
15130
+ }
15131
+ function renderNextStrip(player, left, y, accent) {
15132
+ if (!player.nextPair) return "";
15133
+ let output = "";
15134
+ output += `\x1B[${y};${left}H${accent}\x1B[1mNEXT\x1B[0m `;
15135
+ output += renderGemCell(player.nextPair.primary, false, false);
15136
+ output += renderGemCell(player.nextPair.secondary, false, false);
15137
+ output += ` \x1B[2m${accent}\u25CF\u25CF\u25CF\x1B[0m`;
15138
+ return output;
15139
+ }
15140
+ function renderScorePanel(left, y, score, pending, accent, tag) {
15141
+ const { levelLabel, levelColor } = getIncomingThreatLevel(pending);
15142
+ const meterWidth = 5;
15143
+ const filled = Math.min(meterWidth, pending);
15144
+ const meter = `${"\u25A0".repeat(filled)}${"\xB7".repeat(meterWidth - filled)}`;
15145
+ const scoreLine = `SCORE ${score.toString().padStart(6, "0")} ${tag}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
15146
+ const statusLine = `IN${pending.toString().padStart(2, "0")} ${meter} ${levelLabel}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
15147
+ let output = "";
15148
+ output += `\x1B[${y};${left}H${accent}\x1B[48;5;237m${scoreLine}\x1B[0m`;
15149
+ output += `\x1B[${y + 1};${left}H${levelColor}\x1B[48;5;235m${statusLine}\x1B[0m`;
15150
+ return output;
15151
+ }
15152
+ function getIncomingThreatLevel(pending) {
15153
+ if (pending <= 0) return { levelLabel: "CLEAR", levelColor: "\x1B[92m" };
15154
+ if (pending <= 2) return { levelLabel: "LOW", levelColor: "\x1B[93m" };
15155
+ if (pending <= 5) return { levelLabel: "HIGH", levelColor: "\x1B[91m" };
15156
+ return { levelLabel: "DANGER", levelColor: "\x1B[1;91m" };
15157
+ }
15158
+ function renderBoard(player, left, top, isP1Board, showGhost, phase, phaseTimer, clearedCells, flash) {
15159
+ let output = "";
15160
+ const board = player.board;
15161
+ const pair = player.currentPair;
15162
+ const flashColor = updateFlash(flash);
15163
+ const borderColor = flashColor || themeColor;
15164
+ output += `\x1B[${top};${left}H${borderColor}\u2554${"\u2550".repeat(BOARD_COLS * cellWidth)}\u2557\x1B[0m`;
15165
+ const clearedSet = new Set(clearedCells.map((c) => `${c.row},${c.col}`));
15166
+ let ghost = null;
15167
+ if (showGhost && pair) {
15168
+ ghost = getGhostPosition(pair, board);
15169
+ }
15170
+ for (let r = 0; r < BOARD_ROWS; r++) {
15171
+ let rowContent0 = "";
15172
+ let rowContentN = "";
15173
+ for (let c = 0; c < BOARD_COLS; c++) {
15174
+ const gem = board[r][c];
15175
+ const isCleared = clearedSet.has(`${r},${c}`);
15176
+ let pairGem = null;
15177
+ if (pair) {
15178
+ if (pair.row === r && pair.col === c) pairGem = pair.primary;
15179
+ const sec = getSecondaryPos(pair);
15180
+ if (sec.row === r && sec.col === c) pairGem = pair.secondary;
15181
+ }
15182
+ if (pairGem) {
15183
+ const cell = renderGemCell(pairGem, false, false);
15184
+ rowContent0 += cell;
15185
+ rowContentN += cell;
15186
+ } else if (isCleared && phase === PHASE_FLASH) {
15187
+ const cell = clearedCells.find((cc) => cc.row === r && cc.col === c);
15188
+ let s;
15189
+ if (cell && phaseTimer % 2 === 0) {
15190
+ s = `\x1B[1;97m${cellSolid}\x1B[0m`;
15191
+ } else {
15192
+ s = `${GEM_COLORS[cell?.color || "red"]}${cellSolid}\x1B[0m`;
15193
+ }
15194
+ rowContent0 += s;
15195
+ rowContentN += s;
15196
+ } else if (isCleared && phase === PHASE_DISSOLVE) {
15197
+ const stage = DISSOLVE_FRAMES - phaseTimer;
15198
+ let s;
15199
+ if (stage === 0) s = `\x1B[2m${cellPower}\x1B[0m`;
15200
+ else if (stage === 1) s = `\x1B[2m${cellGhost}\x1B[0m`;
15201
+ else s = cellEmpty;
15202
+ rowContent0 += s;
15203
+ rowContentN += s;
15204
+ } else if (gem) {
15205
+ const inPowerGem = gem.powerGemId !== void 0;
15206
+ const cell = renderGemCell(gem, inPowerGem, false);
15207
+ rowContent0 += cell;
15208
+ rowContentN += cell;
15209
+ } else {
15210
+ let isGhost = false;
15211
+ if (ghost) {
15212
+ if (ghost.primaryRow === r && ghost.primaryCol === c || ghost.secondaryRow === r && ghost.secondaryCol === c) {
15213
+ isGhost = true;
15214
+ }
15215
+ }
15216
+ if (isGhost) {
15217
+ const g = `\x1B[2;90m${cellGhost}\x1B[0m`;
15218
+ rowContent0 += g;
15219
+ rowContentN += g;
15220
+ } else {
15221
+ rowContent0 += `\x1B[38;5;238m${cellEmptyDot}\x1B[0m`;
15222
+ rowContentN += cellEmpty;
15223
+ }
15224
+ }
15225
+ }
15226
+ for (let h = 0; h < cellHeight; h++) {
15227
+ const rowY = top + 1 + r * cellHeight + h;
15228
+ output += `\x1B[${rowY};${left}H${borderColor}\u2551\x1B[0m`;
15229
+ output += h === 0 ? rowContent0 : rowContentN;
15230
+ output += `${borderColor}\u2551\x1B[0m`;
15231
+ }
15232
+ }
15233
+ output += `\x1B[${top + boardDisplayHeight - 1};${left}H${borderColor}\u255A${"\u2550".repeat(BOARD_COLS * cellWidth)}\u255D\x1B[0m`;
15234
+ const garbageDrop = isP1Board ? p1GarbageDrop : p2GarbageDrop;
15235
+ if (garbageDrop) {
15236
+ for (const g of garbageDrop.gems) {
15237
+ if (g.delayFrames > 0) continue;
15238
+ const row = Math.max(0, Math.min(BOARD_ROWS - 1, g.currentRow));
15239
+ const cellStr = renderGemCell({ color: g.color, type: "counter", counterTimer: g.timer }, false, false);
15240
+ for (let h = 0; h < cellHeight; h++) {
15241
+ const y = top + 1 + row * cellHeight + h;
15242
+ const x = left + 1 + g.col * cellWidth;
15243
+ output += `\x1B[${y};${x}H${cellStr}`;
15244
+ }
15245
+ }
15246
+ }
15247
+ return output;
15248
+ }
15249
+ function renderGemCell(gem, isPowerGem, _dimmed) {
15250
+ const color = GEM_COLORS[gem.color];
15251
+ switch (gem.type) {
15252
+ case "normal":
15253
+ if (isPowerGem) {
15254
+ return `${color}${cellPower}\x1B[0m`;
15255
+ }
15256
+ return `${color}${cellSolid}\x1B[0m`;
15257
+ case "crash":
15258
+ return `\x1B[1m${color}${cellCrash}\x1B[0m`;
15259
+ case "counter": {
15260
+ const bg = COUNTER_BG_COLORS[gem.color];
15261
+ if (gem.counterTimer !== void 0) {
15262
+ const timerStr = gem.counterTimer.toString().padStart(2, " ");
15263
+ return `\x1B[1;97m${bg}${timerStr.padEnd(cellWidth, " ")}\x1B[0m`;
15264
+ }
15265
+ return `\x1B[1;97m${bg}${" ?".padEnd(cellWidth, " ")}\x1B[0m`;
15266
+ }
15267
+ case "diamond":
15268
+ return `\x1B[1;97m${cellDiamond}\x1B[0m`;
15269
+ default:
15270
+ return `${color}${cellSolid}\x1B[0m`;
15271
+ }
15272
+ }
15273
+ function renderVSColumn() {
15274
+ let output = "";
15275
+ const x = vsColX;
15276
+ const y = boardTop + Math.max(2, Math.floor(boardDisplayHeight / 2) - 5);
15277
+ output += `\x1B[${Math.max(1, y - 2)};${x + 1}H\x1B[1;96mHYPER\x1B[0m`;
15278
+ output += `\x1B[${Math.max(1, y - 1)};${x + 1}H\x1B[1;95mFIGHT\x1B[0m`;
15279
+ output += `\x1B[${y};${x + 2}H\x1B[1;93mVS\x1B[0m`;
15280
+ const p1Lines = p1Character.portraits[p1Pose];
15281
+ output += renderPortrait(p1Lines, x, y + 2, themeColor);
15282
+ const p2Lines = p2Character.portraits[p2Pose];
15283
+ output += renderPortrait(p2Lines, x, y + 6, "\x1B[91m");
15284
+ const maxEnergy = 10;
15285
+ const p1Energy = p1ChainCount * 3 + p1TotalCleared;
15286
+ output += renderEnergyBar(x + 8, y + 2, p1Energy, maxEnergy);
15287
+ return output;
15288
+ }
15289
+ function renderHeaderBar() {
15290
+ const cols = terminal.cols;
15291
+ let output = "";
15292
+ const leftText = " HYPER FIGHTER";
15293
+ const rightText = `${p1Character.name} vs ${p2Character.name} [${selectedDifficulty.name}] `;
15294
+ const padLen = Math.max(0, cols - leftText.length - rightText.length);
15295
+ const row1 = leftText + " ".repeat(padLen) + rightText;
15296
+ output += `\x1B[1;1H\x1B[1;97m\x1B[48;5;236m${row1.slice(0, cols).padEnd(cols, " ")}\x1B[0m`;
15297
+ output += `\x1B[2;1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
15298
+ return output;
15299
+ }
15300
+ function renderFooterBar() {
15301
+ const cols = terminal.cols;
15302
+ const rows = terminal.rows;
15303
+ let output = "";
15304
+ output += `\x1B[${rows - 1};1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
15305
+ const controls = "\u2190\u2192 Move \u2191/W Rotate Z CCW \u2193/S Soft Space Drop ESC Pause";
15306
+ const cx = Math.max(1, Math.floor((cols - controls.length) / 2) + 1);
15307
+ output += `\x1B[${rows};${cx}H\x1B[2m\x1B[90m${controls}\x1B[0m`;
15308
+ return output;
15309
+ }
15310
+ function renderMiniBar(value, max, width, fillColor) {
15311
+ const filled = Math.min(width, Math.round(value / Math.max(1, max) * width));
15312
+ const empty = width - filled;
15313
+ return `${fillColor}${"\u25A0".repeat(filled)}\x1B[38;5;238m${"\xB7".repeat(empty)}\x1B[0m`;
15314
+ }
15315
+ function renderSidePanel(isLeft) {
15316
+ const player = isLeft ? p1 : p2;
15317
+ const x = isLeft ? sidePanel1X : sidePanel2X;
15318
+ const accent = isLeft ? themeColor : "\x1B[1;38;5;203m";
15319
+ const dropSpeed = isLeft ? p1DropSpeed : p2DropSpeed;
15320
+ const chainCount = isLeft ? p1ChainCount : p2ChainCount;
15321
+ let output = "";
15322
+ let y = boardTop + 1;
15323
+ output += `\x1B[${y};${x}H${accent}\x1B[1mNEXT\x1B[0m`;
15324
+ y++;
15325
+ if (player.nextPair) {
15326
+ output += `\x1B[${y};${x}H`;
15327
+ output += renderGemCell(player.nextPair.primary, false, false);
15328
+ output += renderGemCell(player.nextPair.secondary, false, false);
15329
+ }
15330
+ y += 2;
15331
+ output += `\x1B[${y};${x}H\x1B[38;5;245mSCORE\x1B[0m`;
15332
+ y++;
15333
+ output += `\x1B[${y};${x}H${accent}${player.score.toString().padStart(7, "0")}\x1B[0m`;
15334
+ y += 2;
15335
+ output += `\x1B[${y};${x}H\x1B[38;5;245mSPEED\x1B[0m`;
15336
+ y++;
15337
+ const speedPct = Math.round(BASE_DROP_SPEED / Math.max(1, dropSpeed) * 100);
15338
+ output += `\x1B[${y};${x}H${speedPct >= 200 ? "\x1B[91m" : speedPct >= 150 ? "\x1B[93m" : "\x1B[92m"}${speedPct}%\x1B[0m `;
15339
+ output += renderMiniBar(speedPct, 300, 6, speedPct >= 200 ? "\x1B[91m" : "\x1B[92m");
15340
+ y += 2;
15341
+ if (chainCount > 0) {
15342
+ output += `\x1B[${y};${x}H\x1B[38;5;245mCHAIN\x1B[0m`;
15343
+ y++;
15344
+ output += `\x1B[${y};${x}H\x1B[1;93m${chainCount}\x1B[0m`;
15345
+ y += 2;
15346
+ } else {
15347
+ y += 3;
15348
+ }
15349
+ const { levelLabel, levelColor } = getIncomingThreatLevel(player.pendingGarbage);
15350
+ output += `\x1B[${y};${x}H\x1B[38;5;245mINCOMING\x1B[0m`;
15351
+ y++;
15352
+ output += `\x1B[${y};${x}H${levelColor}${player.pendingGarbage.toString().padStart(2, "0")}\x1B[0m `;
15353
+ output += renderMiniBar(player.pendingGarbage, 10, 6, levelColor);
15354
+ y++;
15355
+ output += `\x1B[${y};${x}H${levelColor}${levelLabel}\x1B[0m`;
15356
+ return output;
15357
+ }
15358
+ function renderPauseOverlay() {
15359
+ const cols = terminal.cols;
15360
+ const rows = terminal.rows;
15361
+ const centerX = Math.floor(cols / 2);
15362
+ const centerY = Math.floor(rows / 2);
15363
+ let output = "";
15364
+ output += `\x1B[${centerY - 3};${centerX - 4}H\x1B[1;5m${themeColor}\u23F8 PAUSED\x1B[0m`;
15365
+ output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
15366
+ centerX,
15367
+ startY: centerY - 1,
15368
+ showShortcuts: true
15369
+ });
15370
+ return output;
15371
+ }
15372
+ function renderGameOverOverlay() {
15373
+ const cols = terminal.cols;
15374
+ const rows = terminal.rows;
15375
+ const centerX = Math.floor(cols / 2);
15376
+ const centerY = Math.floor(rows / 2);
15377
+ let output = "";
15378
+ gameOverTimer++;
15379
+ const panelWidth = Math.min(44, Math.max(34, cols - 8));
15380
+ const panelHeight = 12;
15381
+ const panelLeft = Math.max(2, centerX - Math.floor(panelWidth / 2));
15382
+ const panelTop = Math.max(2, centerY - Math.floor(panelHeight / 2));
15383
+ for (let y = 0; y < panelHeight; y++) {
15384
+ output += `\x1B[${panelTop + y};${panelLeft}H\x1B[40m${" ".repeat(panelWidth)}\x1B[0m`;
15385
+ }
15386
+ output += `\x1B[${panelTop};${panelLeft}H${themeColor}\u2554${"\u2550".repeat(panelWidth - 2)}\u2557\x1B[0m`;
15387
+ for (let y = 1; y < panelHeight - 1; y++) {
15388
+ output += `\x1B[${panelTop + y};${panelLeft}H${themeColor}\u2551\x1B[0m`;
15389
+ output += `\x1B[${panelTop + y};${panelLeft + panelWidth - 1}H${themeColor}\u2551\x1B[0m`;
15390
+ }
15391
+ output += `\x1B[${panelTop + panelHeight - 1};${panelLeft}H${themeColor}\u255A${"\u2550".repeat(panelWidth - 2)}\u255D\x1B[0m`;
15392
+ const winnerChar = winner === 1 ? p1Character : p2Character;
15393
+ const winText = winner === 1 ? `${winnerChar.name} WINS!` : `${winnerChar.name} WINS!`;
15394
+ const winColor = winner === 1 ? "\x1B[1;92m" : "\x1B[1;91m";
15395
+ const winX = Math.max(1, centerX - Math.floor(winText.length / 2));
15396
+ output += `\x1B[${panelTop + 2};${winX}H${winColor}${winText}\x1B[0m`;
15397
+ const scoreLine = `${p1Character.name}: ${p1.score} | ${p2Character.name}: ${p2.score}`;
15398
+ const sX = Math.max(1, centerX - Math.floor(scoreLine.length / 2));
15399
+ output += `\x1B[${panelTop + 4};${sX}H${themeColor}${scoreLine}\x1B[0m`;
15400
+ const menuItems = [
15401
+ { label: "RESTART", shortcut: "R" },
15402
+ { label: "QUIT", shortcut: "Q" },
15403
+ { label: "LIST GAMES", shortcut: "L" },
15404
+ { label: "NEXT GAME", shortcut: "N" }
15405
+ ];
15406
+ output += renderSimpleMenu(menuItems, pauseMenuSelection, {
15407
+ centerX,
15408
+ startY: panelTop + 6,
15409
+ showShortcuts: true
15410
+ });
15411
+ return output;
15412
+ }
15413
+ const keyListener = terminal.onKey(({ domEvent }) => {
15414
+ if (!running) return;
15415
+ domEvent.preventDefault();
15416
+ domEvent.stopPropagation();
15417
+ const key = domEvent.key.toLowerCase();
15418
+ switch (gameState) {
15419
+ case "difficulty":
15420
+ handleDifficultyInput(key, domEvent);
15421
+ break;
15422
+ case "characterSelect":
15423
+ handleCharacterSelectInput(key, domEvent);
15424
+ break;
15425
+ case "running":
15426
+ handleGameInput(key, domEvent);
15427
+ break;
15428
+ case "paused":
15429
+ handlePauseInput(key, domEvent);
15430
+ break;
15431
+ case "gameOver":
15432
+ handleGameOverInput(key, domEvent);
15433
+ break;
15434
+ }
15435
+ });
15436
+ function handleDifficultyInput(key, domEvent) {
15437
+ if (domEvent.key === "ArrowUp" || key === "w") {
15438
+ difficultySelection = (difficultySelection - 1 + 3) % 3;
15439
+ } else if (domEvent.key === "ArrowDown" || key === "s") {
15440
+ difficultySelection = (difficultySelection + 1) % 3;
15441
+ } else if (domEvent.key === "Enter" || domEvent.key === " ") {
15442
+ const diffs = ["easy", "normal", "hard"];
15443
+ selectedDifficulty = DIFFICULTIES2[diffs[difficultySelection]];
15444
+ enterCharacterSelect();
15445
+ } else if (key === "1") {
15446
+ selectedDifficulty = DIFFICULTIES2.easy;
15447
+ difficultySelection = 0;
15448
+ enterCharacterSelect();
15449
+ } else if (key === "2") {
15450
+ selectedDifficulty = DIFFICULTIES2.normal;
15451
+ difficultySelection = 1;
15452
+ enterCharacterSelect();
15453
+ } else if (key === "3") {
15454
+ selectedDifficulty = DIFFICULTIES2.hard;
15455
+ difficultySelection = 2;
15456
+ enterCharacterSelect();
15457
+ } else if (key === "q") {
15458
+ cleanup();
15459
+ dispatchGameQuit(terminal);
15460
+ }
15461
+ }
15462
+ function enterCharacterSelect() {
15463
+ gameState = "characterSelect";
15464
+ charGridRow = 0;
15465
+ charGridCol = 0;
15466
+ }
15467
+ function handleCharacterSelectInput(key, domEvent) {
15468
+ if (key === "q" || key === "escape") {
15469
+ gameState = "difficulty";
15470
+ return;
15471
+ }
15472
+ if (domEvent.key === "ArrowLeft" || key === "a") {
15473
+ const row = CHAR_GRID[charGridRow];
15474
+ charGridCol = (charGridCol - 1 + row.length) % row.length;
15475
+ } else if (domEvent.key === "ArrowRight" || key === "d") {
15476
+ const row = CHAR_GRID[charGridRow];
15477
+ charGridCol = (charGridCol + 1) % row.length;
15478
+ } else if (domEvent.key === "ArrowUp" || key === "w") {
15479
+ charGridRow = (charGridRow - 1 + CHAR_GRID.length) % CHAR_GRID.length;
15480
+ charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
15481
+ } else if (domEvent.key === "ArrowDown" || key === "s") {
15482
+ charGridRow = (charGridRow + 1) % CHAR_GRID.length;
15483
+ charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
15484
+ } else if (domEvent.key === "Enter" || domEvent.key === " ") {
15485
+ const idx = CHAR_GRID[charGridRow][charGridCol];
15486
+ p1Character = CHARACTERS[idx];
15487
+ p2Character = getRandomCharacter();
15488
+ gameState = "running";
15489
+ initGame();
15490
+ }
15491
+ }
15492
+ function handleGameInput(key, domEvent) {
15493
+ if (key === "escape") {
15494
+ gameState = "paused";
15495
+ pauseMenuSelection = 0;
15496
+ return;
15497
+ }
15498
+ if (!p1.currentPair || p1Phase !== PHASE_NONE) return;
15499
+ switch (domEvent.key) {
15500
+ case "ArrowLeft":
15501
+ case "a":
15502
+ movePair(p1.currentPair, p1.board, -1);
15503
+ break;
15504
+ case "ArrowRight":
15505
+ case "d":
15506
+ movePair(p1.currentPair, p1.board, 1);
15507
+ break;
15508
+ case "ArrowUp":
15509
+ case "w":
15510
+ rotatePair(p1.currentPair, p1.board, true);
15511
+ break;
15512
+ case "z":
15513
+ rotatePair(p1.currentPair, p1.board, false);
15514
+ break;
15515
+ case "ArrowDown":
15516
+ case "s":
15517
+ if (dropPair(p1.currentPair, p1.board)) {
15518
+ p1DropTimer = 0;
15519
+ } else {
15520
+ lockAndResolve(p1, true);
15521
+ }
15522
+ break;
15523
+ case " ":
15524
+ hardDrop(p1.currentPair, p1.board);
15525
+ lockAndResolve(p1, true);
15526
+ break;
15527
+ }
15528
+ }
15529
+ function handlePauseInput(key, domEvent) {
15530
+ if (key === "escape") {
15531
+ gameState = "running";
15532
+ return;
15533
+ }
15534
+ const { newSelection, confirmed } = navigateMenu(
15535
+ pauseMenuSelection,
15536
+ PAUSE_MENU_ITEMS.length,
15537
+ key,
15538
+ domEvent
15539
+ );
15540
+ if (newSelection !== pauseMenuSelection) {
15541
+ pauseMenuSelection = newSelection;
15542
+ }
15543
+ const shortcutIdx = checkShortcut(PAUSE_MENU_ITEMS, key);
15544
+ if (confirmed || shortcutIdx >= 0) {
15545
+ const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
15546
+ const item = PAUSE_MENU_ITEMS[idx];
15547
+ switch (item.label) {
15548
+ case "RESUME":
15549
+ gameState = "running";
15550
+ break;
15551
+ case "RESTART":
15552
+ gameState = "running";
15553
+ initGame();
15554
+ break;
15555
+ case "QUIT":
15556
+ cleanup();
15557
+ dispatchGameQuit(terminal);
15558
+ break;
15559
+ case "LIST GAMES":
15560
+ cleanup();
15561
+ dispatchGamesMenu(terminal);
15562
+ break;
15563
+ case "NEXT GAME":
15564
+ cleanup();
15565
+ dispatchGameSwitch(terminal);
15566
+ break;
15567
+ }
15568
+ }
15569
+ }
15570
+ function handleGameOverInput(key, domEvent) {
15571
+ const menuItems = [
15572
+ { label: "RESTART", shortcut: "R" },
15573
+ { label: "QUIT", shortcut: "Q" },
15574
+ { label: "LIST GAMES", shortcut: "L" },
15575
+ { label: "NEXT GAME", shortcut: "N" }
15576
+ ];
15577
+ const { newSelection, confirmed } = navigateMenu(
15578
+ pauseMenuSelection,
15579
+ menuItems.length,
15580
+ key,
15581
+ domEvent
15582
+ );
15583
+ if (newSelection !== pauseMenuSelection) {
15584
+ pauseMenuSelection = newSelection;
15585
+ }
15586
+ const shortcutIdx = checkShortcut(menuItems, key);
15587
+ if (confirmed || shortcutIdx >= 0) {
15588
+ const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
15589
+ switch (menuItems[idx].label) {
15590
+ case "RESTART":
15591
+ gameState = "difficulty";
15592
+ pauseMenuSelection = 0;
15593
+ break;
15594
+ case "QUIT":
15595
+ cleanup();
15596
+ dispatchGameQuit(terminal);
15597
+ break;
15598
+ case "LIST GAMES":
15599
+ cleanup();
15600
+ dispatchGamesMenu(terminal);
15601
+ break;
15602
+ case "NEXT GAME":
15603
+ cleanup();
15604
+ dispatchGameSwitch(terminal);
15605
+ break;
15606
+ }
15607
+ }
15608
+ }
15609
+ const resizeListener = terminal.onResize(() => {
15610
+ calculateLayout();
15611
+ });
15612
+ function cleanup() {
15613
+ running = false;
15614
+ clearInterval(gameLoop);
15615
+ keyListener.dispose();
15616
+ resizeListener.dispose();
15617
+ }
15618
+ enterAlternateBuffer(terminal, "hyper-fighter");
15619
+ const gameLoop = setInterval(() => {
15620
+ if (!running) {
15621
+ clearInterval(gameLoop);
15622
+ return;
15623
+ }
15624
+ update();
15625
+ render();
15626
+ }, TICK_MS);
15627
+ const originalStop = controller.stop;
15628
+ controller.stop = () => {
15629
+ cleanup();
15630
+ exitAlternateBuffer(terminal, "hyper-fighter");
15631
+ originalStop();
15632
+ };
15633
+ return controller;
15634
+ }
15635
+
13141
15636
  // src/games/gamesMenu.ts
13142
15637
  function showGamesMenu(terminal, optionsOrCallback) {
13143
15638
  const options = typeof optionsOrCallback === "function" ? { onGameSelect: optionsOrCallback } : optionsOrCallback || {};
@@ -13157,8 +15652,8 @@ function showGamesMenu(terminal, optionsOrCallback) {
13157
15652
  }
13158
15653
  };
13159
15654
  const title = [
13160
- "\u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2580\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580",
13161
- "\u2588\u2584\u2588 \u2588\u2580\u2588 \u2588 \u2580 \u2588 \u2588\u2588\u2584 \u2584\u2588"
15655
+ "\u2588 \u2588 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2580\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580",
15656
+ "\u2588\u2580\u2588 \u2588 \u2588\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588 \u2580 \u2588 \u2588\u2588\u2584 \u2584\u2588"
13162
15657
  ];
13163
15658
  function render() {
13164
15659
  let output = "";
@@ -13425,7 +15920,8 @@ var games = [
13425
15920
  { id: "typingtest", name: "Typing Test", description: "Test your speed", run: runTypingTest },
13426
15921
  { id: "tron", name: "Tron", description: "Light cycle battle", run: runTronGame },
13427
15922
  { id: "crack", name: "Crack", description: "Hack the system", run: runCrackGame },
13428
- { id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame }
15923
+ { id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame },
15924
+ { id: "hyper-fighter", name: "Hyper Fighter", description: "Gem battle vs AI", run: runHyperFighterGame }
13429
15925
  ];
13430
15926
 
13431
15927
  // src/cli.ts