@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/index.js CHANGED
@@ -623,8 +623,8 @@ var TILE_BG_COLORS = {
623
623
  };
624
624
  function run2048Game(terminal) {
625
625
  const themeColor = getCurrentThemeColor();
626
- const MIN_COLS = 36;
627
- const MIN_ROWS = 16;
626
+ const MIN_COLS2 = 36;
627
+ const MIN_ROWS2 = 16;
628
628
  const GRID_SIZE = 4;
629
629
  const TILE_WIDTH = 8;
630
630
  const TILE_HEIGHT = 3;
@@ -688,7 +688,7 @@ function run2048Game(terminal) {
688
688
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
689
689
  scorePopups.push({ x, y, text, frames: 20, color });
690
690
  }
691
- function triggerShake2(frames, intensity) {
691
+ function triggerShake3(frames, intensity) {
692
692
  shakeFrames = frames;
693
693
  shakeIntensity = intensity;
694
694
  }
@@ -812,13 +812,13 @@ function run2048Game(terminal) {
812
812
  if (anyMoved) {
813
813
  score += totalMergeScore;
814
814
  if (totalMergeScore > 0) {
815
- triggerShake2(4, 1);
815
+ triggerShake3(4, 1);
816
816
  }
817
817
  spawnTile();
818
818
  if (!won && !continuedAfterWin && has2048()) {
819
819
  won = true;
820
820
  if (score > highScore) highScore = score;
821
- triggerShake2(8, 2);
821
+ triggerShake3(8, 2);
822
822
  for (let y = 0; y < GRID_SIZE; y++) {
823
823
  for (let x = 0; x < GRID_SIZE; x++) {
824
824
  if (grid[y][x] === 2048) {
@@ -904,12 +904,12 @@ function run2048Game(terminal) {
904
904
  if (shakeFrames > 0) shakeFrames--;
905
905
  const cols = terminal.cols;
906
906
  const rows = terminal.rows;
907
- if (cols < MIN_COLS || rows < MIN_ROWS) {
907
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
908
908
  const msg1 = "Terminal too small!";
909
- const needWidth = cols < MIN_COLS;
910
- const needHeight = rows < MIN_ROWS;
909
+ const needWidth = cols < MIN_COLS2;
910
+ const needHeight = rows < MIN_ROWS2;
911
911
  let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider ->" : "Make pane taller v";
912
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
912
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
913
913
  const centerX = Math.floor(cols / 2);
914
914
  const centerY = Math.floor(rows / 2);
915
915
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -1221,8 +1221,8 @@ function run2048Game(terminal) {
1221
1221
  // src/games/asteroids/index.ts
1222
1222
  function runAsteroidsGame(terminal) {
1223
1223
  const themeColor = getCurrentThemeColor();
1224
- const MIN_COLS = 35;
1225
- const MIN_ROWS = 16;
1224
+ const MIN_COLS2 = 35;
1225
+ const MIN_ROWS2 = 16;
1226
1226
  let GAME_WIDTH = 30;
1227
1227
  let GAME_HEIGHT = 12;
1228
1228
  const SHIP_TURN_SPEED = 0.2;
@@ -1320,7 +1320,7 @@ function runAsteroidsGame(terminal) {
1320
1320
  if (value >= max) return value - max;
1321
1321
  return value;
1322
1322
  }
1323
- function triggerShake2(frames, intensity) {
1323
+ function triggerShake3(frames, intensity) {
1324
1324
  shakeFrames = frames;
1325
1325
  shakeIntensity = intensity;
1326
1326
  }
@@ -1357,7 +1357,7 @@ function runAsteroidsGame(terminal) {
1357
1357
  if (invincibilityFrames > 0) return;
1358
1358
  lives--;
1359
1359
  spawnParticles3(ship.x, ship.y, 15, "\x1B[91m");
1360
- triggerShake2(10, 3);
1360
+ triggerShake3(10, 3);
1361
1361
  if (lives <= 0) {
1362
1362
  gameOver = true;
1363
1363
  if (score > highScore) highScore = score;
@@ -1438,7 +1438,7 @@ function runAsteroidsGame(terminal) {
1438
1438
  bullets.splice(j, 1);
1439
1439
  splitAsteroid(ast);
1440
1440
  asteroids.splice(i, 1);
1441
- triggerShake2(4, 1);
1441
+ triggerShake3(4, 1);
1442
1442
  break;
1443
1443
  }
1444
1444
  }
@@ -1468,9 +1468,9 @@ function runAsteroidsGame(terminal) {
1468
1468
  if (shakeFrames > 0) shakeFrames--;
1469
1469
  const cols = terminal.cols;
1470
1470
  const rows = terminal.rows;
1471
- if (cols < MIN_COLS || rows < MIN_ROWS) {
1471
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
1472
1472
  const msg = "Terminal too small!";
1473
- const need = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
1473
+ const need = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
1474
1474
  output += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor((cols - msg.length) / 2))}H${themeColor}${msg}\x1B[0m`;
1475
1475
  output += `\x1B[${Math.floor(rows / 2) + 2};${Math.max(1, Math.floor((cols - need.length) / 2))}H\x1B[2m${need}\x1B[0m`;
1476
1476
  terminal.write(output);
@@ -1661,8 +1661,8 @@ function runAsteroidsGame(terminal) {
1661
1661
  // src/games/breakout/index.ts
1662
1662
  function runBreakoutGame(terminal) {
1663
1663
  const themeColor = getCurrentThemeColor();
1664
- const MIN_COLS = 40;
1665
- const MIN_ROWS = 18;
1664
+ const MIN_COLS2 = 40;
1665
+ const MIN_ROWS2 = 18;
1666
1666
  const GAME_WIDTH = 46;
1667
1667
  const GAME_HEIGHT = 20;
1668
1668
  const PADDLE_WIDTH_NORMAL = 7;
@@ -1731,7 +1731,7 @@ function runBreakoutGame(terminal) {
1731
1731
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
1732
1732
  scorePopups.push({ x, y, text, frames: 18, color });
1733
1733
  }
1734
- function triggerShake2(frames, intensity) {
1734
+ function triggerShake3(frames, intensity) {
1735
1735
  shakeFrames = frames;
1736
1736
  shakeIntensity = intensity;
1737
1737
  }
@@ -1788,7 +1788,7 @@ function runBreakoutGame(terminal) {
1788
1788
  powerUps.push({ x, y, type: types[Math.floor(Math.random() * types.length)], vy: 0.15 });
1789
1789
  }
1790
1790
  function applyPowerUp(type) {
1791
- triggerShake2(8, 2);
1791
+ triggerShake3(8, 2);
1792
1792
  borderFlash = 15;
1793
1793
  switch (type) {
1794
1794
  case "multiball": {
@@ -1964,7 +1964,7 @@ function runBreakoutGame(terminal) {
1964
1964
  const totalPts = basePts + comboBonus;
1965
1965
  score += totalPts;
1966
1966
  const intensity = Math.min(comboCount, 8);
1967
- triggerShake2(3 + intensity, 1 + Math.floor(intensity / 3));
1967
+ triggerShake3(3 + intensity, 1 + Math.floor(intensity / 3));
1968
1968
  spawnParticles3(brick.x + brick.width / 2, brick.y, 6 + intensity, brickColors[brick.type]);
1969
1969
  addScorePopup2(brick.x + 1, brick.y - 1, comboCount > 1 ? `+${totalPts}!` : `+${totalPts}`, brickColors[brick.type]);
1970
1970
  spawnPowerUp(brick.x + brick.width / 2, brick.y);
@@ -1982,12 +1982,12 @@ function runBreakoutGame(terminal) {
1982
1982
  if (lives <= 0) {
1983
1983
  gameOver = true;
1984
1984
  if (score > highScore) highScore = score;
1985
- triggerShake2(20, 4);
1985
+ triggerShake3(20, 4);
1986
1986
  spawnParticles3(paddleX, GAME_HEIGHT - 2, 15, "\x1B[1;91m", ["\u2717", "\u2620", "\xD7", "\u2593"]);
1987
1987
  } else {
1988
1988
  ballAttached = true;
1989
1989
  balls = [{ x: paddleX, y: GAME_HEIGHT - 3, vx: 0, vy: 0, active: true }];
1990
- triggerShake2(10, 2);
1990
+ triggerShake3(10, 2);
1991
1991
  }
1992
1992
  }
1993
1993
  const aliveBricks = bricks.filter((b) => b.alive);
@@ -1996,7 +1996,7 @@ function runBreakoutGame(terminal) {
1996
1996
  gameOver = true;
1997
1997
  level++;
1998
1998
  if (score > highScore) highScore = score;
1999
- triggerShake2(12, 2);
1999
+ triggerShake3(12, 2);
2000
2000
  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);
2001
2001
  }
2002
2002
  }
@@ -2005,11 +2005,11 @@ function runBreakoutGame(terminal) {
2005
2005
  if (shakeFrames > 0) shakeFrames--;
2006
2006
  if (borderFlash > 0) borderFlash--;
2007
2007
  const cols = terminal.cols, rows = terminal.rows;
2008
- if (cols < MIN_COLS || rows < MIN_ROWS) {
2008
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
2009
2009
  const msg1 = "Terminal too small!";
2010
- const needW = cols < MIN_COLS, needH = rows < MIN_ROWS;
2010
+ const needW = cols < MIN_COLS2, needH = rows < MIN_ROWS2;
2011
2011
  const hint2 = needW && needH ? "Make pane larger" : needW ? "Make pane wider \u2192" : "Make pane taller \u2193";
2012
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
2012
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
2013
2013
  const cX = Math.floor(cols / 2), cY = Math.floor(rows / 2);
2014
2014
  output += `\x1B[${cY - 1};${Math.max(1, cX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
2015
2015
  output += `\x1B[${cY + 1};${Math.max(1, cX - Math.floor(msg2.length / 2))}H\x1B[2m${msg2}\x1B[0m`;
@@ -2571,8 +2571,8 @@ var ROPE_RETRACT_SPEED = 0.15;
2571
2571
  var ROPE_FAST_RETRACT_SPEED = 0.3;
2572
2572
  function runCourierGame(terminal) {
2573
2573
  const themeColor = getCurrentThemeColor();
2574
- const MIN_COLS = 40;
2575
- const MIN_ROWS = 16;
2574
+ const MIN_COLS2 = 40;
2575
+ const MIN_ROWS2 = 16;
2576
2576
  let running = true;
2577
2577
  let gameStarted = false;
2578
2578
  let gameOver = false;
@@ -3201,8 +3201,8 @@ function runCourierGame(terminal) {
3201
3201
  output += "\x1B[2J\x1B[H";
3202
3202
  const cols = terminal.cols;
3203
3203
  const rows = terminal.rows;
3204
- if (cols < MIN_COLS || rows < MIN_ROWS) {
3205
- const msg = `Need ${MIN_COLS}\xD7${MIN_ROWS}, have ${cols}\xD7${rows}`;
3204
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
3205
+ const msg = `Need ${MIN_COLS2}\xD7${MIN_ROWS2}, have ${cols}\xD7${rows}`;
3206
3206
  const centerX = Math.floor(cols / 2);
3207
3207
  const centerY = Math.floor(rows / 2);
3208
3208
  output += `\x1B[${centerY};${Math.max(1, centerX - msg.length / 2)}H${themeColor}${msg}\x1B[0m`;
@@ -3961,8 +3961,8 @@ var LOG_MESSAGES = {
3961
3961
  };
3962
3962
  function runCrackGame(terminal) {
3963
3963
  const themeColor = getCurrentThemeColor();
3964
- const MIN_COLS = 40;
3965
- const MIN_ROWS = 16;
3964
+ const MIN_COLS2 = 40;
3965
+ const MIN_ROWS2 = 16;
3966
3966
  let cols = terminal.cols;
3967
3967
  let rows = terminal.rows;
3968
3968
  const updateDimensions = () => {
@@ -4174,9 +4174,9 @@ function runCrackGame(terminal) {
4174
4174
  function render() {
4175
4175
  let output = "";
4176
4176
  output += "\x1B[2J\x1B[H";
4177
- if (cols < MIN_COLS || rows < MIN_ROWS) {
4177
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
4178
4178
  const msg1 = "Terminal too small!";
4179
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
4179
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
4180
4180
  const centerX = Math.floor(cols / 2);
4181
4181
  const centerY = Math.floor(rows / 2);
4182
4182
  output += `\x1B[${centerY};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -4629,8 +4629,8 @@ function runCrackGame(terminal) {
4629
4629
  // src/games/frogger/index.ts
4630
4630
  function runFroggerGame(terminal) {
4631
4631
  const themeColor = getCurrentThemeColor();
4632
- const MIN_COLS = 40;
4633
- const MIN_ROWS = 16;
4632
+ const MIN_COLS2 = 40;
4633
+ const MIN_ROWS2 = 16;
4634
4634
  const LILY_PAD_COUNT = 5;
4635
4635
  const TIME_LIMIT = 45;
4636
4636
  const MOVE_COOLDOWN = 100;
@@ -5015,10 +5015,10 @@ function runFroggerGame(terminal) {
5015
5015
  output += "\x1B[2J\x1B[H";
5016
5016
  const cols = terminal.cols;
5017
5017
  const rows = terminal.rows;
5018
- if (cols < MIN_COLS || rows < MIN_ROWS) {
5018
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
5019
5019
  const msg1 = "Terminal too small!";
5020
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
5021
- const hint2 = cols < MIN_COLS && rows < MIN_ROWS ? "Make pane larger" : cols < MIN_COLS ? "Make pane wider \u2192" : "Make pane taller \u2193";
5020
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
5021
+ const hint2 = cols < MIN_COLS2 && rows < MIN_ROWS2 ? "Make pane larger" : cols < MIN_COLS2 ? "Make pane wider \u2192" : "Make pane taller \u2193";
5022
5022
  const cx = Math.floor(cols / 2);
5023
5023
  const cy = Math.floor(rows / 2);
5024
5024
  output += `\x1B[${cy - 1};${Math.max(1, cx - msg1.length / 2)}H${themeColor}${msg1}\x1B[0m`;
@@ -5456,8 +5456,8 @@ var HANGMAN_STAGES = [
5456
5456
  function runHangmanGame(terminal) {
5457
5457
  const themeColor = getCurrentThemeColor();
5458
5458
  const GAME_WIDTH = 40;
5459
- const MIN_COLS = 32;
5460
- const MIN_ROWS = 16;
5459
+ const MIN_COLS2 = 32;
5460
+ const MIN_ROWS2 = 16;
5461
5461
  let running = true;
5462
5462
  let gameStarted = false;
5463
5463
  let gameOver = false;
@@ -5621,10 +5621,10 @@ function runHangmanGame(terminal) {
5621
5621
  }
5622
5622
  const cols = terminal.cols;
5623
5623
  const rows = terminal.rows;
5624
- if (cols < MIN_COLS || rows < MIN_ROWS) {
5624
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
5625
5625
  const msg1 = "Terminal too small!";
5626
- const needWidth = cols < MIN_COLS;
5627
- const needHeight = rows < MIN_ROWS;
5626
+ const needWidth = cols < MIN_COLS2;
5627
+ const needHeight = rows < MIN_ROWS2;
5628
5628
  let hint2 = "";
5629
5629
  if (needWidth && needHeight) {
5630
5630
  hint2 = "Make pane larger";
@@ -5633,7 +5633,7 @@ function runHangmanGame(terminal) {
5633
5633
  } else {
5634
5634
  hint2 = "Make pane taller \u2193";
5635
5635
  }
5636
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
5636
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
5637
5637
  const centerX = Math.floor(cols / 2);
5638
5638
  const centerY = Math.floor(rows / 2);
5639
5639
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -5988,7 +5988,7 @@ function runMinesweeperGame(terminal) {
5988
5988
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
5989
5989
  scorePopups.push({ x, y, text, frames: 20, color });
5990
5990
  }
5991
- function triggerShake2(frames, intensity) {
5991
+ function triggerShake3(frames, intensity) {
5992
5992
  shakeFrames = frames;
5993
5993
  shakeIntensity = intensity;
5994
5994
  }
@@ -6075,7 +6075,7 @@ function runMinesweeperGame(terminal) {
6075
6075
  if (cell.isMine) {
6076
6076
  gameOver = true;
6077
6077
  won = false;
6078
- triggerShake2(25, 4);
6078
+ triggerShake3(25, 4);
6079
6079
  spawnExplosion(x * 2 + 1, y);
6080
6080
  addScorePopup2(x * 2, y - 1, "MALWARE!", "\x1B[1;91m");
6081
6081
  revealAllMines();
@@ -6124,7 +6124,7 @@ function runMinesweeperGame(terminal) {
6124
6124
  if (cellsRevealed >= totalNonMines) {
6125
6125
  gameOver = true;
6126
6126
  won = true;
6127
- triggerShake2(10, 2);
6127
+ triggerShake3(10, 2);
6128
6128
  for (let i = 0; i < 5; i++) {
6129
6129
  const x = Math.floor(Math.random() * difficulty.width);
6130
6130
  const y = Math.floor(Math.random() * difficulty.height);
@@ -6522,8 +6522,8 @@ function runMinesweeperGame(terminal) {
6522
6522
  // src/games/pong/index.ts
6523
6523
  function runPongGame(terminal) {
6524
6524
  const themeColor = getCurrentThemeColor();
6525
- const MIN_COLS = 40;
6526
- const MIN_ROWS = 16;
6525
+ const MIN_COLS2 = 40;
6526
+ const MIN_ROWS2 = 16;
6527
6527
  const getGameDimensions = () => {
6528
6528
  const cols = terminal.cols;
6529
6529
  const rows = terminal.rows;
@@ -6663,10 +6663,10 @@ function runPongGame(terminal) {
6663
6663
  }
6664
6664
  const cols = terminal.cols;
6665
6665
  const rows = terminal.rows;
6666
- if (cols < MIN_COLS || rows < MIN_ROWS) {
6666
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
6667
6667
  const msg1 = "Terminal too small!";
6668
- const needWidth = cols < MIN_COLS;
6669
- const needHeight = rows < MIN_ROWS;
6668
+ const needWidth = cols < MIN_COLS2;
6669
+ const needHeight = rows < MIN_ROWS2;
6670
6670
  let hint2 = "";
6671
6671
  if (needWidth && needHeight) {
6672
6672
  hint2 = "Make pane larger";
@@ -6675,7 +6675,7 @@ function runPongGame(terminal) {
6675
6675
  } else {
6676
6676
  hint2 = "Make pane taller \u2193";
6677
6677
  }
6678
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
6678
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
6679
6679
  const centerX2 = Math.floor(cols / 2);
6680
6680
  const centerY = Math.floor(rows / 2);
6681
6681
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -7111,8 +7111,8 @@ function runRunnerGame(terminal) {
7111
7111
  const GAME_WIDTH = ROAD_WIDTH_BOTTOM + SCENERY_WIDTH * 2;
7112
7112
  let gameTop = 5;
7113
7113
  let gameLeft = 4;
7114
- const MIN_COLS = GAME_WIDTH + 2;
7115
- const MIN_ROWS = TRACK_HEIGHT + 8;
7114
+ const MIN_COLS2 = GAME_WIDTH + 2;
7115
+ const MIN_ROWS2 = TRACK_HEIGHT + 8;
7116
7116
  let running = true;
7117
7117
  let gameStarted = false;
7118
7118
  let gameOver = false;
@@ -7343,12 +7343,12 @@ function runRunnerGame(terminal) {
7343
7343
  }
7344
7344
  const cols = terminal.cols;
7345
7345
  const rows = terminal.rows;
7346
- if (cols < MIN_COLS || rows < MIN_ROWS) {
7346
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
7347
7347
  const msg1 = "Terminal too small!";
7348
- const needWidth = cols < MIN_COLS;
7349
- const needHeight = rows < MIN_ROWS;
7348
+ const needWidth = cols < MIN_COLS2;
7349
+ const needHeight = rows < MIN_ROWS2;
7350
7350
  let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
7351
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
7351
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
7352
7352
  const centerX2 = Math.floor(cols / 2);
7353
7353
  const centerY = Math.floor(rows / 2);
7354
7354
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -7875,8 +7875,8 @@ function runRunnerGame(terminal) {
7875
7875
  // src/games/simon/index.ts
7876
7876
  function runSimonGame(terminal) {
7877
7877
  const themeColor = getCurrentThemeColor();
7878
- const MIN_COLS = 40;
7879
- const MIN_ROWS = 18;
7878
+ const MIN_COLS2 = 40;
7879
+ const MIN_ROWS2 = 18;
7880
7880
  const GAME_WIDTH = 40;
7881
7881
  const GAME_HEIGHT = 18;
7882
7882
  const QUADRANT_COLORS = [
@@ -7990,7 +7990,7 @@ function runSimonGame(terminal) {
7990
7990
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
7991
7991
  scorePopups.push({ x, y, text, frames: 20, color });
7992
7992
  }
7993
- function triggerShake2(frames, intensity) {
7993
+ function triggerShake3(frames, intensity) {
7994
7994
  shakeFrames = frames;
7995
7995
  shakeIntensity = intensity;
7996
7996
  }
@@ -8052,7 +8052,7 @@ function runSimonGame(terminal) {
8052
8052
  statusMessage = "PATTERN ACCEPTED";
8053
8053
  statusColor = "\x1B[1;92m";
8054
8054
  statusBlink = true;
8055
- triggerShake2(6, 1);
8055
+ triggerShake3(6, 1);
8056
8056
  }
8057
8057
  } else {
8058
8058
  phase = "failure";
@@ -8064,7 +8064,7 @@ function runSimonGame(terminal) {
8064
8064
  statusMessage = "ACCESS DENIED";
8065
8065
  statusColor = "\x1B[1;91m";
8066
8066
  statusBlink = true;
8067
- triggerShake2(15, 3);
8067
+ triggerShake3(15, 3);
8068
8068
  const centerX = GAME_WIDTH / 2;
8069
8069
  const centerY = GAME_HEIGHT / 2;
8070
8070
  spawnParticles3(centerX, centerY, 15, "\x1B[1;91m", ["\u2717", "\u2716", "\xD7", "\u2573"]);
@@ -8190,12 +8190,12 @@ function runSimonGame(terminal) {
8190
8190
  if (shakeFrames > 0) shakeFrames--;
8191
8191
  const cols = terminal.cols;
8192
8192
  const rows = terminal.rows;
8193
- if (cols < MIN_COLS || rows < MIN_ROWS) {
8193
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
8194
8194
  const msg1 = "Terminal too small!";
8195
- const needWidth = cols < MIN_COLS;
8196
- const needHeight = rows < MIN_ROWS;
8195
+ const needWidth = cols < MIN_COLS2;
8196
+ const needHeight = rows < MIN_ROWS2;
8197
8197
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
8198
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
8198
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
8199
8199
  const centerX = Math.floor(cols / 2);
8200
8200
  const centerY = Math.floor(rows / 2);
8201
8201
  outputParts.push(`\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`);
@@ -8457,8 +8457,8 @@ function runSimonGame(terminal) {
8457
8457
  // src/games/snake/index.ts
8458
8458
  function runSnakeGame(terminal) {
8459
8459
  const themeColor = getCurrentThemeColor();
8460
- const MIN_COLS = 36;
8461
- const MIN_ROWS = 16;
8460
+ const MIN_COLS2 = 36;
8461
+ const MIN_ROWS2 = 16;
8462
8462
  let cols = terminal.cols;
8463
8463
  let rows = terminal.rows;
8464
8464
  let gameTop = 6;
@@ -8561,7 +8561,7 @@ function runSnakeGame(terminal) {
8561
8561
  });
8562
8562
  }
8563
8563
  }
8564
- function updateParticles3() {
8564
+ function updateParticles4() {
8565
8565
  for (let i = particles.length - 1; i >= 0; i--) {
8566
8566
  const p = particles[i];
8567
8567
  p.x += p.vx;
@@ -8586,7 +8586,7 @@ function runSnakeGame(terminal) {
8586
8586
  scorePopup.y -= 0.3;
8587
8587
  if (scorePopup.frames <= 0) scorePopup = null;
8588
8588
  }
8589
- updateParticles3();
8589
+ updateParticles4();
8590
8590
  let renderGameLeft = gameLeft;
8591
8591
  let renderGameTop = gameTop;
8592
8592
  if (shakeFrames > 0) {
@@ -8595,10 +8595,10 @@ function runSnakeGame(terminal) {
8595
8595
  renderGameLeft = Math.max(1, gameLeft + shakeX);
8596
8596
  renderGameTop = Math.max(3, gameTop + shakeY);
8597
8597
  }
8598
- if (cols < MIN_COLS || rows < MIN_ROWS) {
8598
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
8599
8599
  const msg1 = "Terminal too small!";
8600
- const needWidth = cols < MIN_COLS;
8601
- const needHeight = rows < MIN_ROWS;
8600
+ const needWidth = cols < MIN_COLS2;
8601
+ const needHeight = rows < MIN_ROWS2;
8602
8602
  let hint2 = "";
8603
8603
  if (needWidth && needHeight) {
8604
8604
  hint2 = "Make pane larger";
@@ -8607,7 +8607,7 @@ function runSnakeGame(terminal) {
8607
8607
  } else {
8608
8608
  hint2 = "Make pane taller \u2193";
8609
8609
  }
8610
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
8610
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
8611
8611
  const centerX = Math.floor(cols / 2);
8612
8612
  const centerY = Math.floor(rows / 2);
8613
8613
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -8929,8 +8929,8 @@ function runSnakeGame(terminal) {
8929
8929
  // src/games/spaceinvaders/index.ts
8930
8930
  function runSpaceInvadersGame(terminal) {
8931
8931
  const themeColor = getCurrentThemeColor();
8932
- const MIN_COLS = 40;
8933
- const MIN_ROWS = 16;
8932
+ const MIN_COLS2 = 40;
8933
+ const MIN_ROWS2 = 16;
8934
8934
  const getGameDimensions = () => {
8935
8935
  const cols = terminal.cols;
8936
8936
  const rows = terminal.rows;
@@ -9108,10 +9108,10 @@ function runSpaceInvadersGame(terminal) {
9108
9108
  }
9109
9109
  const cols = terminal.cols;
9110
9110
  const rows = terminal.rows;
9111
- if (cols < MIN_COLS || rows < MIN_ROWS) {
9111
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
9112
9112
  const msg1 = "Terminal too small!";
9113
- const needWidth = cols < MIN_COLS;
9114
- const needHeight = rows < MIN_ROWS;
9113
+ const needWidth = cols < MIN_COLS2;
9114
+ const needHeight = rows < MIN_ROWS2;
9115
9115
  let hint2 = "";
9116
9116
  if (needWidth && needHeight) {
9117
9117
  hint2 = "Make pane larger";
@@ -9120,7 +9120,7 @@ function runSpaceInvadersGame(terminal) {
9120
9120
  } else {
9121
9121
  hint2 = "Make pane taller \u2193";
9122
9122
  }
9123
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
9123
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
9124
9124
  const centerX = Math.floor(cols / 2);
9125
9125
  const centerY = Math.floor(rows / 2);
9126
9126
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -9593,13 +9593,13 @@ function runTetrisGame(terminal) {
9593
9593
  const themeColor = getCurrentThemeColor();
9594
9594
  const BOARD_WIDTH = 12;
9595
9595
  const BOARD_HEIGHT = 20;
9596
- const SIDE_PANEL_WIDTH = 14;
9596
+ const SIDE_PANEL_WIDTH2 = 14;
9597
9597
  const BOARD_ONLY_WIDTH = BOARD_WIDTH * 2 + 2;
9598
- const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH + 4;
9599
- const TICK_MS = 25;
9598
+ const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH2 + 4;
9599
+ const TICK_MS2 = 25;
9600
9600
  const SPRINT_TARGET_LINES = 40;
9601
- const MIN_COLS = BOARD_ONLY_WIDTH + 2;
9602
- const MIN_ROWS = BOARD_HEIGHT + 3;
9601
+ const MIN_COLS2 = BOARD_ONLY_WIDTH + 2;
9602
+ const MIN_ROWS2 = BOARD_HEIGHT + 3;
9603
9603
  const MIN_ROWS_WITH_TITLE = BOARD_HEIGHT + 4;
9604
9604
  let running = true;
9605
9605
  let gameStarted = false;
@@ -9682,7 +9682,7 @@ function runTetrisGame(terminal) {
9682
9682
  const shape = TETROMINOES[currentPiece][0];
9683
9683
  pieceX = Math.floor((BOARD_WIDTH - shape[0].length) / 2);
9684
9684
  pieceY = 0;
9685
- if (!isValidPosition(pieceX, pieceY, currentRotation)) {
9685
+ if (!isValidPosition2(pieceX, pieceY, currentRotation)) {
9686
9686
  gameOver = true;
9687
9687
  if (score > highScore) highScore = score;
9688
9688
  }
@@ -9691,7 +9691,7 @@ function runTetrisGame(terminal) {
9691
9691
  const rotations = TETROMINOES[currentPiece];
9692
9692
  return rotations[rotation % rotations.length];
9693
9693
  }
9694
- function isValidPosition(x, y, rotation) {
9694
+ function isValidPosition2(x, y, rotation) {
9695
9695
  const shape = TETROMINOES[currentPiece][rotation % TETROMINOES[currentPiece].length];
9696
9696
  for (let row = 0; row < shape.length; row++) {
9697
9697
  for (let col = 0; col < shape[row].length; col++) {
@@ -9808,23 +9808,23 @@ function runTetrisGame(terminal) {
9808
9808
  }
9809
9809
  }
9810
9810
  function moveLeft() {
9811
- if (isValidPosition(pieceX - 1, pieceY, currentRotation)) {
9811
+ if (isValidPosition2(pieceX - 1, pieceY, currentRotation)) {
9812
9812
  pieceX--;
9813
9813
  }
9814
9814
  }
9815
9815
  function moveRight() {
9816
- if (isValidPosition(pieceX + 1, pieceY, currentRotation)) {
9816
+ if (isValidPosition2(pieceX + 1, pieceY, currentRotation)) {
9817
9817
  pieceX++;
9818
9818
  }
9819
9819
  }
9820
9820
  function moveDown() {
9821
- if (isValidPosition(pieceX, pieceY + 1, currentRotation)) {
9821
+ if (isValidPosition2(pieceX, pieceY + 1, currentRotation)) {
9822
9822
  pieceY++;
9823
9823
  return true;
9824
9824
  }
9825
9825
  return false;
9826
9826
  }
9827
- function hardDrop() {
9827
+ function hardDrop2() {
9828
9828
  if (hardDropping) return;
9829
9829
  if (hardDropConsumed) return;
9830
9830
  hardDropping = true;
@@ -9832,13 +9832,13 @@ function runTetrisGame(terminal) {
9832
9832
  }
9833
9833
  function rotate() {
9834
9834
  const newRotation = (currentRotation + 1) % TETROMINOES[currentPiece].length;
9835
- if (isValidPosition(pieceX, pieceY, newRotation)) {
9835
+ if (isValidPosition2(pieceX, pieceY, newRotation)) {
9836
9836
  currentRotation = newRotation;
9837
9837
  return;
9838
9838
  }
9839
9839
  const kicks = [-1, 1, -2, 2];
9840
9840
  for (const kick of kicks) {
9841
- if (isValidPosition(pieceX + kick, pieceY, newRotation)) {
9841
+ if (isValidPosition2(pieceX + kick, pieceY, newRotation)) {
9842
9842
  pieceX += kick;
9843
9843
  currentRotation = newRotation;
9844
9844
  return;
@@ -9850,10 +9850,10 @@ function runTetrisGame(terminal) {
9850
9850
  output += "\x1B[2J\x1B[H";
9851
9851
  const cols = terminal.cols;
9852
9852
  const rows = terminal.rows;
9853
- if (cols < MIN_COLS || rows < MIN_ROWS) {
9853
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
9854
9854
  const msg1 = "Terminal too small!";
9855
- const needWidth = cols < MIN_COLS;
9856
- const needHeight = rows < MIN_ROWS;
9855
+ const needWidth = cols < MIN_COLS2;
9856
+ const needHeight = rows < MIN_ROWS2;
9857
9857
  let hint = "";
9858
9858
  if (needWidth && needHeight) {
9859
9859
  hint = "Make pane larger";
@@ -9862,7 +9862,7 @@ function runTetrisGame(terminal) {
9862
9862
  } else {
9863
9863
  hint = "Make pane taller \u2193";
9864
9864
  }
9865
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
9865
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
9866
9866
  const centerX = Math.floor(cols / 2);
9867
9867
  const centerY = Math.floor(rows / 2);
9868
9868
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -9996,7 +9996,7 @@ function runTetrisGame(terminal) {
9996
9996
  }
9997
9997
  }
9998
9998
  let ghostY = pieceY;
9999
- while (isValidPosition(pieceX, ghostY + 1, currentRotation)) {
9999
+ while (isValidPosition2(pieceX, ghostY + 1, currentRotation)) {
10000
10000
  ghostY++;
10001
10001
  }
10002
10002
  if (ghostY !== pieceY) {
@@ -10096,7 +10096,7 @@ function runTetrisGame(terminal) {
10096
10096
  return;
10097
10097
  }
10098
10098
  dropCounter++;
10099
- if (dropCounter * TICK_MS >= dropInterval) {
10099
+ if (dropCounter * TICK_MS2 >= dropInterval) {
10100
10100
  dropCounter = 0;
10101
10101
  if (!moveDown()) {
10102
10102
  lockPiece();
@@ -10127,14 +10127,14 @@ function runTetrisGame(terminal) {
10127
10127
  return;
10128
10128
  }
10129
10129
  render();
10130
- }, TICK_MS);
10130
+ }, TICK_MS2);
10131
10131
  const gameInterval = setInterval(() => {
10132
10132
  if (!running) {
10133
10133
  clearInterval(gameInterval);
10134
10134
  return;
10135
10135
  }
10136
10136
  update();
10137
- }, TICK_MS);
10137
+ }, TICK_MS2);
10138
10138
  const handleKeyUp = (e) => {
10139
10139
  if (e.key === " ") {
10140
10140
  hardDropConsumed = false;
@@ -10271,7 +10271,7 @@ function runTetrisGame(terminal) {
10271
10271
  rotate();
10272
10272
  break;
10273
10273
  case " ":
10274
- hardDrop();
10274
+ hardDrop2();
10275
10275
  break;
10276
10276
  }
10277
10277
  });
@@ -10306,8 +10306,8 @@ var BLOCK_COLORS = [
10306
10306
  ];
10307
10307
  function runTowerGame(terminal) {
10308
10308
  const themeColor = getCurrentThemeColor();
10309
- const MIN_COLS = 30;
10310
- const MIN_ROWS = 16;
10309
+ const MIN_COLS2 = 30;
10310
+ const MIN_ROWS2 = 16;
10311
10311
  const INITIAL_BLOCK_WIDTH = 8;
10312
10312
  const MIN_BLOCK_WIDTH = 2;
10313
10313
  const PERFECT_THRESHOLD = 0.5;
@@ -10386,7 +10386,7 @@ function runTowerGame(terminal) {
10386
10386
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
10387
10387
  scorePopups.push({ x, y, text, frames: 24, color });
10388
10388
  }
10389
- function triggerShake2(frames, intensity) {
10389
+ function triggerShake3(frames, intensity) {
10390
10390
  shakeFrames = frames;
10391
10391
  shakeIntensity = intensity;
10392
10392
  }
@@ -10447,7 +10447,7 @@ function runTowerGame(terminal) {
10447
10447
  if (score > highScore) highScore = score;
10448
10448
  if (perfectCombo > maxCombo) maxCombo = perfectCombo;
10449
10449
  spawnParticles3(dropX + dropWidth / 2, landY, 15, "\x1B[91m");
10450
- triggerShake2(10, 3);
10450
+ triggerShake3(10, 3);
10451
10451
  return;
10452
10452
  }
10453
10453
  const isPerfect = Math.abs(dropLeft - topLeft) < PERFECT_THRESHOLD && Math.abs(dropRight - topRight) < PERFECT_THRESHOLD;
@@ -10465,7 +10465,7 @@ function runTowerGame(terminal) {
10465
10465
  popupText,
10466
10466
  perfectCombo > 3 ? "\x1B[1;91m" : "\x1B[1;93m"
10467
10467
  );
10468
- triggerShake2(4, 1);
10468
+ triggerShake3(4, 1);
10469
10469
  } else {
10470
10470
  if (perfectCombo > maxCombo) maxCombo = perfectCombo;
10471
10471
  perfectCombo = 0;
@@ -10479,7 +10479,7 @@ function runTowerGame(terminal) {
10479
10479
  spawnFallingPiece(topRight, landY, overhangWidth, dropColor);
10480
10480
  spawnParticles3(topRight + overhangWidth / 2, landY, 5, dropColor);
10481
10481
  }
10482
- triggerShake2(3, 1);
10482
+ triggerShake3(3, 1);
10483
10483
  }
10484
10484
  score += points;
10485
10485
  height++;
@@ -10547,12 +10547,12 @@ function runTowerGame(terminal) {
10547
10547
  if (shakeFrames > 0) shakeFrames--;
10548
10548
  const cols = terminal.cols;
10549
10549
  const rows = terminal.rows;
10550
- if (cols < MIN_COLS || rows < MIN_ROWS) {
10550
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
10551
10551
  const msg1 = "Terminal too small!";
10552
- const needWidth = cols < MIN_COLS;
10553
- const needHeight = rows < MIN_ROWS;
10552
+ const needWidth = cols < MIN_COLS2;
10553
+ const needHeight = rows < MIN_ROWS2;
10554
10554
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
10555
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
10555
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
10556
10556
  const centerX = Math.floor(cols / 2);
10557
10557
  const centerY = Math.floor(rows / 2);
10558
10558
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -10797,8 +10797,8 @@ function runTowerGame(terminal) {
10797
10797
  // src/games/tron/index.ts
10798
10798
  function runTronGame(terminal) {
10799
10799
  const themeColor = getCurrentThemeColor();
10800
- const MIN_COLS = 40;
10801
- const MIN_ROWS = 18;
10800
+ const MIN_COLS2 = 40;
10801
+ const MIN_ROWS2 = 18;
10802
10802
  const GAME_WIDTH = 46;
10803
10803
  const GAME_HEIGHT = 18;
10804
10804
  const ROUNDS_TO_WIN = 3;
@@ -10867,9 +10867,9 @@ function runTronGame(terminal) {
10867
10867
  }
10868
10868
  function spawnExplosion(x, y, color) {
10869
10869
  spawnParticles3(x, y, 20, color, ["\u2717", "\xD7", "\u2591", "\u2592", "\u2593", "\u2588"]);
10870
- triggerShake2(12, 3);
10870
+ triggerShake3(12, 3);
10871
10871
  }
10872
- function triggerShake2(frames, intensity) {
10872
+ function triggerShake3(frames, intensity) {
10873
10873
  shakeFrames = frames;
10874
10874
  shakeIntensity = intensity;
10875
10875
  }
@@ -11084,7 +11084,7 @@ function runTronGame(terminal) {
11084
11084
  arenaMinY += SHRINK_AMOUNT;
11085
11085
  arenaMaxY -= SHRINK_AMOUNT;
11086
11086
  }
11087
- triggerShake2(4, 1);
11087
+ triggerShake3(4, 1);
11088
11088
  }
11089
11089
  }
11090
11090
  }
@@ -11151,12 +11151,12 @@ function runTronGame(terminal) {
11151
11151
  if (shakeFrames > 0) shakeFrames--;
11152
11152
  const cols = terminal.cols;
11153
11153
  const rows = terminal.rows;
11154
- if (cols < MIN_COLS || rows < MIN_ROWS) {
11154
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
11155
11155
  const msg1 = "Terminal too small!";
11156
- const needWidth = cols < MIN_COLS;
11157
- const needHeight = rows < MIN_ROWS;
11156
+ const needWidth = cols < MIN_COLS2;
11157
+ const needHeight = rows < MIN_ROWS2;
11158
11158
  const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
11159
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
11159
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
11160
11160
  const centerX = Math.floor(cols / 2);
11161
11161
  const centerY = Math.floor(rows / 2);
11162
11162
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -11245,7 +11245,7 @@ function runTronGame(terminal) {
11245
11245
  const nextMsg = "Press SPACE for next round";
11246
11246
  const nextX = gameLeft + Math.floor((GAME_WIDTH - nextMsg.length) / 2) + 1;
11247
11247
  output += `\x1B[${msgY + 2};${nextX}H\x1B[2m${themeColor}${nextMsg}\x1B[0m`;
11248
- output = renderParticles(output, renderLeft, renderTop);
11248
+ output = renderParticles2(output, renderLeft, renderTop);
11249
11249
  } else if (gameOver) {
11250
11250
  output = renderTrailsAndCycles(output, renderLeft, renderTop);
11251
11251
  const overColor = playerWins >= ROUNDS_TO_WIN ? "\x1B[1;92m" : "\x1B[1;91m";
@@ -11259,7 +11259,7 @@ function runTronGame(terminal) {
11259
11259
  const restart = "\u255A [R] RESTART [Q] QUIT \u255D";
11260
11260
  const restartX = gameLeft + Math.floor((GAME_WIDTH - restart.length) / 2) + 1;
11261
11261
  output += `\x1B[${overY + 2};${restartX}H\x1B[2m${themeColor}${restart}\x1B[0m`;
11262
- output = renderParticles(output, renderLeft, renderTop);
11262
+ output = renderParticles2(output, renderLeft, renderTop);
11263
11263
  } else {
11264
11264
  for (const pos of player.trail) {
11265
11265
  if (pos.x > arenaMinX && pos.x < arenaMaxX && pos.y > arenaMinY && pos.y < arenaMaxY) {
@@ -11277,7 +11277,7 @@ function runTronGame(terminal) {
11277
11277
  if (ai.alive) {
11278
11278
  output += `\x1B[${renderTop + 1 + ai.y};${renderLeft + 1 + ai.x}H\x1B[1m${ai.color}${ai.char}\x1B[0m`;
11279
11279
  }
11280
- output = renderParticles(output, renderLeft, renderTop);
11280
+ output = renderParticles2(output, renderLeft, renderTop);
11281
11281
  }
11282
11282
  const hint = gameStarted && !gameOver && !paused ? `SPEED: ${Math.round((1 - gameSpeed / BASE_SPEED) * 100) + 100}% [ ESC ] MENU` : "";
11283
11283
  const hintX = Math.floor((cols - hint.length) / 2);
@@ -11299,7 +11299,7 @@ function runTronGame(terminal) {
11299
11299
  }
11300
11300
  return output;
11301
11301
  }
11302
- function renderParticles(output, renderLeft, renderTop) {
11302
+ function renderParticles2(output, renderLeft, renderTop) {
11303
11303
  for (const p of particles) {
11304
11304
  const screenX = Math.round(renderLeft + 1 + p.x);
11305
11305
  const screenY = Math.round(renderTop + 1 + p.y);
@@ -11634,8 +11634,8 @@ var SENTENCES = [
11634
11634
  function runTypingTest(terminal) {
11635
11635
  const themeColor = getCurrentThemeColor();
11636
11636
  const GAME_WIDTH = 60;
11637
- const MIN_COLS = 40;
11638
- const MIN_ROWS = 18;
11637
+ const MIN_COLS2 = 40;
11638
+ const MIN_ROWS2 = 18;
11639
11639
  let running = true;
11640
11640
  let gameStarted = false;
11641
11641
  let gameOver = false;
@@ -11834,10 +11834,10 @@ function runTypingTest(terminal) {
11834
11834
  }
11835
11835
  const cols = terminal.cols;
11836
11836
  const rows = terminal.rows;
11837
- if (cols < MIN_COLS || rows < MIN_ROWS) {
11837
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
11838
11838
  const msg1 = "Terminal too small!";
11839
- const needWidth = cols < MIN_COLS;
11840
- const needHeight = rows < MIN_ROWS;
11839
+ const needWidth = cols < MIN_COLS2;
11840
+ const needHeight = rows < MIN_ROWS2;
11841
11841
  let hint2 = "";
11842
11842
  if (needWidth && needHeight) {
11843
11843
  hint2 = "Make pane larger";
@@ -11846,7 +11846,7 @@ function runTypingTest(terminal) {
11846
11846
  } else {
11847
11847
  hint2 = "Make pane taller \u2193";
11848
11848
  }
11849
- const msg2 = `Need: ${MIN_COLS}\xD7${MIN_ROWS} Have: ${cols}\xD7${rows}`;
11849
+ const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
11850
11850
  const centerX = Math.floor(cols / 2);
11851
11851
  const centerY = Math.floor(rows / 2);
11852
11852
  output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -12491,8 +12491,8 @@ var WORDS2 = [
12491
12491
  ];
12492
12492
  function runWordleGame(terminal) {
12493
12493
  const themeColor = getCurrentThemeColor();
12494
- const MIN_COLS = 36;
12495
- const MIN_ROWS = 20;
12494
+ const MIN_COLS2 = 36;
12495
+ const MIN_ROWS2 = 20;
12496
12496
  const MAX_GUESSES = 6;
12497
12497
  const WORD_LENGTH = 5;
12498
12498
  const KEYBOARD_ROWS = [
@@ -12557,7 +12557,7 @@ function runWordleGame(terminal) {
12557
12557
  });
12558
12558
  }
12559
12559
  }
12560
- function spawnFirework3(x, y) {
12560
+ function spawnFirework4(x, y) {
12561
12561
  const colors = ["\x1B[1;92m", "\x1B[1;93m", "\x1B[1;96m", "\x1B[1;95m"];
12562
12562
  const color = colors[Math.floor(Math.random() * colors.length)];
12563
12563
  const chars = ["*", "+", "o", ".", "`", "'"];
@@ -12578,7 +12578,7 @@ function runWordleGame(terminal) {
12578
12578
  function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
12579
12579
  scorePopups.push({ x, y, text, frames: 25, color });
12580
12580
  }
12581
- function triggerShake2(frames, intensity) {
12581
+ function triggerShake3(frames, intensity) {
12582
12582
  shakeFrames = frames;
12583
12583
  shakeIntensity = intensity;
12584
12584
  }
@@ -12636,7 +12636,7 @@ function runWordleGame(terminal) {
12636
12636
  }
12637
12637
  function submitGuess() {
12638
12638
  if (currentGuess.length !== WORD_LENGTH) {
12639
- triggerShake2(4, 2);
12639
+ triggerShake3(4, 2);
12640
12640
  addScorePopup2(Math.floor(cols / 2), 8, "NOT ENOUGH LETTERS", "\x1B[1;91m");
12641
12641
  return;
12642
12642
  }
@@ -12662,7 +12662,7 @@ function runWordleGame(terminal) {
12662
12662
  }
12663
12663
  stats.guessDistribution[guesses.length - 1]++;
12664
12664
  borderFlash = 20;
12665
- triggerShake2(10, 3);
12665
+ triggerShake3(10, 3);
12666
12666
  const messages = ["CIPHER BREACHED!", "CODE CRACKED!", "DECRYPTED!", "BRILLIANT!"];
12667
12667
  addScorePopup2(centerX, guessY - 2, messages[Math.floor(Math.random() * messages.length)], "\x1B[1;92m");
12668
12668
  setTimeout(() => {
@@ -12670,7 +12670,7 @@ function runWordleGame(terminal) {
12670
12670
  for (let i = 0; i < 5; i++) {
12671
12671
  setTimeout(() => {
12672
12672
  if (running && won) {
12673
- spawnFirework3(
12673
+ spawnFirework4(
12674
12674
  10 + Math.random() * (cols - 20),
12675
12675
  5 + Math.random() * 10
12676
12676
  );
@@ -12683,7 +12683,7 @@ function runWordleGame(terminal) {
12683
12683
  gameOver = true;
12684
12684
  stats.gamesPlayed++;
12685
12685
  stats.currentStreak = 0;
12686
- triggerShake2(8, 4);
12686
+ triggerShake3(8, 4);
12687
12687
  borderFlash = 15;
12688
12688
  addScorePopup2(centerX, guessY - 2, "DECRYPTION FAILED", "\x1B[1;91m");
12689
12689
  spawnParticles3(centerX, guessY, 10, "\x1B[1;91m", ["X", "x", ".", "*"]);
@@ -12754,12 +12754,12 @@ function runWordleGame(terminal) {
12754
12754
  rows = terminal.rows;
12755
12755
  if (shakeFrames > 0) shakeFrames--;
12756
12756
  if (borderFlash > 0) borderFlash--;
12757
- if (cols < MIN_COLS || rows < MIN_ROWS) {
12757
+ if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
12758
12758
  const msg1 = "Terminal too small!";
12759
- const needWidth = cols < MIN_COLS;
12760
- const needHeight = rows < MIN_ROWS;
12759
+ const needWidth = cols < MIN_COLS2;
12760
+ const needHeight = rows < MIN_ROWS2;
12761
12761
  let hint = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider" : "Make pane taller";
12762
- const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
12762
+ const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
12763
12763
  const centerX2 = Math.floor(cols / 2);
12764
12764
  const centerY = Math.floor(rows / 2);
12765
12765
  output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
@@ -13078,6 +13078,2492 @@ function runWordleGame(terminal) {
13078
13078
  return controller;
13079
13079
  }
13080
13080
 
13081
+ // src/games/hyper-fighter/engine.ts
13082
+ var BOARD_ROWS = 12;
13083
+ var BOARD_COLS = 6;
13084
+ var COLORS = ["red", "green", "blue", "yellow"];
13085
+ var CRASH_GEM_CHANCE = 0.25;
13086
+ var DROP_ALLEY_COL = 3;
13087
+ var DIAMOND_INTERVAL = 25;
13088
+ var nextPowerGemId = 1;
13089
+ function createBoard() {
13090
+ const board = [];
13091
+ for (let r = 0; r < BOARD_ROWS; r++) {
13092
+ board.push(new Array(BOARD_COLS).fill(null));
13093
+ }
13094
+ return board;
13095
+ }
13096
+ function randomColor() {
13097
+ return COLORS[Math.floor(Math.random() * COLORS.length)];
13098
+ }
13099
+ function randomGem() {
13100
+ const isCrash = Math.random() < CRASH_GEM_CHANCE;
13101
+ return {
13102
+ color: randomColor(),
13103
+ type: isCrash ? "crash" : "normal"
13104
+ };
13105
+ }
13106
+ function generatePair() {
13107
+ return {
13108
+ primary: randomGem(),
13109
+ secondary: randomGem(),
13110
+ col: DROP_ALLEY_COL,
13111
+ row: 0,
13112
+ orientation: 0
13113
+ };
13114
+ }
13115
+ function getSecondaryPos(pair) {
13116
+ switch (pair.orientation) {
13117
+ case 0:
13118
+ return { row: pair.row - 1, col: pair.col };
13119
+ // up
13120
+ case 1:
13121
+ return { row: pair.row, col: pair.col + 1 };
13122
+ // right
13123
+ case 2:
13124
+ return { row: pair.row + 1, col: pair.col };
13125
+ // down
13126
+ case 3:
13127
+ return { row: pair.row, col: pair.col - 1 };
13128
+ // left
13129
+ default:
13130
+ return { row: pair.row - 1, col: pair.col };
13131
+ }
13132
+ }
13133
+ function isValidPosition(pair, board) {
13134
+ const sec = getSecondaryPos(pair);
13135
+ if (pair.col < 0 || pair.col >= BOARD_COLS) return false;
13136
+ if (pair.row >= BOARD_ROWS) return false;
13137
+ if (sec.col < 0 || sec.col >= BOARD_COLS) return false;
13138
+ if (sec.row >= BOARD_ROWS) return false;
13139
+ if (pair.row >= 0 && board[pair.row][pair.col] !== null) return false;
13140
+ if (sec.row >= 0 && board[sec.row][sec.col] !== null) return false;
13141
+ return true;
13142
+ }
13143
+ function movePair(pair, board, dx) {
13144
+ const test = { ...pair, col: pair.col + dx };
13145
+ if (isValidPosition(test, board)) {
13146
+ pair.col += dx;
13147
+ return true;
13148
+ }
13149
+ return false;
13150
+ }
13151
+ function rotatePair(pair, board, clockwise) {
13152
+ const newOrientation = clockwise ? (pair.orientation + 1) % 4 : (pair.orientation + 3) % 4;
13153
+ const test = { ...pair, orientation: newOrientation };
13154
+ if (isValidPosition(test, board)) {
13155
+ pair.orientation = newOrientation;
13156
+ return true;
13157
+ }
13158
+ for (const kick of [1, -1, 2, -2]) {
13159
+ const kicked = { ...test, col: test.col + kick };
13160
+ if (isValidPosition(kicked, board)) {
13161
+ pair.orientation = newOrientation;
13162
+ pair.col += kick;
13163
+ return true;
13164
+ }
13165
+ }
13166
+ const upKick = { ...test, row: test.row - 1 };
13167
+ if (isValidPosition(upKick, board)) {
13168
+ pair.orientation = newOrientation;
13169
+ pair.row -= 1;
13170
+ return true;
13171
+ }
13172
+ return false;
13173
+ }
13174
+ function dropPair(pair, board) {
13175
+ const test = { ...pair, row: pair.row + 1 };
13176
+ if (isValidPosition(test, board)) {
13177
+ pair.row += 1;
13178
+ return true;
13179
+ }
13180
+ return false;
13181
+ }
13182
+ function hardDrop(pair, board) {
13183
+ let dropped = 0;
13184
+ while (dropPair(pair, board)) {
13185
+ dropped++;
13186
+ }
13187
+ return dropped;
13188
+ }
13189
+ function lockPair(pair, board) {
13190
+ if (pair.row >= 0 && pair.row < BOARD_ROWS) {
13191
+ board[pair.row][pair.col] = { ...pair.primary };
13192
+ }
13193
+ const sec = getSecondaryPos(pair);
13194
+ if (sec.row >= 0 && sec.row < BOARD_ROWS) {
13195
+ board[sec.row][sec.col] = { ...pair.secondary };
13196
+ }
13197
+ }
13198
+ function applyGravityFull(board, powerGems) {
13199
+ void powerGems;
13200
+ stripPowerGemIds(board);
13201
+ let moved = false;
13202
+ for (let col = 0; col < BOARD_COLS; col++) {
13203
+ let writeRow = BOARD_ROWS - 1;
13204
+ for (let row = BOARD_ROWS - 1; row >= 0; row--) {
13205
+ if (board[row][col] !== null) {
13206
+ if (row !== writeRow) {
13207
+ board[writeRow][col] = board[row][col];
13208
+ board[row][col] = null;
13209
+ moved = true;
13210
+ }
13211
+ writeRow--;
13212
+ }
13213
+ }
13214
+ }
13215
+ return moved;
13216
+ }
13217
+ function stripPowerGemIds(board) {
13218
+ for (let r = 0; r < BOARD_ROWS; r++) {
13219
+ for (let c = 0; c < BOARD_COLS; c++) {
13220
+ const gem = board[r][c];
13221
+ if (gem && gem.powerGemId !== void 0) {
13222
+ delete gem.powerGemId;
13223
+ }
13224
+ }
13225
+ }
13226
+ }
13227
+ function detectPowerGems(board) {
13228
+ const found = [];
13229
+ const used = /* @__PURE__ */ new Set();
13230
+ for (let r = 0; r < BOARD_ROWS; r++) {
13231
+ for (let c = 0; c < BOARD_COLS; c++) {
13232
+ const gem = board[r][c];
13233
+ if (!gem || gem.type !== "normal") continue;
13234
+ for (let h = 2; h <= BOARD_ROWS - r; h++) {
13235
+ for (let w = 2; w <= BOARD_COLS - c; w++) {
13236
+ if (isUniformRect(board, r, c, w, h, gem.color)) {
13237
+ const key = `${r},${c},${w},${h}`;
13238
+ if (!used.has(key)) {
13239
+ found.push({
13240
+ id: nextPowerGemId++,
13241
+ color: gem.color,
13242
+ x: c,
13243
+ y: r,
13244
+ width: w,
13245
+ height: h
13246
+ });
13247
+ }
13248
+ }
13249
+ }
13250
+ }
13251
+ }
13252
+ }
13253
+ found.sort((a, b) => b.width * b.height - a.width * a.height);
13254
+ const result = [];
13255
+ const claimed = /* @__PURE__ */ new Set();
13256
+ for (const pg of found) {
13257
+ let overlap = false;
13258
+ for (let r = pg.y; r < pg.y + pg.height; r++) {
13259
+ for (let c = pg.x; c < pg.x + pg.width; c++) {
13260
+ if (claimed.has(`${r},${c}`)) {
13261
+ overlap = true;
13262
+ break;
13263
+ }
13264
+ }
13265
+ if (overlap) break;
13266
+ }
13267
+ if (!overlap) {
13268
+ result.push(pg);
13269
+ for (let r = pg.y; r < pg.y + pg.height; r++) {
13270
+ for (let c = pg.x; c < pg.x + pg.width; c++) {
13271
+ claimed.add(`${r},${c}`);
13272
+ const gem = board[r][c];
13273
+ if (gem) gem.powerGemId = pg.id;
13274
+ }
13275
+ }
13276
+ }
13277
+ }
13278
+ return result;
13279
+ }
13280
+ function isUniformRect(board, startRow, startCol, width, height, color) {
13281
+ for (let r = startRow; r < startRow + height; r++) {
13282
+ for (let c = startCol; c < startCol + width; c++) {
13283
+ const gem = board[r][c];
13284
+ if (!gem || gem.type !== "normal" || gem.color !== color) return false;
13285
+ }
13286
+ }
13287
+ return true;
13288
+ }
13289
+ function findCrashTargets(board, _powerGems) {
13290
+ void _powerGems;
13291
+ const targets = findCrashTargetsCore(board);
13292
+ return targets;
13293
+ }
13294
+ function findCrashTargetsWithPowerInfo(board, powerGems) {
13295
+ const targets = findCrashTargetsCore(board);
13296
+ const destroyedPgIds = /* @__PURE__ */ new Set();
13297
+ for (const t of targets) {
13298
+ const gem = board[t.row][t.col];
13299
+ if (gem && gem.powerGemId !== void 0) {
13300
+ destroyedPgIds.add(gem.powerGemId);
13301
+ }
13302
+ }
13303
+ const destroyedPowerGemSizes = [];
13304
+ for (const pg of powerGems) {
13305
+ if (destroyedPgIds.has(pg.id)) {
13306
+ destroyedPowerGemSizes.push(pg.width * pg.height);
13307
+ }
13308
+ }
13309
+ return { targets, destroyedPowerGemSizes };
13310
+ }
13311
+ function findCrashTargetsCore(board) {
13312
+ const targetsByCell = /* @__PURE__ */ new Map();
13313
+ const visited = /* @__PURE__ */ new Set();
13314
+ for (let r = 0; r < BOARD_ROWS; r++) {
13315
+ for (let c = 0; c < BOARD_COLS; c++) {
13316
+ const gem = board[r][c];
13317
+ if (!gem || gem.type !== "crash") continue;
13318
+ const neighbors = getNeighbors(r, c);
13319
+ let hasTarget = false;
13320
+ for (const [nr, nc] of neighbors) {
13321
+ const ng = board[nr][nc];
13322
+ if (ng && ng.color === gem.color && ng.type === "normal") {
13323
+ hasTarget = true;
13324
+ break;
13325
+ }
13326
+ }
13327
+ if (hasTarget) {
13328
+ const connected = floodFillCrashGroup(board, r, c, gem.color, visited);
13329
+ for (const cell of connected) {
13330
+ targetsByCell.set(`${cell.row},${cell.col}`, { ...cell, color: gem.color });
13331
+ }
13332
+ }
13333
+ }
13334
+ }
13335
+ const targetSet = new Set(Array.from(targetsByCell.keys()));
13336
+ for (const key of Array.from(targetSet)) {
13337
+ const [rStr, cStr] = key.split(",");
13338
+ const r = parseInt(rStr, 10);
13339
+ const c = parseInt(cStr, 10);
13340
+ for (const [nr, nc] of getNeighbors(r, c)) {
13341
+ const nKey = `${nr},${nc}`;
13342
+ if (targetSet.has(nKey)) continue;
13343
+ const ng = board[nr][nc];
13344
+ if (ng && ng.type === "counter") {
13345
+ targetsByCell.set(nKey, { row: nr, col: nc, color: ng.color });
13346
+ targetSet.add(nKey);
13347
+ }
13348
+ }
13349
+ }
13350
+ return Array.from(targetsByCell.values());
13351
+ }
13352
+ function getNeighbors(row, col) {
13353
+ const n = [];
13354
+ if (row > 0) n.push([row - 1, col]);
13355
+ if (row < BOARD_ROWS - 1) n.push([row + 1, col]);
13356
+ if (col > 0) n.push([row, col - 1]);
13357
+ if (col < BOARD_COLS - 1) n.push([row, col + 1]);
13358
+ return n;
13359
+ }
13360
+ function floodFillCrashGroup(board, startRow, startCol, color, globalVisited) {
13361
+ const result = [];
13362
+ const stack = [[startRow, startCol]];
13363
+ const localVisited = /* @__PURE__ */ new Set();
13364
+ while (stack.length > 0) {
13365
+ const [r, c] = stack.pop();
13366
+ const key = `${r},${c}`;
13367
+ if (localVisited.has(key) || globalVisited.has(key)) continue;
13368
+ const gem = board[r][c];
13369
+ if (!gem) continue;
13370
+ if (gem.color !== color) continue;
13371
+ if (gem.type !== "normal" && gem.type !== "crash") continue;
13372
+ localVisited.add(key);
13373
+ globalVisited.add(key);
13374
+ result.push({ row: r, col: c });
13375
+ for (const [nr, nc] of getNeighbors(r, c)) {
13376
+ stack.push([nr, nc]);
13377
+ }
13378
+ }
13379
+ return result;
13380
+ }
13381
+ function clearGems(board, targets) {
13382
+ for (const t of targets) {
13383
+ board[t.row][t.col] = null;
13384
+ }
13385
+ }
13386
+ function resolveDiamond(board) {
13387
+ const cleared = [];
13388
+ for (let r = 0; r < BOARD_ROWS; r++) {
13389
+ for (let c = 0; c < BOARD_COLS; c++) {
13390
+ const gem = board[r][c];
13391
+ if (!gem || gem.type !== "diamond") continue;
13392
+ let targetColor = null;
13393
+ if (r + 1 < BOARD_ROWS && board[r + 1][c]) {
13394
+ targetColor = board[r + 1][c].color;
13395
+ }
13396
+ if (!targetColor) {
13397
+ for (const [nr, nc] of getNeighbors(r, c)) {
13398
+ if (board[nr][nc] && board[nr][nc].type !== "diamond") {
13399
+ targetColor = board[nr][nc].color;
13400
+ break;
13401
+ }
13402
+ }
13403
+ }
13404
+ board[r][c] = null;
13405
+ cleared.push({ row: r, col: c, color: targetColor || "red" });
13406
+ if (!targetColor) continue;
13407
+ for (let dr = 0; dr < BOARD_ROWS; dr++) {
13408
+ for (let dc = 0; dc < BOARD_COLS; dc++) {
13409
+ const g = board[dr][dc];
13410
+ if (g && g.color === targetColor) {
13411
+ cleared.push({ row: dr, col: dc, color: targetColor });
13412
+ board[dr][dc] = null;
13413
+ }
13414
+ }
13415
+ }
13416
+ }
13417
+ }
13418
+ return cleared;
13419
+ }
13420
+ function shouldSpawnDiamond(totalDrops) {
13421
+ return totalDrops > 0 && totalDrops % DIAMOND_INTERVAL === 0;
13422
+ }
13423
+ function generateDiamondPair() {
13424
+ return {
13425
+ primary: { color: "red", type: "diamond" },
13426
+ secondary: randomGem(),
13427
+ col: DROP_ALLEY_COL,
13428
+ row: 0,
13429
+ orientation: 0
13430
+ };
13431
+ }
13432
+ function calculateStepAttack(info) {
13433
+ let pgBonus = 0;
13434
+ for (const area of info.powerGemSizes) {
13435
+ pgBonus += Math.floor(area / 8);
13436
+ }
13437
+ return Math.floor((info.gemsCleared + pgBonus) * info.chainStep);
13438
+ }
13439
+ function applyAttackModifiers(total, damageModifier = 1, isDiamondClear = false) {
13440
+ let attack = Math.floor(total * damageModifier);
13441
+ if (isDiamondClear) attack = Math.floor(attack * 0.5);
13442
+ return attack;
13443
+ }
13444
+ function resolveCounterAttack(attack, pendingGarbage, ratio = 2) {
13445
+ if (attack <= 0 || pendingGarbage <= 0) {
13446
+ return {
13447
+ remainingAttack: attack,
13448
+ remainingPending: pendingGarbage,
13449
+ canceledGems: 0,
13450
+ pendingStartsAtThree: false
13451
+ };
13452
+ }
13453
+ const cancelable = Math.floor(attack / ratio);
13454
+ const canceledGems = Math.min(cancelable, pendingGarbage);
13455
+ const remainingPending = pendingGarbage - canceledGems;
13456
+ const remainingAttack = attack - canceledGems * ratio;
13457
+ return {
13458
+ remainingAttack,
13459
+ remainingPending,
13460
+ canceledGems,
13461
+ pendingStartsAtThree: canceledGems > 0 && remainingPending > 0
13462
+ };
13463
+ }
13464
+ function decrementCounters(player) {
13465
+ for (let r = 0; r < BOARD_ROWS; r++) {
13466
+ for (let c = 0; c < BOARD_COLS; c++) {
13467
+ const gem = player.board[r][c];
13468
+ if (gem && gem.type === "counter" && gem.counterTimer !== void 0) {
13469
+ gem.counterTimer--;
13470
+ if (gem.counterTimer <= 0) {
13471
+ gem.type = "normal";
13472
+ delete gem.counterTimer;
13473
+ }
13474
+ }
13475
+ }
13476
+ }
13477
+ }
13478
+ function checkGameOver(board) {
13479
+ return board[0][DROP_ALLEY_COL] !== null;
13480
+ }
13481
+ function getGhostPosition(pair, board) {
13482
+ const ghost = { ...pair };
13483
+ while (true) {
13484
+ const test = { ...ghost, row: ghost.row + 1 };
13485
+ if (!isValidPosition(test, board)) break;
13486
+ ghost.row++;
13487
+ }
13488
+ const sec = getSecondaryPos(ghost);
13489
+ return {
13490
+ primaryRow: ghost.row,
13491
+ primaryCol: ghost.col,
13492
+ secondaryRow: sec.row,
13493
+ secondaryCol: sec.col
13494
+ };
13495
+ }
13496
+ function createPlayerState() {
13497
+ return {
13498
+ board: createBoard(),
13499
+ currentPair: null,
13500
+ nextPair: generatePair(),
13501
+ powerGems: [],
13502
+ score: 0,
13503
+ pendingGarbage: 0,
13504
+ pendingCounteredGarbage: 0,
13505
+ alive: true,
13506
+ totalDrops: 0
13507
+ };
13508
+ }
13509
+ function spawnPair(player) {
13510
+ player.currentPair = player.nextPair;
13511
+ player.currentPair.col = DROP_ALLEY_COL;
13512
+ player.currentPair.row = 0;
13513
+ player.currentPair.orientation = 0;
13514
+ player.totalDrops++;
13515
+ if (shouldSpawnDiamond(player.totalDrops + 1)) {
13516
+ player.nextPair = generateDiamondPair();
13517
+ } else {
13518
+ player.nextPair = generatePair();
13519
+ }
13520
+ if (!isValidPosition(player.currentPair, player.board)) {
13521
+ player.alive = false;
13522
+ return false;
13523
+ }
13524
+ return true;
13525
+ }
13526
+
13527
+ // src/games/hyper-fighter/ai.ts
13528
+ var DIFFICULTIES2 = {
13529
+ easy: { name: "EASY", thinkFrames: 14, mistakeRate: 0.3, dropSpeedBoost: 1, simulateDepth: 0 },
13530
+ normal: { name: "NORMAL", thinkFrames: 8, mistakeRate: 0.1, dropSpeedBoost: 1.5, simulateDepth: 0 },
13531
+ hard: { name: "HARD", thinkFrames: 4, mistakeRate: 0.02, dropSpeedBoost: 2, simulateDepth: 1 }
13532
+ };
13533
+ function createAIState(difficulty) {
13534
+ return {
13535
+ targetCol: 2,
13536
+ targetOrientation: 0,
13537
+ thinkTimer: 0,
13538
+ moveTimer: 0,
13539
+ decided: false,
13540
+ difficulty
13541
+ };
13542
+ }
13543
+ function getColumnHeight(board, col) {
13544
+ for (let r = 0; r < BOARD_ROWS; r++) {
13545
+ if (board[r][col] !== null) return BOARD_ROWS - r;
13546
+ }
13547
+ return 0;
13548
+ }
13549
+ function evaluateBoard(board) {
13550
+ let score = 0;
13551
+ for (let c = 0; c < BOARD_COLS; c++) {
13552
+ const h = getColumnHeight(board, c);
13553
+ score -= h * h * 2;
13554
+ }
13555
+ const heights = [];
13556
+ for (let c = 0; c < BOARD_COLS; c++) {
13557
+ heights.push(getColumnHeight(board, c));
13558
+ }
13559
+ for (let c = 0; c < BOARD_COLS - 1; c++) {
13560
+ const diff = Math.abs(heights[c] - heights[c + 1]);
13561
+ score -= diff * 3;
13562
+ }
13563
+ for (let r = 0; r < BOARD_ROWS; r++) {
13564
+ for (let c = 0; c < BOARD_COLS; c++) {
13565
+ const gem = board[r][c];
13566
+ if (!gem || gem.type === "counter") continue;
13567
+ if (c + 1 < BOARD_COLS) {
13568
+ const right = board[r][c + 1];
13569
+ if (right && right.color === gem.color && right.type !== "counter") {
13570
+ score += 4;
13571
+ }
13572
+ }
13573
+ if (r + 1 < BOARD_ROWS) {
13574
+ const below = board[r + 1][c];
13575
+ if (below && below.color === gem.color && below.type !== "counter") {
13576
+ score += 4;
13577
+ }
13578
+ }
13579
+ }
13580
+ }
13581
+ for (let r = 0; r < BOARD_ROWS; r++) {
13582
+ for (let c = 0; c < BOARD_COLS; c++) {
13583
+ const gem = board[r][c];
13584
+ if (!gem || gem.type !== "crash") continue;
13585
+ const neighbors = [
13586
+ [r - 1, c],
13587
+ [r + 1, c],
13588
+ [r, c - 1],
13589
+ [r, c + 1]
13590
+ ];
13591
+ for (const [nr, nc] of neighbors) {
13592
+ if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) {
13593
+ const ng = board[nr][nc];
13594
+ if (ng && ng.color === gem.color && ng.type === "normal") {
13595
+ score += 15;
13596
+ }
13597
+ }
13598
+ }
13599
+ }
13600
+ }
13601
+ const powerGems = detectPowerGems(board);
13602
+ for (const pg of powerGems) {
13603
+ score += pg.width * pg.height * 8;
13604
+ }
13605
+ const targets = findCrashTargets(board, powerGems);
13606
+ score += targets.length * 10;
13607
+ const maxHeight = Math.max(...heights);
13608
+ if (maxHeight >= BOARD_ROWS - 2) score -= 200;
13609
+ if (maxHeight >= BOARD_ROWS - 1) score -= 500;
13610
+ const deathHeight = heights[DROP_ALLEY_COL];
13611
+ if (deathHeight >= BOARD_ROWS - 3) score -= 100;
13612
+ return score;
13613
+ }
13614
+ function cloneBoard(board) {
13615
+ return board.map((row) => row.map((gem) => gem ? { ...gem } : null));
13616
+ }
13617
+ function evaluatePlacement(pair, board, simulateDepth = 0) {
13618
+ const testBoard = cloneBoard(board);
13619
+ const testPair = {
13620
+ primary: { ...pair.primary },
13621
+ secondary: { ...pair.secondary },
13622
+ col: pair.col,
13623
+ row: pair.row,
13624
+ orientation: pair.orientation
13625
+ };
13626
+ while (true) {
13627
+ const next = { ...testPair, row: testPair.row + 1 };
13628
+ if (!isValidPosition(next, testBoard)) break;
13629
+ testPair.row++;
13630
+ }
13631
+ lockPair(testPair, testBoard);
13632
+ applyGravityFull(testBoard, []);
13633
+ if (simulateDepth > 0) {
13634
+ const pg = detectPowerGems(testBoard);
13635
+ const targets = findCrashTargets(testBoard, pg);
13636
+ if (targets.length > 0) {
13637
+ clearGems(testBoard, targets);
13638
+ applyGravityFull(testBoard, []);
13639
+ }
13640
+ }
13641
+ return evaluateBoard(testBoard);
13642
+ }
13643
+ function selectMove(player, ai) {
13644
+ const pair = player.currentPair;
13645
+ if (!pair) return;
13646
+ if (Math.random() < ai.difficulty.mistakeRate) {
13647
+ ai.targetCol = Math.floor(Math.random() * BOARD_COLS);
13648
+ ai.targetOrientation = Math.floor(Math.random() * 4);
13649
+ ai.decided = true;
13650
+ return;
13651
+ }
13652
+ const options = [];
13653
+ for (let orient = 0; orient < 4; orient++) {
13654
+ for (let col = 0; col < BOARD_COLS; col++) {
13655
+ const testPair = {
13656
+ primary: { ...pair.primary },
13657
+ secondary: { ...pair.secondary },
13658
+ col,
13659
+ row: 0,
13660
+ orientation: orient
13661
+ };
13662
+ if (!isValidPosition(testPair, player.board)) continue;
13663
+ const score = evaluatePlacement(testPair, player.board, ai.difficulty.simulateDepth);
13664
+ options.push({ col, orientation: orient, score });
13665
+ }
13666
+ }
13667
+ if (options.length === 0) {
13668
+ ai.targetCol = pair.col;
13669
+ ai.targetOrientation = pair.orientation;
13670
+ ai.decided = true;
13671
+ return;
13672
+ }
13673
+ options.sort((a, b) => b.score - a.score);
13674
+ const best = options[0];
13675
+ ai.targetCol = best.col;
13676
+ ai.targetOrientation = best.orientation;
13677
+ ai.decided = true;
13678
+ }
13679
+ function aiTick(player, ai) {
13680
+ if (!player.currentPair) return "none";
13681
+ ai.thinkTimer++;
13682
+ if (!ai.decided) {
13683
+ if (ai.thinkTimer >= ai.difficulty.thinkFrames) {
13684
+ selectMove(player, ai);
13685
+ ai.thinkTimer = 0;
13686
+ }
13687
+ return "none";
13688
+ }
13689
+ ai.moveTimer++;
13690
+ const moveInterval = Math.max(2, Math.floor(ai.difficulty.thinkFrames / 3));
13691
+ if (ai.moveTimer < moveInterval) return "none";
13692
+ ai.moveTimer = 0;
13693
+ const pair = player.currentPair;
13694
+ if (pair.orientation !== ai.targetOrientation) {
13695
+ const cwDist = (ai.targetOrientation - pair.orientation + 4) % 4;
13696
+ const ccwDist = (pair.orientation - ai.targetOrientation + 4) % 4;
13697
+ return cwDist <= ccwDist ? "rotate_cw" : "rotate_ccw";
13698
+ }
13699
+ if (pair.col < ai.targetCol) {
13700
+ return "move";
13701
+ } else if (pair.col > ai.targetCol) {
13702
+ return "move";
13703
+ }
13704
+ return "drop";
13705
+ }
13706
+ function getAIMoveDirection(player, ai) {
13707
+ if (!player.currentPair) return 0;
13708
+ if (player.currentPair.col < ai.targetCol) return 1;
13709
+ if (player.currentPair.col > ai.targetCol) return -1;
13710
+ return 0;
13711
+ }
13712
+
13713
+ // src/games/hyper-fighter/effects.ts
13714
+ var GEM_ANSI = {
13715
+ red: "\x1B[1;38;5;196m",
13716
+ green: "\x1B[1;38;5;46m",
13717
+ blue: "\x1B[1;38;5;27m",
13718
+ yellow: "\x1B[1;38;5;226m"
13719
+ };
13720
+ var PARTICLE_CHARS = ["\u2726", "\u2605", "\u25C6", "\u25CF", "\xB7", "*", "\u25AA"];
13721
+ function spawnClearParticles(x, y, color, count, particles) {
13722
+ const ansi = GEM_ANSI[color];
13723
+ for (let i = 0; i < count; i++) {
13724
+ particles.push({
13725
+ x: x + (Math.random() - 0.5) * 2,
13726
+ y: y + (Math.random() - 0.5),
13727
+ char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
13728
+ color: ansi,
13729
+ vx: (Math.random() - 0.5) * 3,
13730
+ vy: (Math.random() - 0.8) * 2,
13731
+ life: 8 + Math.floor(Math.random() * 8),
13732
+ maxLife: 16
13733
+ });
13734
+ }
13735
+ }
13736
+ function spawnFirework2(x, y, particles) {
13737
+ const colors = ["\x1B[91m", "\x1B[93m", "\x1B[92m", "\x1B[96m", "\x1B[95m"];
13738
+ for (let i = 0; i < 20; i++) {
13739
+ const angle = Math.PI * 2 * i / 20;
13740
+ const speed = 1.5 + Math.random();
13741
+ particles.push({
13742
+ x,
13743
+ y,
13744
+ char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
13745
+ color: colors[Math.floor(Math.random() * colors.length)],
13746
+ vx: Math.cos(angle) * speed,
13747
+ vy: Math.sin(angle) * speed * 0.5,
13748
+ life: 12 + Math.floor(Math.random() * 8),
13749
+ maxLife: 20
13750
+ });
13751
+ }
13752
+ }
13753
+ function spawnCollapse(startX, startY, width, height, particles) {
13754
+ for (let r = 0; r < height; r++) {
13755
+ for (let c = 0; c < width; c++) {
13756
+ if (Math.random() < 0.4) {
13757
+ particles.push({
13758
+ x: startX + c * 2,
13759
+ y: startY + r,
13760
+ char: "\u2593",
13761
+ color: "\x1B[90m",
13762
+ vx: (Math.random() - 0.5) * 0.5,
13763
+ vy: 0.3 + Math.random() * 0.5,
13764
+ life: 10 + Math.floor(Math.random() * 15),
13765
+ maxLife: 25
13766
+ });
13767
+ }
13768
+ }
13769
+ }
13770
+ }
13771
+ function updateParticles2(particles) {
13772
+ for (let i = particles.length - 1; i >= 0; i--) {
13773
+ const p = particles[i];
13774
+ p.x += p.vx;
13775
+ p.y += p.vy;
13776
+ p.vy += 0.08;
13777
+ p.life--;
13778
+ if (p.life <= 0) {
13779
+ particles.splice(i, 1);
13780
+ }
13781
+ }
13782
+ }
13783
+ function renderParticles(particles, minX, minY, maxX, maxY) {
13784
+ let output = "";
13785
+ for (const p of particles) {
13786
+ const px = Math.round(p.x);
13787
+ const py = Math.round(p.y);
13788
+ if (px < minX || px > maxX || py < minY || py > maxY) continue;
13789
+ const fade = p.life > p.maxLife * 0.5 ? "\x1B[1m" : "\x1B[2m";
13790
+ output += `\x1B[${py};${px}H${fade}${p.color}${p.char}\x1B[0m`;
13791
+ }
13792
+ return output;
13793
+ }
13794
+ var COMBO_MESSAGES = [
13795
+ { text: "NICE!", color: "\x1B[92m" },
13796
+ { text: "GREAT!", color: "\x1B[93m" },
13797
+ { text: "AMAZING!", color: "\x1B[96m" },
13798
+ { text: "UNSTOPPABLE!", color: "\x1B[95m" },
13799
+ { text: "GODLIKE!!", color: "\x1B[91m" },
13800
+ { text: "GODLIKE!!", color: "\x1B[1;91m" }
13801
+ ];
13802
+ function spawnComboText(chains, x, y, texts) {
13803
+ const idx = Math.min(chains - 1, COMBO_MESSAGES.length - 1);
13804
+ const msg = COMBO_MESSAGES[idx];
13805
+ texts.push({
13806
+ text: `${msg.text} \xD7${chains}`,
13807
+ x: x - Math.floor(msg.text.length / 2),
13808
+ y,
13809
+ color: msg.color,
13810
+ frames: 20,
13811
+ maxFrames: 20
13812
+ });
13813
+ }
13814
+ function spawnChainCounter(chains, x, y, texts) {
13815
+ texts.push({
13816
+ text: `${chains} CHAIN`,
13817
+ x: x - 3,
13818
+ y: y + 1,
13819
+ color: "\x1B[1;97m",
13820
+ frames: 16,
13821
+ maxFrames: 16
13822
+ });
13823
+ }
13824
+ function updateFloatingTexts(texts) {
13825
+ for (let i = texts.length - 1; i >= 0; i--) {
13826
+ const t = texts[i];
13827
+ t.frames--;
13828
+ if (t.frames % 3 === 0) t.y -= 1;
13829
+ if (t.frames <= 0) {
13830
+ texts.splice(i, 1);
13831
+ }
13832
+ }
13833
+ }
13834
+ function renderFloatingTexts(texts) {
13835
+ let output = "";
13836
+ for (const t of texts) {
13837
+ if (t.y < 1) continue;
13838
+ const fade = t.frames > t.maxFrames * 0.4 ? "\x1B[1m" : "\x1B[2m";
13839
+ output += `\x1B[${t.y};${Math.max(1, t.x)}H${fade}${t.color}${t.text}\x1B[0m`;
13840
+ }
13841
+ return output;
13842
+ }
13843
+ function spawnProjectile(fromX, toX, y, count, projectiles) {
13844
+ let char;
13845
+ let color;
13846
+ if (count >= 8) {
13847
+ char = "\u25C8";
13848
+ color = "\x1B[1;91m";
13849
+ } else if (count >= 4) {
13850
+ char = "\u25C6";
13851
+ color = "\x1B[1;93m";
13852
+ } else {
13853
+ char = "\u25CF";
13854
+ color = "\x1B[1;97m";
13855
+ }
13856
+ projectiles.push({ fromX, toX, y, progress: 0, char, color, count });
13857
+ }
13858
+ function updateProjectiles(projectiles) {
13859
+ for (let i = projectiles.length - 1; i >= 0; i--) {
13860
+ const p = projectiles[i];
13861
+ p.progress += 1 / 7;
13862
+ if (p.progress >= 1) {
13863
+ projectiles.splice(i, 1);
13864
+ }
13865
+ }
13866
+ }
13867
+ function renderProjectiles(projectiles) {
13868
+ let output = "";
13869
+ for (const p of projectiles) {
13870
+ const x = Math.round(p.fromX + (p.toX - p.fromX) * p.progress);
13871
+ if (x < 1) continue;
13872
+ output += `\x1B[${p.y};${x}H${p.color}${p.char}\x1B[0m`;
13873
+ }
13874
+ return output;
13875
+ }
13876
+ function renderPortrait(lines, x, y, color) {
13877
+ let output = "";
13878
+ for (let i = 0; i < lines.length; i++) {
13879
+ output += `\x1B[${y + i};${x}H${color}${lines[i]}\x1B[0m`;
13880
+ }
13881
+ return output;
13882
+ }
13883
+ function triggerShake(shake, intensity, frames) {
13884
+ shake.intensity = Math.max(shake.intensity, intensity);
13885
+ shake.frames = Math.max(shake.frames, frames);
13886
+ }
13887
+ function updateShake(shake) {
13888
+ if (shake.frames <= 0) return { dx: 0, dy: 0 };
13889
+ shake.frames--;
13890
+ if (shake.frames <= 0) {
13891
+ shake.intensity = 0;
13892
+ return { dx: 0, dy: 0 };
13893
+ }
13894
+ return {
13895
+ dx: Math.round((Math.random() - 0.5) * shake.intensity * 2),
13896
+ dy: Math.round((Math.random() - 0.5) * shake.intensity)
13897
+ };
13898
+ }
13899
+ function triggerFlash(flash, color, frames) {
13900
+ flash.color = color;
13901
+ flash.frames = frames;
13902
+ }
13903
+ function updateFlash(flash) {
13904
+ if (flash.frames <= 0) return null;
13905
+ flash.frames--;
13906
+ return flash.color;
13907
+ }
13908
+ function renderEnergyBar(x, y, level, maxLevel) {
13909
+ const barHeight = 6;
13910
+ const filled = Math.min(barHeight, Math.round(level / Math.max(1, maxLevel) * barHeight));
13911
+ let output = "";
13912
+ for (let i = 0; i < barHeight; i++) {
13913
+ const isFilled = i >= barHeight - filled;
13914
+ const char = isFilled ? "\u2588" : "\u2591";
13915
+ const color = isFilled ? filled >= barHeight - 1 ? "\x1B[91m" : filled >= barHeight / 2 ? "\x1B[93m" : "\x1B[92m" : "\x1B[90m";
13916
+ output += `\x1B[${y + i};${x}H${color}${char}\x1B[0m`;
13917
+ }
13918
+ return output;
13919
+ }
13920
+
13921
+ // src/games/hyper-fighter/characters.ts
13922
+ var R = "red";
13923
+ var G = "green";
13924
+ var B = "blue";
13925
+ var Y = "yellow";
13926
+ var ryu = {
13927
+ id: "ryu",
13928
+ name: "Ryu",
13929
+ description: "Vertical columns",
13930
+ damageModifier: 1,
13931
+ dropPattern: [
13932
+ [R, G, B, Y, R, G],
13933
+ [R, G, B, Y, R, G],
13934
+ [R, G, B, Y, R, G],
13935
+ [R, G, B, Y, R, G]
13936
+ ],
13937
+ portraits: {
13938
+ idle: [" __ ", " (-_-)", " /| "],
13939
+ attack: [" _\\__ ", " (>o<)", " =|/ "],
13940
+ hit: [" __ ", " (x_x)", " /| "],
13941
+ win: [" \\__/ ", " (^o^)", " /\\ "],
13942
+ lose: [" __ ", " (;_;)", " | "]
13943
+ }
13944
+ };
13945
+ var ken = {
13946
+ id: "ken",
13947
+ name: "Ken",
13948
+ description: "Horizontal rows",
13949
+ damageModifier: 1,
13950
+ dropPattern: [
13951
+ [Y, Y, Y, Y, Y, Y],
13952
+ [G, G, G, G, G, G],
13953
+ [B, B, B, B, B, B],
13954
+ [R, R, R, R, R, R]
13955
+ ],
13956
+ portraits: {
13957
+ idle: [" ^^^ ", " [>_>]", " /| "],
13958
+ attack: [" ^^^/ ", " [>o<]", " /=| "],
13959
+ hit: [" ^^^ ", " [x_x]", " /| "],
13960
+ win: [" \\^^/ ", " [^o^]", " /\\ "],
13961
+ lose: [" ^^^ ", " [;_;]", " | "]
13962
+ }
13963
+ };
13964
+ var chunLi = {
13965
+ id: "chunli",
13966
+ name: "Chun-Li",
13967
+ description: "2x2 color blocks",
13968
+ damageModifier: 1.2,
13969
+ dropPattern: [
13970
+ [R, R, G, G, B, B],
13971
+ [R, R, G, G, B, B],
13972
+ [Y, Y, R, R, G, G],
13973
+ [Y, Y, R, R, G, G]
13974
+ ],
13975
+ portraits: {
13976
+ idle: [" @ @ ", " {^.^}", " /| "],
13977
+ attack: [" @ @\\", " {>.<}", " /|= "],
13978
+ hit: [" @ @ ", " {x.x}", " /| "],
13979
+ win: ["\\@ @/", " {^o^}", " /\\ "],
13980
+ lose: [" @ @ ", " {;.;}", " | "]
13981
+ }
13982
+ };
13983
+ var sakura = {
13984
+ id: "sakura",
13985
+ name: "Sakura",
13986
+ description: "Fixed edges, alt middle",
13987
+ damageModifier: 1,
13988
+ dropPattern: [
13989
+ [G, R, B, R, B, Y],
13990
+ [G, B, R, B, R, Y],
13991
+ [G, R, B, R, B, Y],
13992
+ [G, B, R, B, R, Y]
13993
+ ],
13994
+ portraits: {
13995
+ idle: [" >> ", " <*_*>", " /| "],
13996
+ attack: [" >>/ ", " <*o*>", " /|= "],
13997
+ hit: [" >> ", " <x_x>", " /| "],
13998
+ win: [" \\>>/ ", " <^o^>", " /\\ "],
13999
+ lose: [" >> ", " <;_;>", " | "]
14000
+ }
14001
+ };
14002
+ var morrigan = {
14003
+ id: "morrigan",
14004
+ name: "Morrigan",
14005
+ description: "Symmetric mirrored",
14006
+ damageModifier: 1,
14007
+ dropPattern: [
14008
+ [B, G, R, R, G, B],
14009
+ [G, B, R, R, B, G],
14010
+ [R, G, B, B, G, R],
14011
+ [G, R, B, B, R, G]
14012
+ ],
14013
+ portraits: {
14014
+ idle: [" ~ ~ ", " ~^_^~", " /| "],
14015
+ attack: [" ~/~\\ ", " ~>_<~", " /| "],
14016
+ hit: [" ~ ~ ", " ~x_x~", " /| "],
14017
+ win: ["\\~ ~/", " ~^o~ ", " /\\ "],
14018
+ lose: [" ~ ~ ", " ~;_;~", " | "]
14019
+ }
14020
+ };
14021
+ var hsienKo = {
14022
+ id: "hsienKo",
14023
+ name: "Hsien-Ko",
14024
+ description: "Diagonal staircase",
14025
+ damageModifier: 1,
14026
+ dropPattern: [
14027
+ [R, G, B, Y, R, G],
14028
+ [G, B, Y, R, G, B],
14029
+ [B, Y, R, G, B, Y],
14030
+ [Y, R, G, B, Y, R]
14031
+ ],
14032
+ portraits: {
14033
+ idle: [" == ", " |o_o|", " /| "],
14034
+ attack: [" ==\\ ", " |o_o|", " /|= "],
14035
+ hit: [" == ", " |x_x|", " /| "],
14036
+ win: [" \\==/ ", " |^o^|", " /\\ "],
14037
+ lose: [" == ", " |;_;|", " | "]
14038
+ }
14039
+ };
14040
+ var felicia = {
14041
+ id: "felicia",
14042
+ name: "Felicia",
14043
+ description: "Fixed edges, swap mid",
14044
+ damageModifier: 1,
14045
+ dropPattern: [
14046
+ [G, R, B, R, B, Y],
14047
+ [G, B, R, B, R, Y],
14048
+ [Y, R, B, R, B, G],
14049
+ [Y, B, R, B, R, G]
14050
+ ],
14051
+ portraits: {
14052
+ idle: [" /\\/\\ ", " =^w^=", " /| "],
14053
+ attack: [" /\\/\\\\", " =^o^=", " /|= "],
14054
+ hit: [" /\\/\\ ", " =x_x=", " /| "],
14055
+ win: ["\\/\\/\\/", " =^w^=", " /\\ "],
14056
+ lose: [" /\\/\\ ", " =;w;=", " | "]
14057
+ }
14058
+ };
14059
+ var donovan = {
14060
+ id: "donovan",
14061
+ name: "Donovan",
14062
+ description: "3-col halves + alt base",
14063
+ damageModifier: 1,
14064
+ dropPattern: [
14065
+ [R, R, R, G, G, G],
14066
+ [R, R, R, G, G, G],
14067
+ [B, B, B, Y, Y, Y],
14068
+ [R, G, B, R, G, B]
14069
+ ],
14070
+ portraits: {
14071
+ idle: [" || ", " #-_-#", " /| "],
14072
+ attack: [" ||/ ", " #>_<#", " /|= "],
14073
+ hit: [" || ", " #x_x#", " /| "],
14074
+ win: [" \\||/ ", " #^o^#", " /\\ "],
14075
+ lose: [" || ", " #;_;#", " | "]
14076
+ }
14077
+ };
14078
+ var dan = {
14079
+ id: "dan",
14080
+ name: "Dan",
14081
+ description: "ALL RED (joke char)",
14082
+ damageModifier: 1,
14083
+ dropPattern: [
14084
+ [R, R, R, R, R, R],
14085
+ [R, R, R, R, R, R],
14086
+ [R, R, R, R, R, R],
14087
+ [R, R, R, R, R, R]
14088
+ ],
14089
+ portraits: {
14090
+ idle: [" ^^ ", " (?_?)", " /| "],
14091
+ attack: [" ^^! ", " (!o!)", " /|~ "],
14092
+ hit: [" ^^ ", " (x_x)", " /| "],
14093
+ win: [" \\^^/ ", " (^o^)", " /\\ "],
14094
+ lose: [" ^^ ", " (T_T)", " | "]
14095
+ }
14096
+ };
14097
+ var akuma = {
14098
+ id: "akuma",
14099
+ name: "Akuma",
14100
+ description: "Diagonal rainbow cycle",
14101
+ damageModifier: 0.7,
14102
+ dropPattern: [
14103
+ [R, Y, B, G, R, Y],
14104
+ [Y, B, G, R, Y, B],
14105
+ [B, G, R, Y, B, G],
14106
+ [G, R, Y, B, G, R]
14107
+ ],
14108
+ portraits: {
14109
+ idle: [" /MM\\ ", " !>_<!", " /| "],
14110
+ attack: [" /MM\\|", " !>o<!", " =/| "],
14111
+ hit: [" /MM\\ ", " !x_x!", " /| "],
14112
+ win: ["\\/MM\\/", " !^_^!", " /\\ "],
14113
+ lose: [" /MM\\ ", " !;_;!", " | "]
14114
+ }
14115
+ };
14116
+ var devilotte = {
14117
+ id: "devilotte",
14118
+ name: "Devilotte",
14119
+ description: "Reverse diagonal rainbow",
14120
+ damageModifier: 0.7,
14121
+ dropPattern: [
14122
+ [G, B, Y, R, G, B],
14123
+ [B, Y, R, G, B, Y],
14124
+ [Y, R, G, B, Y, R],
14125
+ [R, G, B, Y, R, G]
14126
+ ],
14127
+ portraits: {
14128
+ idle: [" vVv ", " $v_v$", " /| "],
14129
+ attack: [" vVv\\", " $>_<$", " /|= "],
14130
+ hit: [" vVv ", " $x_x$", " /| "],
14131
+ win: [" \\vVv/", " $^_^$", " /\\ "],
14132
+ lose: [" vVv ", " $;_;$", " | "]
14133
+ }
14134
+ };
14135
+ var CHARACTERS = [
14136
+ ryu,
14137
+ ken,
14138
+ chunLi,
14139
+ sakura,
14140
+ morrigan,
14141
+ hsienKo,
14142
+ felicia,
14143
+ donovan,
14144
+ dan,
14145
+ akuma,
14146
+ devilotte
14147
+ ];
14148
+ var CHAR_GRID = [
14149
+ [0, 1, 2, 3],
14150
+ [4, 5, 6, 7],
14151
+ [8, 9, 10]
14152
+ ];
14153
+ function getRandomCharacter() {
14154
+ return CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
14155
+ }
14156
+
14157
+ // src/games/hyper-fighter/index.ts
14158
+ var TICK_MS = 50;
14159
+ var MIN_COLS = 60;
14160
+ var MIN_ROWS = 36;
14161
+ var VS_COL_WIDTH = 12;
14162
+ var SIDE_PANEL_WIDTH = 14;
14163
+ var HEADER_HEIGHT = 2;
14164
+ var FOOTER_HEIGHT = 2;
14165
+ var MIN_CELL_WIDTH = 3;
14166
+ var MAX_CELL_WIDTH = 6;
14167
+ var MIN_CELL_HEIGHT = 2;
14168
+ var MAX_CELL_HEIGHT = 4;
14169
+ var GEM_COLORS = {
14170
+ red: "\x1B[1;38;5;196m",
14171
+ green: "\x1B[1;38;5;46m",
14172
+ blue: "\x1B[1;38;5;27m",
14173
+ yellow: "\x1B[1;38;5;226m"
14174
+ };
14175
+ var COUNTER_BG_COLORS = {
14176
+ red: "\x1B[48;5;196m",
14177
+ green: "\x1B[48;5;34m",
14178
+ blue: "\x1B[48;5;27m",
14179
+ yellow: "\x1B[48;5;178m"
14180
+ };
14181
+ var BASE_DROP_SPEED = 16;
14182
+ var MIN_DROP_SPEED = 3;
14183
+ var SPEED_RAMP_DROPS = 8;
14184
+ var PHASE_NONE = 0;
14185
+ var PHASE_FLASH = 1;
14186
+ var PHASE_DISSOLVE = 2;
14187
+ var PHASE_GRAVITY = 3;
14188
+ var PHASE_CHECK = 4;
14189
+ var PHASE_GARBAGE = 5;
14190
+ var FLASH_FRAMES = 6;
14191
+ var DISSOLVE_FRAMES = 4;
14192
+ var GRAVITY_FRAMES = 1;
14193
+ var COUNTER_TIMER_NORMAL = 5;
14194
+ var COUNTER_TIMER_DEFENDED = 3;
14195
+ var GARBAGE_DROP_STEP_FRAMES = 2;
14196
+ function runHyperFighterGame(terminal) {
14197
+ const themeColor = getCurrentThemeColor();
14198
+ let running = true;
14199
+ let gameState = "difficulty";
14200
+ let difficultySelection = 1;
14201
+ let pauseMenuSelection = 0;
14202
+ let p1;
14203
+ let p2;
14204
+ let aiState;
14205
+ let selectedDifficulty = DIFFICULTIES2.normal;
14206
+ let p1Character = CHARACTERS[0];
14207
+ let p2Character = getRandomCharacter();
14208
+ let charGridRow = 0;
14209
+ let charGridCol = 0;
14210
+ let p1DropTimer = 0;
14211
+ let p2DropTimer = 0;
14212
+ let p1DropSpeed = BASE_DROP_SPEED;
14213
+ let p2DropSpeed = BASE_DROP_SPEED;
14214
+ let p1Phase = PHASE_NONE;
14215
+ let p2Phase = PHASE_NONE;
14216
+ let p1PhaseTimer = 0;
14217
+ let p2PhaseTimer = 0;
14218
+ let p1ClearedCells = [];
14219
+ let p2ClearedCells = [];
14220
+ let p1ChainCount = 0;
14221
+ let p2ChainCount = 0;
14222
+ let p1TotalCleared = 0;
14223
+ let p2TotalCleared = 0;
14224
+ let p1GarbageDrop = null;
14225
+ let p2GarbageDrop = null;
14226
+ let p1GarbagePatternCursor = 0;
14227
+ let p2GarbagePatternCursor = 0;
14228
+ let p1IsDiamondClear = false;
14229
+ let p2IsDiamondClear = false;
14230
+ let p1AttackAccum = 0;
14231
+ let p2AttackAccum = 0;
14232
+ let particles = [];
14233
+ let floatingTexts = [];
14234
+ let projectiles = [];
14235
+ let p1Shake = { intensity: 0, frames: 0 };
14236
+ let p2Shake = { intensity: 0, frames: 0 };
14237
+ let p1Flash = { color: "", frames: 0 };
14238
+ let p2Flash = { color: "", frames: 0 };
14239
+ let p1Pose = "idle";
14240
+ let p2Pose = "idle";
14241
+ let p1PoseTimer = 0;
14242
+ let p2PoseTimer = 0;
14243
+ let winner = 0;
14244
+ let gameOverTimer = 0;
14245
+ let glitchFrame = 0;
14246
+ let cellWidth = 3;
14247
+ let cellHeight = 2;
14248
+ let boardDisplayWidth = BOARD_COLS * cellWidth + 2;
14249
+ let boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
14250
+ let cellEmpty = " ".repeat(cellWidth);
14251
+ let cellSolid = "\u2588".repeat(cellWidth);
14252
+ let cellPower = "\u2593".repeat(cellWidth);
14253
+ let cellGhost = "\u2591".repeat(cellWidth);
14254
+ let cellCrash = "\u25C6".repeat(cellWidth);
14255
+ let cellDiamond = "\u25C7".repeat(cellWidth);
14256
+ let cellEmptyDot = " ".repeat(Math.floor((cellWidth - 1) / 2)) + "\x1B[38;5;238m\xB7\x1B[0m" + " ".repeat(cellWidth - Math.floor((cellWidth - 1) / 2) - 1);
14257
+ let showSidePanels = false;
14258
+ let sidePanel1X = 0;
14259
+ let sidePanel2X = 0;
14260
+ let boardLeft1 = 2;
14261
+ let boardLeft2 = 28;
14262
+ let vsColX = 16;
14263
+ let boardTop = 3;
14264
+ function recalcCellStrings() {
14265
+ boardDisplayWidth = BOARD_COLS * cellWidth + 2;
14266
+ boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
14267
+ cellEmpty = " ".repeat(cellWidth);
14268
+ cellSolid = "\u2588".repeat(cellWidth);
14269
+ cellPower = "\u2593".repeat(cellWidth);
14270
+ cellGhost = "\u2591".repeat(cellWidth);
14271
+ cellCrash = "\u25C6".repeat(cellWidth);
14272
+ cellDiamond = "\u25C7".repeat(cellWidth);
14273
+ const padLeft = Math.floor((cellWidth - 1) / 2);
14274
+ cellEmptyDot = " ".repeat(padLeft) + "\xB7" + " ".repeat(cellWidth - padLeft - 1);
14275
+ }
14276
+ const controller = {
14277
+ stop: () => {
14278
+ if (!running) return;
14279
+ running = false;
14280
+ },
14281
+ get isRunning() {
14282
+ return running;
14283
+ }
14284
+ };
14285
+ const title = [
14286
+ "\u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588",
14287
+ "\u2588\u2580\u2588 \u2580\u2584\u2580 \u2588\u2580\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584",
14288
+ "\u2588\u2580\u2580 \u2588 \u2588\u2580\u2580 \u2588 \u2588 \u2580\u2588\u2580 \u2588\u2580\u2580 \u2588\u2580\u2588",
14289
+ "\u2588\u2580 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588 \u2588\u2588\u2584 \u2588\u2580\u2584"
14290
+ ];
14291
+ const GLITCH_CHARS2 = "!@#$%^&*\u2591\u2592\u2593\u2588\u2580\u2584";
14292
+ function initGame() {
14293
+ p1 = createPlayerState();
14294
+ p2 = createPlayerState();
14295
+ aiState = createAIState(selectedDifficulty);
14296
+ p1DropTimer = 0;
14297
+ p2DropTimer = 0;
14298
+ p1DropSpeed = BASE_DROP_SPEED;
14299
+ p2DropSpeed = BASE_DROP_SPEED;
14300
+ p1Phase = PHASE_NONE;
14301
+ p2Phase = PHASE_NONE;
14302
+ p1PhaseTimer = 0;
14303
+ p2PhaseTimer = 0;
14304
+ p1ClearedCells = [];
14305
+ p2ClearedCells = [];
14306
+ p1ChainCount = 0;
14307
+ p2ChainCount = 0;
14308
+ p1TotalCleared = 0;
14309
+ p2TotalCleared = 0;
14310
+ p1GarbageDrop = null;
14311
+ p2GarbageDrop = null;
14312
+ p1GarbagePatternCursor = 0;
14313
+ p2GarbagePatternCursor = 0;
14314
+ p1IsDiamondClear = false;
14315
+ p2IsDiamondClear = false;
14316
+ p1AttackAccum = 0;
14317
+ p2AttackAccum = 0;
14318
+ particles = [];
14319
+ floatingTexts = [];
14320
+ projectiles = [];
14321
+ p1Shake = { intensity: 0, frames: 0 };
14322
+ p2Shake = { intensity: 0, frames: 0 };
14323
+ p1Flash = { color: "", frames: 0 };
14324
+ p2Flash = { color: "", frames: 0 };
14325
+ p1Pose = "idle";
14326
+ p2Pose = "idle";
14327
+ p1PoseTimer = 0;
14328
+ p2PoseTimer = 0;
14329
+ winner = 0;
14330
+ gameOverTimer = 0;
14331
+ pauseMenuSelection = 0;
14332
+ spawnPair(p1);
14333
+ spawnPair(p2);
14334
+ }
14335
+ function calculateLayout() {
14336
+ const cols = terminal.cols;
14337
+ const rows = terminal.rows;
14338
+ cellHeight = Math.max(MIN_CELL_HEIGHT, Math.min(
14339
+ MAX_CELL_HEIGHT,
14340
+ Math.floor((rows - HEADER_HEIGHT - FOOTER_HEIGHT - 2 - 2) / BOARD_ROWS)
14341
+ ));
14342
+ const availWidthWithPanels = cols - 2 * SIDE_PANEL_WIDTH - VS_COL_WIDTH - 4;
14343
+ const availWidthWithout = cols - VS_COL_WIDTH - 4;
14344
+ const cwWithPanels = Math.floor(availWidthWithPanels / (2 * BOARD_COLS));
14345
+ const cwWithout = Math.floor(availWidthWithout / (2 * BOARD_COLS));
14346
+ if (cwWithPanels >= MIN_CELL_WIDTH) {
14347
+ showSidePanels = true;
14348
+ cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithPanels));
14349
+ } else {
14350
+ showSidePanels = false;
14351
+ cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithout));
14352
+ }
14353
+ const maxAspectWidth = Math.floor(cellHeight * 1.5);
14354
+ if (cellWidth > maxAspectWidth && maxAspectWidth >= MIN_CELL_WIDTH) {
14355
+ cellWidth = maxAspectWidth;
14356
+ }
14357
+ recalcCellStrings();
14358
+ if (showSidePanels) {
14359
+ const totalWidth = SIDE_PANEL_WIDTH + boardDisplayWidth + VS_COL_WIDTH + boardDisplayWidth + SIDE_PANEL_WIDTH;
14360
+ const startX = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
14361
+ sidePanel1X = startX;
14362
+ boardLeft1 = startX + SIDE_PANEL_WIDTH;
14363
+ vsColX = boardLeft1 + boardDisplayWidth + 1;
14364
+ boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
14365
+ sidePanel2X = boardLeft2 + boardDisplayWidth;
14366
+ } else {
14367
+ const totalWidth = boardDisplayWidth * 2 + VS_COL_WIDTH;
14368
+ boardLeft1 = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
14369
+ boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
14370
+ vsColX = boardLeft1 + boardDisplayWidth + 1;
14371
+ sidePanel1X = 0;
14372
+ sidePanel2X = 0;
14373
+ }
14374
+ const availHeight = rows - HEADER_HEIGHT - FOOTER_HEIGHT;
14375
+ boardTop = HEADER_HEIGHT + Math.max(1, Math.floor((availHeight - boardDisplayHeight) / 2));
14376
+ }
14377
+ function getDropSpeed(player) {
14378
+ const ramp = Math.floor(player.totalDrops / SPEED_RAMP_DROPS);
14379
+ return Math.max(MIN_DROP_SPEED, BASE_DROP_SPEED - ramp);
14380
+ }
14381
+ function startResolution(player, isP1) {
14382
+ if (isP1) p1AttackAccum = 0;
14383
+ else p2AttackAccum = 0;
14384
+ const diamondCleared = resolveDiamond(player.board);
14385
+ if (diamondCleared.length > 0) {
14386
+ applyGravityFull(player.board, player.powerGems);
14387
+ if (isP1) p1IsDiamondClear = true;
14388
+ else p2IsDiamondClear = true;
14389
+ } else {
14390
+ if (isP1) p1IsDiamondClear = false;
14391
+ else p2IsDiamondClear = false;
14392
+ }
14393
+ player.powerGems = detectPowerGems(player.board);
14394
+ const { cleared, clearedCells: crashCells, powerGemSizes } = resolveOneStep(player);
14395
+ const clearedCells = [...diamondCleared, ...crashCells];
14396
+ const totalStepCleared = cleared + diamondCleared.length;
14397
+ if (clearedCells.length === 0) {
14398
+ finishResolution(player, isP1);
14399
+ return;
14400
+ }
14401
+ if (isP1) {
14402
+ p1ClearedCells = clearedCells;
14403
+ p1ChainCount++;
14404
+ p1TotalCleared += totalStepCleared;
14405
+ p1Phase = PHASE_FLASH;
14406
+ p1PhaseTimer = FLASH_FRAMES;
14407
+ const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p1ChainCount };
14408
+ p1AttackAccum += calculateStepAttack(stepInfo);
14409
+ } else {
14410
+ p2ClearedCells = clearedCells;
14411
+ p2ChainCount++;
14412
+ p2TotalCleared += totalStepCleared;
14413
+ p2Phase = PHASE_FLASH;
14414
+ p2PhaseTimer = FLASH_FRAMES;
14415
+ const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p2ChainCount };
14416
+ p2AttackAccum += calculateStepAttack(stepInfo);
14417
+ }
14418
+ const bLeft = isP1 ? boardLeft1 : boardLeft2;
14419
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14420
+ if (chainCount >= 1) {
14421
+ spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
14422
+ if (chainCount >= 2) {
14423
+ spawnChainCounter(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 3, floatingTexts);
14424
+ }
14425
+ }
14426
+ for (const cell of clearedCells) {
14427
+ const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
14428
+ const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
14429
+ spawnClearParticles(px, py, cell.color, 3, particles);
14430
+ }
14431
+ const shakeAmount = Math.min(3, Math.ceil(clearedCells.length / 4));
14432
+ if (isP1) {
14433
+ triggerShake(p1Shake, shakeAmount, 6);
14434
+ triggerFlash(p1Flash, "\x1B[97m", 4);
14435
+ p1Pose = "attack";
14436
+ p1PoseTimer = 12;
14437
+ } else {
14438
+ triggerShake(p2Shake, shakeAmount, 6);
14439
+ triggerFlash(p2Flash, "\x1B[97m", 4);
14440
+ p2Pose = "attack";
14441
+ p2PoseTimer = 12;
14442
+ }
14443
+ }
14444
+ function resolveOneStep(player) {
14445
+ player.powerGems = detectPowerGems(player.board);
14446
+ const { targets, destroyedPowerGemSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14447
+ if (targets.length === 0) return { cleared: 0, chains: 0, clearedCells: [], powerGemSizes: [] };
14448
+ for (const t of targets) {
14449
+ player.board[t.row][t.col] = null;
14450
+ }
14451
+ applyGravityFull(player.board, player.powerGems);
14452
+ player.powerGems = [];
14453
+ return { cleared: targets.length, chains: 1, clearedCells: targets, powerGemSizes: destroyedPowerGemSizes };
14454
+ }
14455
+ function buildGarbageDropState(player, defendedCount, normalCount, startCursor, dropPattern) {
14456
+ const occupied = player.board.map((row) => row.map((cell) => cell !== null));
14457
+ const timers = [];
14458
+ for (let i = 0; i < defendedCount; i++) timers.push(COUNTER_TIMER_DEFENDED);
14459
+ for (let i = 0; i < normalCount; i++) timers.push(COUNTER_TIMER_NORMAL);
14460
+ const gems = [];
14461
+ const patternRows = dropPattern.length;
14462
+ const patternCols = dropPattern[0].length;
14463
+ const patternLen = patternRows * patternCols;
14464
+ let placedCount = 0;
14465
+ function findLandingRow(col) {
14466
+ for (let r = BOARD_ROWS - 1; r >= 0; r--) {
14467
+ if (!occupied[r][col]) return r;
14468
+ }
14469
+ return -1;
14470
+ }
14471
+ for (let i = 0; i < timers.length; i++) {
14472
+ const patternIdx = (startCursor + i) % patternLen;
14473
+ const pRow = Math.floor(patternIdx / patternCols);
14474
+ const pCol = patternIdx % patternCols;
14475
+ const color = dropPattern[pRow][pCol];
14476
+ let placeCol = -1;
14477
+ let placeRow = -1;
14478
+ const landing = findLandingRow(pCol);
14479
+ if (landing >= 0) {
14480
+ placeCol = pCol;
14481
+ placeRow = landing;
14482
+ } else {
14483
+ for (let offset = 1; offset < BOARD_COLS; offset++) {
14484
+ for (const dir of [1, -1]) {
14485
+ const adjCol = pCol + offset * dir;
14486
+ if (adjCol >= 0 && adjCol < BOARD_COLS) {
14487
+ const adjLanding = findLandingRow(adjCol);
14488
+ if (adjLanding >= 0) {
14489
+ placeCol = adjCol;
14490
+ placeRow = adjLanding;
14491
+ break;
14492
+ }
14493
+ }
14494
+ }
14495
+ if (placeRow >= 0) break;
14496
+ }
14497
+ }
14498
+ if (placeRow < 0 || placeCol < 0) continue;
14499
+ occupied[placeRow][placeCol] = true;
14500
+ gems.push({
14501
+ col: placeCol,
14502
+ targetRow: placeRow,
14503
+ currentRow: 0,
14504
+ timer: timers[i],
14505
+ color,
14506
+ delayFrames: i
14507
+ });
14508
+ placedCount++;
14509
+ }
14510
+ if (gems.length === 0) {
14511
+ return { dropState: null, nextCursor: startCursor };
14512
+ }
14513
+ const nextCursor = (startCursor + placedCount) % patternLen;
14514
+ return { dropState: { gems, frameTick: 0 }, nextCursor };
14515
+ }
14516
+ function triggerLoss(isP1) {
14517
+ const loser = isP1 ? p1 : p2;
14518
+ loser.alive = false;
14519
+ gameState = "gameOver";
14520
+ winner = isP1 ? 2 : 1;
14521
+ gameOverTimer = 0;
14522
+ pauseMenuSelection = 0;
14523
+ if (isP1) {
14524
+ p1Pose = "lose";
14525
+ p2Pose = "win";
14526
+ } else {
14527
+ p2Pose = "lose";
14528
+ p1Pose = "win";
14529
+ }
14530
+ const loserLeft = isP1 ? boardLeft1 : boardLeft2;
14531
+ const winnerLeft = isP1 ? boardLeft2 : boardLeft1;
14532
+ spawnCollapse(loserLeft, boardTop, boardDisplayWidth, BOARD_ROWS * cellHeight, particles);
14533
+ spawnFirework2(winnerLeft + boardDisplayWidth / 2, boardTop + 3, particles);
14534
+ }
14535
+ function finalizePostResolution(player, isP1) {
14536
+ if (checkGameOver(player.board)) {
14537
+ triggerLoss(isP1);
14538
+ return;
14539
+ }
14540
+ player.powerGems = detectPowerGems(player.board);
14541
+ if (!spawnPair(player)) {
14542
+ triggerLoss(isP1);
14543
+ }
14544
+ }
14545
+ function tickGarbageDrop(player, isP1) {
14546
+ const dropState = isP1 ? p1GarbageDrop : p2GarbageDrop;
14547
+ if (!dropState) {
14548
+ if (isP1) p1Phase = PHASE_NONE;
14549
+ else p2Phase = PHASE_NONE;
14550
+ finalizePostResolution(player, isP1);
14551
+ return;
14552
+ }
14553
+ dropState.frameTick++;
14554
+ if (dropState.frameTick < GARBAGE_DROP_STEP_FRAMES) return;
14555
+ dropState.frameTick = 0;
14556
+ let stillDropping = false;
14557
+ for (const gem of dropState.gems) {
14558
+ if (gem.delayFrames > 0) {
14559
+ gem.delayFrames--;
14560
+ stillDropping = true;
14561
+ continue;
14562
+ }
14563
+ if (gem.currentRow < gem.targetRow) {
14564
+ gem.currentRow++;
14565
+ stillDropping = true;
14566
+ }
14567
+ }
14568
+ if (stillDropping) return;
14569
+ for (const gem of dropState.gems) {
14570
+ if (player.board[gem.targetRow][gem.col] !== null) continue;
14571
+ player.board[gem.targetRow][gem.col] = {
14572
+ color: gem.color,
14573
+ type: "counter",
14574
+ counterTimer: gem.timer
14575
+ };
14576
+ }
14577
+ if (isP1) {
14578
+ p1GarbageDrop = null;
14579
+ p1Phase = PHASE_NONE;
14580
+ } else {
14581
+ p2GarbageDrop = null;
14582
+ p2Phase = PHASE_NONE;
14583
+ }
14584
+ finalizePostResolution(player, isP1);
14585
+ }
14586
+ function tickResolution(isP1) {
14587
+ const phase = isP1 ? p1Phase : p2Phase;
14588
+ const timer = isP1 ? p1PhaseTimer : p2PhaseTimer;
14589
+ const player = isP1 ? p1 : p2;
14590
+ if (phase === PHASE_NONE) return;
14591
+ if (timer > 0) {
14592
+ if (isP1) p1PhaseTimer--;
14593
+ else p2PhaseTimer--;
14594
+ return;
14595
+ }
14596
+ switch (phase) {
14597
+ case PHASE_FLASH:
14598
+ if (isP1) {
14599
+ p1Phase = PHASE_DISSOLVE;
14600
+ p1PhaseTimer = DISSOLVE_FRAMES;
14601
+ } else {
14602
+ p2Phase = PHASE_DISSOLVE;
14603
+ p2PhaseTimer = DISSOLVE_FRAMES;
14604
+ }
14605
+ break;
14606
+ case PHASE_DISSOLVE:
14607
+ if (isP1) {
14608
+ p1Phase = PHASE_GRAVITY;
14609
+ p1PhaseTimer = GRAVITY_FRAMES;
14610
+ } else {
14611
+ p2Phase = PHASE_GRAVITY;
14612
+ p2PhaseTimer = GRAVITY_FRAMES;
14613
+ }
14614
+ break;
14615
+ case PHASE_GRAVITY:
14616
+ if (isP1) {
14617
+ p1Phase = PHASE_CHECK;
14618
+ p1PhaseTimer = 0;
14619
+ } else {
14620
+ p2Phase = PHASE_CHECK;
14621
+ p2PhaseTimer = 0;
14622
+ }
14623
+ break;
14624
+ case PHASE_GARBAGE:
14625
+ tickGarbageDrop(player, isP1);
14626
+ break;
14627
+ case PHASE_CHECK: {
14628
+ player.powerGems = detectPowerGems(player.board);
14629
+ const { targets: more, destroyedPowerGemSizes: pgSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14630
+ if (more.length > 0) {
14631
+ for (const t of more) {
14632
+ player.board[t.row][t.col] = null;
14633
+ }
14634
+ applyGravityFull(player.board, player.powerGems);
14635
+ player.powerGems = [];
14636
+ if (isP1) {
14637
+ p1ClearedCells = more;
14638
+ p1ChainCount++;
14639
+ p1TotalCleared += more.length;
14640
+ p1Phase = PHASE_FLASH;
14641
+ p1PhaseTimer = FLASH_FRAMES;
14642
+ const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p1ChainCount };
14643
+ p1AttackAccum += calculateStepAttack(stepInfo);
14644
+ } else {
14645
+ p2ClearedCells = more;
14646
+ p2ChainCount++;
14647
+ p2TotalCleared += more.length;
14648
+ p2Phase = PHASE_FLASH;
14649
+ p2PhaseTimer = FLASH_FRAMES;
14650
+ const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p2ChainCount };
14651
+ p2AttackAccum += calculateStepAttack(stepInfo);
14652
+ }
14653
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14654
+ const bLeft = isP1 ? boardLeft1 : boardLeft2;
14655
+ spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
14656
+ for (const cell of more) {
14657
+ const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
14658
+ const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
14659
+ spawnClearParticles(px, py, cell.color, 4 + chainCount, particles);
14660
+ }
14661
+ triggerShake(isP1 ? p1Shake : p2Shake, Math.min(4, chainCount), 8);
14662
+ } else {
14663
+ const totalCleared = isP1 ? p1TotalCleared : p2TotalCleared;
14664
+ const chainCount = isP1 ? p1ChainCount : p2ChainCount;
14665
+ const dmgMod = isP1 ? p1Character.damageModifier : p2Character.damageModifier;
14666
+ const isDiamond = isP1 ? p1IsDiamondClear : p2IsDiamondClear;
14667
+ const rawAttack = isP1 ? p1AttackAccum : p2AttackAccum;
14668
+ let attack = applyAttackModifiers(rawAttack, dmgMod, isDiamond);
14669
+ if (attack > 0 && player.pendingGarbage > 0) {
14670
+ const defense = resolveCounterAttack(attack, player.pendingGarbage);
14671
+ attack = defense.remainingAttack;
14672
+ player.pendingGarbage = defense.remainingPending;
14673
+ player.pendingCounteredGarbage = defense.pendingStartsAtThree ? defense.remainingPending : 0;
14674
+ if (defense.canceledGems > 0) {
14675
+ const left = isP1 ? boardLeft1 : boardLeft2;
14676
+ floatingTexts.push({
14677
+ text: `DEFENSE -${defense.canceledGems}`,
14678
+ x: left + 2,
14679
+ y: Math.max(1, boardTop - 1),
14680
+ color: "\x1B[96m",
14681
+ frames: 18,
14682
+ maxFrames: 18
14683
+ });
14684
+ }
14685
+ }
14686
+ if (attack > 0) {
14687
+ const opponent = isP1 ? p2 : p1;
14688
+ opponent.pendingGarbage += attack;
14689
+ const fromBLeft = isP1 ? boardLeft1 : boardLeft2;
14690
+ const toBLeft = isP1 ? boardLeft2 : boardLeft1;
14691
+ spawnProjectile(
14692
+ fromBLeft + boardDisplayWidth / 2,
14693
+ toBLeft + boardDisplayWidth / 2,
14694
+ boardTop + Math.floor(boardDisplayHeight / 2),
14695
+ attack,
14696
+ projectiles
14697
+ );
14698
+ if (isP1) {
14699
+ p2Pose = "hit";
14700
+ p2PoseTimer = 10;
14701
+ } else {
14702
+ p1Pose = "hit";
14703
+ p1PoseTimer = 10;
14704
+ }
14705
+ triggerFlash(isP1 ? p2Flash : p1Flash, "\x1B[91m", 6);
14706
+ triggerShake(isP1 ? p2Shake : p1Shake, Math.min(3, Math.ceil(attack / 2)), 6);
14707
+ }
14708
+ player.score += totalCleared * 10 + (chainCount > 1 ? chainCount * 50 : 0);
14709
+ finishResolution(player, isP1);
14710
+ }
14711
+ break;
14712
+ }
14713
+ }
14714
+ }
14715
+ function finishResolution(player, isP1) {
14716
+ if (isP1) {
14717
+ p1Phase = PHASE_NONE;
14718
+ p1PhaseTimer = 0;
14719
+ p1ChainCount = 0;
14720
+ p1TotalCleared = 0;
14721
+ p1ClearedCells = [];
14722
+ p1AttackAccum = 0;
14723
+ } else {
14724
+ p2Phase = PHASE_NONE;
14725
+ p2PhaseTimer = 0;
14726
+ p2ChainCount = 0;
14727
+ p2TotalCleared = 0;
14728
+ p2ClearedCells = [];
14729
+ p2AttackAccum = 0;
14730
+ }
14731
+ decrementCounters(player);
14732
+ player.powerGems = detectPowerGems(player.board);
14733
+ const { targets: postCounterTargets } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
14734
+ if (postCounterTargets.length > 0) {
14735
+ startResolution(player, isP1);
14736
+ return;
14737
+ }
14738
+ if (player.pendingGarbage > 0) {
14739
+ const incomingCount = player.pendingGarbage;
14740
+ const defendedCount = Math.min(player.pendingCounteredGarbage, player.pendingGarbage);
14741
+ const normalCount = player.pendingGarbage - defendedCount;
14742
+ const cursor = isP1 ? p1GarbagePatternCursor : p2GarbagePatternCursor;
14743
+ const attackerPattern = isP1 ? p2Character.dropPattern : p1Character.dropPattern;
14744
+ const { dropState, nextCursor } = buildGarbageDropState(player, defendedCount, normalCount, cursor, attackerPattern);
14745
+ if (isP1) p1GarbagePatternCursor = nextCursor;
14746
+ else p2GarbagePatternCursor = nextCursor;
14747
+ player.pendingGarbage = 0;
14748
+ player.pendingCounteredGarbage = 0;
14749
+ const left = isP1 ? boardLeft1 : boardLeft2;
14750
+ floatingTexts.push({
14751
+ text: `DROP +${incomingCount}`,
14752
+ x: left + 2,
14753
+ y: Math.max(1, boardTop - 1),
14754
+ color: "\x1B[1;91m",
14755
+ frames: 20,
14756
+ maxFrames: 20
14757
+ });
14758
+ if (dropState) {
14759
+ if (isP1) {
14760
+ p1GarbageDrop = dropState;
14761
+ p1Phase = PHASE_GARBAGE;
14762
+ p1PhaseTimer = 0;
14763
+ } else {
14764
+ p2GarbageDrop = dropState;
14765
+ p2Phase = PHASE_GARBAGE;
14766
+ p2PhaseTimer = 0;
14767
+ }
14768
+ return;
14769
+ }
14770
+ }
14771
+ finalizePostResolution(player, isP1);
14772
+ }
14773
+ function update() {
14774
+ if (gameState !== "running") return;
14775
+ glitchFrame++;
14776
+ updateParticles2(particles);
14777
+ updateFloatingTexts(floatingTexts);
14778
+ updateProjectiles(projectiles);
14779
+ if (p1PoseTimer > 0) {
14780
+ p1PoseTimer--;
14781
+ if (p1PoseTimer <= 0) p1Pose = "idle";
14782
+ }
14783
+ if (p2PoseTimer > 0) {
14784
+ p2PoseTimer--;
14785
+ if (p2PoseTimer <= 0) p2Pose = "idle";
14786
+ }
14787
+ if (p1Phase !== PHASE_NONE) {
14788
+ tickResolution(true);
14789
+ }
14790
+ if (p2Phase !== PHASE_NONE) {
14791
+ tickResolution(false);
14792
+ }
14793
+ if (p1Phase === PHASE_NONE && p1.currentPair && p1.alive) {
14794
+ p1DropSpeed = getDropSpeed(p1);
14795
+ p1DropTimer++;
14796
+ if (p1DropTimer >= p1DropSpeed) {
14797
+ p1DropTimer = 0;
14798
+ if (!dropPair(p1.currentPair, p1.board)) {
14799
+ lockAndResolve(p1, true);
14800
+ }
14801
+ }
14802
+ }
14803
+ if (p2Phase === PHASE_NONE && p2.currentPair && p2.alive) {
14804
+ p2DropSpeed = Math.max(MIN_DROP_SPEED, Math.floor(getDropSpeed(p2) / aiState.difficulty.dropSpeedBoost));
14805
+ const action = aiTick(p2, aiState);
14806
+ switch (action) {
14807
+ case "rotate_cw":
14808
+ rotatePair(p2.currentPair, p2.board, true);
14809
+ break;
14810
+ case "rotate_ccw":
14811
+ rotatePair(p2.currentPair, p2.board, false);
14812
+ break;
14813
+ case "move": {
14814
+ const dir = getAIMoveDirection(p2, aiState);
14815
+ if (dir !== 0) movePair(p2.currentPair, p2.board, dir);
14816
+ break;
14817
+ }
14818
+ case "drop":
14819
+ if (!dropPair(p2.currentPair, p2.board)) {
14820
+ lockAndResolve(p2, false);
14821
+ }
14822
+ break;
14823
+ }
14824
+ p2DropTimer++;
14825
+ if (p2DropTimer >= p2DropSpeed) {
14826
+ p2DropTimer = 0;
14827
+ if (p2.currentPair && !dropPair(p2.currentPair, p2.board)) {
14828
+ lockAndResolve(p2, false);
14829
+ }
14830
+ }
14831
+ }
14832
+ }
14833
+ function lockAndResolve(player, isP1) {
14834
+ if (!player.currentPair) return;
14835
+ lockPair(player.currentPair, player.board);
14836
+ player.currentPair = null;
14837
+ applyGravityFull(player.board, player.powerGems);
14838
+ if (!isP1) {
14839
+ aiState.decided = false;
14840
+ aiState.thinkTimer = 0;
14841
+ aiState.moveTimer = 0;
14842
+ }
14843
+ startResolution(player, isP1);
14844
+ }
14845
+ function render() {
14846
+ if (!running) return;
14847
+ const cols = terminal.cols;
14848
+ const rows = terminal.rows;
14849
+ if (cols < MIN_COLS || rows < MIN_ROWS) {
14850
+ let output2 = "\x1B[2J\x1B[H";
14851
+ output2 += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor(cols / 2) - 10)}H`;
14852
+ output2 += `${themeColor}Need ${MIN_COLS}\xD7${MIN_ROWS} (have ${cols}\xD7${rows})\x1B[0m`;
14853
+ terminal.write(output2);
14854
+ return;
14855
+ }
14856
+ calculateLayout();
14857
+ let output = "\x1B[2J\x1B[H";
14858
+ switch (gameState) {
14859
+ case "difficulty":
14860
+ output += renderDifficultyScreen();
14861
+ break;
14862
+ case "characterSelect":
14863
+ output += renderCharacterSelectScreen();
14864
+ break;
14865
+ case "running":
14866
+ output += renderGame();
14867
+ break;
14868
+ case "paused":
14869
+ output += renderGame();
14870
+ output += renderPauseOverlay();
14871
+ break;
14872
+ case "gameOver":
14873
+ output += renderGame({
14874
+ showEffects: false,
14875
+ showHud: false,
14876
+ showControls: false,
14877
+ showVs: false
14878
+ });
14879
+ output += renderGameOverOverlay();
14880
+ break;
14881
+ }
14882
+ terminal.write(output);
14883
+ }
14884
+ function renderDifficultyScreen() {
14885
+ const cols = terminal.cols;
14886
+ const rows = terminal.rows;
14887
+ const centerX = Math.floor(cols / 2);
14888
+ let output = "";
14889
+ const titleY = Math.max(2, Math.floor(rows / 2) - 8);
14890
+ for (let i = 0; i < title.length; i++) {
14891
+ let line = title[i];
14892
+ if (Math.random() < 0.15) {
14893
+ const pos = Math.floor(Math.random() * line.length);
14894
+ const glitchChar = GLITCH_CHARS2[Math.floor(Math.random() * GLITCH_CHARS2.length)];
14895
+ line = line.substring(0, pos) + glitchChar + line.substring(pos + 1);
14896
+ }
14897
+ const x = Math.max(1, centerX - Math.floor(line.length / 2));
14898
+ const color = i < 2 ? themeColor : "\x1B[93m";
14899
+ output += `\x1B[${titleY + i};${x}H${color}\x1B[1m${line}\x1B[0m`;
14900
+ }
14901
+ const subtitle = "\u2554\u2550\u2550 GEM BATTLE VS AI \u2550\u2550\u2557";
14902
+ const subX = Math.max(1, centerX - Math.floor(subtitle.length / 2));
14903
+ output += `\x1B[${titleY + title.length + 1};${subX}H${themeColor}${subtitle}\x1B[0m`;
14904
+ const diffY = titleY + title.length + 4;
14905
+ const diffs = ["easy", "normal", "hard"];
14906
+ const diffLabels = ["EASY", "NORMAL", "HARD"];
14907
+ const diffDescs = ["Relaxed pace, AI makes mistakes", "Balanced challenge", "Fast & ruthless AI"];
14908
+ for (let i = 0; i < diffs.length; i++) {
14909
+ const isSelected = i === difficultySelection;
14910
+ const label = `[${i + 1}] ${diffLabels[i]}`;
14911
+ const text = isSelected ? `\u25BA ${label} \u25C4` : ` ${label} `;
14912
+ const style = isSelected ? "\x1B[1;93m" : `\x1B[2m${themeColor}`;
14913
+ const x = Math.max(1, centerX - Math.floor(text.length / 2));
14914
+ output += `\x1B[${diffY + i * 2};${x}H${style}${text}\x1B[0m`;
14915
+ if (isSelected) {
14916
+ const descX = Math.max(1, centerX - Math.floor(diffDescs[i].length / 2));
14917
+ output += `\x1B[${diffY + i * 2 + 1};${descX}H\x1B[2m${themeColor}${diffDescs[i]}\x1B[0m`;
14918
+ }
14919
+ }
14920
+ const controlsY = diffY + 8;
14921
+ const controls = "\u2191\u2193 Select Enter Confirm Q Quit";
14922
+ const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
14923
+ output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
14924
+ const refY = controlsY + 2;
14925
+ const refLines = [
14926
+ "\u2190\u2192/AD Move \u2191/W Rotate Z Counter-rotate",
14927
+ "\u2193/S Soft drop Space Hard drop ESC Pause"
14928
+ ];
14929
+ for (let i = 0; i < refLines.length; i++) {
14930
+ const rx = Math.max(1, centerX - Math.floor(refLines[i].length / 2));
14931
+ output += `\x1B[${refY + i};${rx}H\x1B[2m\x1B[90m${refLines[i]}\x1B[0m`;
14932
+ }
14933
+ return output;
14934
+ }
14935
+ const PATTERN_PREVIEW_COLORS = {
14936
+ red: "\x1B[1;38;5;196m",
14937
+ green: "\x1B[1;38;5;46m",
14938
+ blue: "\x1B[1;38;5;27m",
14939
+ yellow: "\x1B[1;38;5;226m"
14940
+ };
14941
+ function renderCharacterSelectScreen() {
14942
+ const cols = terminal.cols;
14943
+ const rows = terminal.rows;
14944
+ const centerX = Math.floor(cols / 2);
14945
+ let output = "";
14946
+ const titleY = Math.max(2, Math.floor(rows / 2) - 14);
14947
+ const selectTitle = "\u2554\u2550\u2550 SELECT YOUR FIGHTER \u2550\u2550\u2557";
14948
+ const stX = Math.max(1, centerX - Math.floor(selectTitle.length / 2));
14949
+ output += `\x1B[${titleY};${stX}H${themeColor}\x1B[1m${selectTitle}\x1B[0m`;
14950
+ const badge = `[${selectedDifficulty.name}]`;
14951
+ const bX = Math.max(1, centerX - Math.floor(badge.length / 2));
14952
+ output += `\x1B[${titleY + 1};${bX}H\x1B[2m${themeColor}${badge}\x1B[0m`;
14953
+ const gridY = titleY + 3;
14954
+ const cellW = 11;
14955
+ const selectedIdx = CHAR_GRID[charGridRow][charGridCol];
14956
+ for (let row = 0; row < CHAR_GRID.length; row++) {
14957
+ const rowChars = CHAR_GRID[row];
14958
+ const rowWidth = rowChars.length * cellW;
14959
+ const rowStartX = Math.max(1, centerX - Math.floor(rowWidth / 2));
14960
+ for (let col = 0; col < rowChars.length; col++) {
14961
+ const charIdx = rowChars[col];
14962
+ const char2 = CHARACTERS[charIdx];
14963
+ const isSelected = charIdx === selectedIdx;
14964
+ const x = rowStartX + col * cellW;
14965
+ const y = gridY + row * 3;
14966
+ const name = char2.name.slice(0, cellW - 2).padEnd(cellW - 2);
14967
+ if (isSelected) {
14968
+ output += `\x1B[${y};${x}H\x1B[1;93m\u25BA${name}\u25C4\x1B[0m`;
14969
+ } else {
14970
+ output += `\x1B[${y};${x}H${themeColor} ${name} \x1B[0m`;
14971
+ }
14972
+ const dmgStr = char2.damageModifier !== 1 ? `${Math.round(char2.damageModifier * 100)}%` : " ";
14973
+ const dmgColor = char2.damageModifier > 1 ? "\x1B[92m" : char2.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
14974
+ output += `\x1B[${y + 1};${x + 1}H${dmgColor}${dmgStr}\x1B[0m`;
14975
+ }
14976
+ }
14977
+ const char = CHARACTERS[selectedIdx];
14978
+ const detailY = gridY + CHAR_GRID.length * 3 + 1;
14979
+ const charTitle = `${char.name} \u2014 ${char.description}`;
14980
+ const ctX = Math.max(1, centerX - Math.floor(charTitle.length / 2));
14981
+ output += `\x1B[${detailY};${ctX}H\x1B[1;97m${charTitle}\x1B[0m`;
14982
+ 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)`;
14983
+ const dmgLabelColor = char.damageModifier > 1 ? "\x1B[92m" : char.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
14984
+ const dlX = Math.max(1, centerX - Math.floor(dmgLabel.length / 2));
14985
+ output += `\x1B[${detailY + 1};${dlX}H${dmgLabelColor}${dmgLabel}\x1B[0m`;
14986
+ const previewWidth = 6 * 2;
14987
+ const previewX = Math.max(1, centerX - Math.floor(previewWidth / 2));
14988
+ const previewY = detailY + 3;
14989
+ output += `\x1B[${previewY - 1};${previewX}H\x1B[2m${themeColor}Drop Pattern:\x1B[0m`;
14990
+ for (let pr = 0; pr < char.dropPattern.length; pr++) {
14991
+ let rowStr = "";
14992
+ for (let pc = 0; pc < char.dropPattern[pr].length; pc++) {
14993
+ const color = char.dropPattern[pr][pc];
14994
+ rowStr += `${PATTERN_PREVIEW_COLORS[color]}\u2588\u2588\x1B[0m`;
14995
+ }
14996
+ output += `\x1B[${previewY + pr};${previewX}H${rowStr}`;
14997
+ }
14998
+ const portraitX = previewX + previewWidth + 3;
14999
+ const portraitLines = char.portraits.idle;
15000
+ output += renderPortrait(portraitLines, portraitX, previewY, themeColor);
15001
+ const controlsY = previewY + char.dropPattern.length + 2;
15002
+ const controls = "\u2190\u2192\u2191\u2193 Select Enter Confirm Esc Back";
15003
+ const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
15004
+ output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
15005
+ return output;
15006
+ }
15007
+ function renderGame(options) {
15008
+ const config = {
15009
+ showEffects: options?.showEffects ?? true,
15010
+ showHud: options?.showHud ?? true,
15011
+ showControls: options?.showControls ?? true,
15012
+ showVs: options?.showVs ?? true
15013
+ };
15014
+ let output = "";
15015
+ output += renderHeaderBar();
15016
+ const s1 = updateShake(p1Shake);
15017
+ const s2 = updateShake(p2Shake);
15018
+ output += renderBoard(p1, boardLeft1 + s1.dx, boardTop + s1.dy, true, true, p1Phase, p1PhaseTimer, p1ClearedCells, p1Flash);
15019
+ output += renderBoard(p2, boardLeft2 + s2.dx, boardTop + s2.dy, false, false, p2Phase, p2PhaseTimer, p2ClearedCells, p2Flash);
15020
+ if (config.showVs) {
15021
+ output += renderVSColumn();
15022
+ }
15023
+ if (showSidePanels && config.showHud) {
15024
+ output += renderSidePanel(true);
15025
+ output += renderSidePanel(false);
15026
+ }
15027
+ if (!showSidePanels) {
15028
+ output += `\x1B[${boardTop - 1};${boardLeft1 + 2}H${themeColor}${p1Character.name}\x1B[0m`;
15029
+ output += `\x1B[${boardTop - 1};${boardLeft2 + 2}H\x1B[91m${p2Character.name}\x1B[0m`;
15030
+ output += renderNextStrip(p1, boardLeft1, Math.max(1, boardTop - 2), themeColor);
15031
+ output += renderNextStrip(p2, boardLeft2, Math.max(1, boardTop - 2), "\x1B[91m");
15032
+ if (config.showHud) {
15033
+ const panelY = boardTop + boardDisplayHeight + 1;
15034
+ output += renderScorePanel(
15035
+ boardLeft1,
15036
+ panelY,
15037
+ p1.score,
15038
+ p1.pendingGarbage,
15039
+ themeColor,
15040
+ `S${Math.round(BASE_DROP_SPEED / Math.max(1, p1DropSpeed) * 100)}%`
15041
+ );
15042
+ output += renderScorePanel(
15043
+ boardLeft2,
15044
+ panelY,
15045
+ p2.score,
15046
+ p2.pendingGarbage,
15047
+ "\x1B[1;38;5;203m",
15048
+ "AI"
15049
+ );
15050
+ }
15051
+ }
15052
+ if (config.showEffects) {
15053
+ output += renderParticles(particles, 1, 1, terminal.cols, terminal.rows);
15054
+ output += renderFloatingTexts(floatingTexts);
15055
+ output += renderProjectiles(projectiles);
15056
+ }
15057
+ if (config.showControls) {
15058
+ output += renderFooterBar();
15059
+ }
15060
+ return output;
15061
+ }
15062
+ function renderNextStrip(player, left, y, accent) {
15063
+ if (!player.nextPair) return "";
15064
+ let output = "";
15065
+ output += `\x1B[${y};${left}H${accent}\x1B[1mNEXT\x1B[0m `;
15066
+ output += renderGemCell(player.nextPair.primary, false, false);
15067
+ output += renderGemCell(player.nextPair.secondary, false, false);
15068
+ output += ` \x1B[2m${accent}\u25CF\u25CF\u25CF\x1B[0m`;
15069
+ return output;
15070
+ }
15071
+ function renderScorePanel(left, y, score, pending, accent, tag) {
15072
+ const { levelLabel, levelColor } = getIncomingThreatLevel(pending);
15073
+ const meterWidth = 5;
15074
+ const filled = Math.min(meterWidth, pending);
15075
+ const meter = `${"\u25A0".repeat(filled)}${"\xB7".repeat(meterWidth - filled)}`;
15076
+ const scoreLine = `SCORE ${score.toString().padStart(6, "0")} ${tag}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
15077
+ const statusLine = `IN${pending.toString().padStart(2, "0")} ${meter} ${levelLabel}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
15078
+ let output = "";
15079
+ output += `\x1B[${y};${left}H${accent}\x1B[48;5;237m${scoreLine}\x1B[0m`;
15080
+ output += `\x1B[${y + 1};${left}H${levelColor}\x1B[48;5;235m${statusLine}\x1B[0m`;
15081
+ return output;
15082
+ }
15083
+ function getIncomingThreatLevel(pending) {
15084
+ if (pending <= 0) return { levelLabel: "CLEAR", levelColor: "\x1B[92m" };
15085
+ if (pending <= 2) return { levelLabel: "LOW", levelColor: "\x1B[93m" };
15086
+ if (pending <= 5) return { levelLabel: "HIGH", levelColor: "\x1B[91m" };
15087
+ return { levelLabel: "DANGER", levelColor: "\x1B[1;91m" };
15088
+ }
15089
+ function renderBoard(player, left, top, isP1Board, showGhost, phase, phaseTimer, clearedCells, flash) {
15090
+ let output = "";
15091
+ const board = player.board;
15092
+ const pair = player.currentPair;
15093
+ const flashColor = updateFlash(flash);
15094
+ const borderColor = flashColor || themeColor;
15095
+ output += `\x1B[${top};${left}H${borderColor}\u2554${"\u2550".repeat(BOARD_COLS * cellWidth)}\u2557\x1B[0m`;
15096
+ const clearedSet = new Set(clearedCells.map((c) => `${c.row},${c.col}`));
15097
+ let ghost = null;
15098
+ if (showGhost && pair) {
15099
+ ghost = getGhostPosition(pair, board);
15100
+ }
15101
+ for (let r = 0; r < BOARD_ROWS; r++) {
15102
+ let rowContent0 = "";
15103
+ let rowContentN = "";
15104
+ for (let c = 0; c < BOARD_COLS; c++) {
15105
+ const gem = board[r][c];
15106
+ const isCleared = clearedSet.has(`${r},${c}`);
15107
+ let pairGem = null;
15108
+ if (pair) {
15109
+ if (pair.row === r && pair.col === c) pairGem = pair.primary;
15110
+ const sec = getSecondaryPos(pair);
15111
+ if (sec.row === r && sec.col === c) pairGem = pair.secondary;
15112
+ }
15113
+ if (pairGem) {
15114
+ const cell = renderGemCell(pairGem, false, false);
15115
+ rowContent0 += cell;
15116
+ rowContentN += cell;
15117
+ } else if (isCleared && phase === PHASE_FLASH) {
15118
+ const cell = clearedCells.find((cc) => cc.row === r && cc.col === c);
15119
+ let s;
15120
+ if (cell && phaseTimer % 2 === 0) {
15121
+ s = `\x1B[1;97m${cellSolid}\x1B[0m`;
15122
+ } else {
15123
+ s = `${GEM_COLORS[cell?.color || "red"]}${cellSolid}\x1B[0m`;
15124
+ }
15125
+ rowContent0 += s;
15126
+ rowContentN += s;
15127
+ } else if (isCleared && phase === PHASE_DISSOLVE) {
15128
+ const stage = DISSOLVE_FRAMES - phaseTimer;
15129
+ let s;
15130
+ if (stage === 0) s = `\x1B[2m${cellPower}\x1B[0m`;
15131
+ else if (stage === 1) s = `\x1B[2m${cellGhost}\x1B[0m`;
15132
+ else s = cellEmpty;
15133
+ rowContent0 += s;
15134
+ rowContentN += s;
15135
+ } else if (gem) {
15136
+ const inPowerGem = gem.powerGemId !== void 0;
15137
+ const cell = renderGemCell(gem, inPowerGem, false);
15138
+ rowContent0 += cell;
15139
+ rowContentN += cell;
15140
+ } else {
15141
+ let isGhost = false;
15142
+ if (ghost) {
15143
+ if (ghost.primaryRow === r && ghost.primaryCol === c || ghost.secondaryRow === r && ghost.secondaryCol === c) {
15144
+ isGhost = true;
15145
+ }
15146
+ }
15147
+ if (isGhost) {
15148
+ const g = `\x1B[2;90m${cellGhost}\x1B[0m`;
15149
+ rowContent0 += g;
15150
+ rowContentN += g;
15151
+ } else {
15152
+ rowContent0 += `\x1B[38;5;238m${cellEmptyDot}\x1B[0m`;
15153
+ rowContentN += cellEmpty;
15154
+ }
15155
+ }
15156
+ }
15157
+ for (let h = 0; h < cellHeight; h++) {
15158
+ const rowY = top + 1 + r * cellHeight + h;
15159
+ output += `\x1B[${rowY};${left}H${borderColor}\u2551\x1B[0m`;
15160
+ output += h === 0 ? rowContent0 : rowContentN;
15161
+ output += `${borderColor}\u2551\x1B[0m`;
15162
+ }
15163
+ }
15164
+ output += `\x1B[${top + boardDisplayHeight - 1};${left}H${borderColor}\u255A${"\u2550".repeat(BOARD_COLS * cellWidth)}\u255D\x1B[0m`;
15165
+ const garbageDrop = isP1Board ? p1GarbageDrop : p2GarbageDrop;
15166
+ if (garbageDrop) {
15167
+ for (const g of garbageDrop.gems) {
15168
+ if (g.delayFrames > 0) continue;
15169
+ const row = Math.max(0, Math.min(BOARD_ROWS - 1, g.currentRow));
15170
+ const cellStr = renderGemCell({ color: g.color, type: "counter", counterTimer: g.timer }, false, false);
15171
+ for (let h = 0; h < cellHeight; h++) {
15172
+ const y = top + 1 + row * cellHeight + h;
15173
+ const x = left + 1 + g.col * cellWidth;
15174
+ output += `\x1B[${y};${x}H${cellStr}`;
15175
+ }
15176
+ }
15177
+ }
15178
+ return output;
15179
+ }
15180
+ function renderGemCell(gem, isPowerGem, _dimmed) {
15181
+ const color = GEM_COLORS[gem.color];
15182
+ switch (gem.type) {
15183
+ case "normal":
15184
+ if (isPowerGem) {
15185
+ return `${color}${cellPower}\x1B[0m`;
15186
+ }
15187
+ return `${color}${cellSolid}\x1B[0m`;
15188
+ case "crash":
15189
+ return `\x1B[1m${color}${cellCrash}\x1B[0m`;
15190
+ case "counter": {
15191
+ const bg = COUNTER_BG_COLORS[gem.color];
15192
+ if (gem.counterTimer !== void 0) {
15193
+ const timerStr = gem.counterTimer.toString().padStart(2, " ");
15194
+ return `\x1B[1;97m${bg}${timerStr.padEnd(cellWidth, " ")}\x1B[0m`;
15195
+ }
15196
+ return `\x1B[1;97m${bg}${" ?".padEnd(cellWidth, " ")}\x1B[0m`;
15197
+ }
15198
+ case "diamond":
15199
+ return `\x1B[1;97m${cellDiamond}\x1B[0m`;
15200
+ default:
15201
+ return `${color}${cellSolid}\x1B[0m`;
15202
+ }
15203
+ }
15204
+ function renderVSColumn() {
15205
+ let output = "";
15206
+ const x = vsColX;
15207
+ const y = boardTop + Math.max(2, Math.floor(boardDisplayHeight / 2) - 5);
15208
+ output += `\x1B[${Math.max(1, y - 2)};${x + 1}H\x1B[1;96mHYPER\x1B[0m`;
15209
+ output += `\x1B[${Math.max(1, y - 1)};${x + 1}H\x1B[1;95mFIGHT\x1B[0m`;
15210
+ output += `\x1B[${y};${x + 2}H\x1B[1;93mVS\x1B[0m`;
15211
+ const p1Lines = p1Character.portraits[p1Pose];
15212
+ output += renderPortrait(p1Lines, x, y + 2, themeColor);
15213
+ const p2Lines = p2Character.portraits[p2Pose];
15214
+ output += renderPortrait(p2Lines, x, y + 6, "\x1B[91m");
15215
+ const maxEnergy = 10;
15216
+ const p1Energy = p1ChainCount * 3 + p1TotalCleared;
15217
+ output += renderEnergyBar(x + 8, y + 2, p1Energy, maxEnergy);
15218
+ return output;
15219
+ }
15220
+ function renderHeaderBar() {
15221
+ const cols = terminal.cols;
15222
+ let output = "";
15223
+ const leftText = " HYPER FIGHTER";
15224
+ const rightText = `${p1Character.name} vs ${p2Character.name} [${selectedDifficulty.name}] `;
15225
+ const padLen = Math.max(0, cols - leftText.length - rightText.length);
15226
+ const row1 = leftText + " ".repeat(padLen) + rightText;
15227
+ output += `\x1B[1;1H\x1B[1;97m\x1B[48;5;236m${row1.slice(0, cols).padEnd(cols, " ")}\x1B[0m`;
15228
+ output += `\x1B[2;1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
15229
+ return output;
15230
+ }
15231
+ function renderFooterBar() {
15232
+ const cols = terminal.cols;
15233
+ const rows = terminal.rows;
15234
+ let output = "";
15235
+ output += `\x1B[${rows - 1};1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
15236
+ const controls = "\u2190\u2192 Move \u2191/W Rotate Z CCW \u2193/S Soft Space Drop ESC Pause";
15237
+ const cx = Math.max(1, Math.floor((cols - controls.length) / 2) + 1);
15238
+ output += `\x1B[${rows};${cx}H\x1B[2m\x1B[90m${controls}\x1B[0m`;
15239
+ return output;
15240
+ }
15241
+ function renderMiniBar(value, max, width, fillColor) {
15242
+ const filled = Math.min(width, Math.round(value / Math.max(1, max) * width));
15243
+ const empty = width - filled;
15244
+ return `${fillColor}${"\u25A0".repeat(filled)}\x1B[38;5;238m${"\xB7".repeat(empty)}\x1B[0m`;
15245
+ }
15246
+ function renderSidePanel(isLeft) {
15247
+ const player = isLeft ? p1 : p2;
15248
+ const x = isLeft ? sidePanel1X : sidePanel2X;
15249
+ const accent = isLeft ? themeColor : "\x1B[1;38;5;203m";
15250
+ const dropSpeed = isLeft ? p1DropSpeed : p2DropSpeed;
15251
+ const chainCount = isLeft ? p1ChainCount : p2ChainCount;
15252
+ let output = "";
15253
+ let y = boardTop + 1;
15254
+ output += `\x1B[${y};${x}H${accent}\x1B[1mNEXT\x1B[0m`;
15255
+ y++;
15256
+ if (player.nextPair) {
15257
+ output += `\x1B[${y};${x}H`;
15258
+ output += renderGemCell(player.nextPair.primary, false, false);
15259
+ output += renderGemCell(player.nextPair.secondary, false, false);
15260
+ }
15261
+ y += 2;
15262
+ output += `\x1B[${y};${x}H\x1B[38;5;245mSCORE\x1B[0m`;
15263
+ y++;
15264
+ output += `\x1B[${y};${x}H${accent}${player.score.toString().padStart(7, "0")}\x1B[0m`;
15265
+ y += 2;
15266
+ output += `\x1B[${y};${x}H\x1B[38;5;245mSPEED\x1B[0m`;
15267
+ y++;
15268
+ const speedPct = Math.round(BASE_DROP_SPEED / Math.max(1, dropSpeed) * 100);
15269
+ output += `\x1B[${y};${x}H${speedPct >= 200 ? "\x1B[91m" : speedPct >= 150 ? "\x1B[93m" : "\x1B[92m"}${speedPct}%\x1B[0m `;
15270
+ output += renderMiniBar(speedPct, 300, 6, speedPct >= 200 ? "\x1B[91m" : "\x1B[92m");
15271
+ y += 2;
15272
+ if (chainCount > 0) {
15273
+ output += `\x1B[${y};${x}H\x1B[38;5;245mCHAIN\x1B[0m`;
15274
+ y++;
15275
+ output += `\x1B[${y};${x}H\x1B[1;93m${chainCount}\x1B[0m`;
15276
+ y += 2;
15277
+ } else {
15278
+ y += 3;
15279
+ }
15280
+ const { levelLabel, levelColor } = getIncomingThreatLevel(player.pendingGarbage);
15281
+ output += `\x1B[${y};${x}H\x1B[38;5;245mINCOMING\x1B[0m`;
15282
+ y++;
15283
+ output += `\x1B[${y};${x}H${levelColor}${player.pendingGarbage.toString().padStart(2, "0")}\x1B[0m `;
15284
+ output += renderMiniBar(player.pendingGarbage, 10, 6, levelColor);
15285
+ y++;
15286
+ output += `\x1B[${y};${x}H${levelColor}${levelLabel}\x1B[0m`;
15287
+ return output;
15288
+ }
15289
+ function renderPauseOverlay() {
15290
+ const cols = terminal.cols;
15291
+ const rows = terminal.rows;
15292
+ const centerX = Math.floor(cols / 2);
15293
+ const centerY = Math.floor(rows / 2);
15294
+ let output = "";
15295
+ output += `\x1B[${centerY - 3};${centerX - 4}H\x1B[1;5m${themeColor}\u23F8 PAUSED\x1B[0m`;
15296
+ output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
15297
+ centerX,
15298
+ startY: centerY - 1,
15299
+ showShortcuts: true
15300
+ });
15301
+ return output;
15302
+ }
15303
+ function renderGameOverOverlay() {
15304
+ const cols = terminal.cols;
15305
+ const rows = terminal.rows;
15306
+ const centerX = Math.floor(cols / 2);
15307
+ const centerY = Math.floor(rows / 2);
15308
+ let output = "";
15309
+ gameOverTimer++;
15310
+ const panelWidth = Math.min(44, Math.max(34, cols - 8));
15311
+ const panelHeight = 12;
15312
+ const panelLeft = Math.max(2, centerX - Math.floor(panelWidth / 2));
15313
+ const panelTop = Math.max(2, centerY - Math.floor(panelHeight / 2));
15314
+ for (let y = 0; y < panelHeight; y++) {
15315
+ output += `\x1B[${panelTop + y};${panelLeft}H\x1B[40m${" ".repeat(panelWidth)}\x1B[0m`;
15316
+ }
15317
+ output += `\x1B[${panelTop};${panelLeft}H${themeColor}\u2554${"\u2550".repeat(panelWidth - 2)}\u2557\x1B[0m`;
15318
+ for (let y = 1; y < panelHeight - 1; y++) {
15319
+ output += `\x1B[${panelTop + y};${panelLeft}H${themeColor}\u2551\x1B[0m`;
15320
+ output += `\x1B[${panelTop + y};${panelLeft + panelWidth - 1}H${themeColor}\u2551\x1B[0m`;
15321
+ }
15322
+ output += `\x1B[${panelTop + panelHeight - 1};${panelLeft}H${themeColor}\u255A${"\u2550".repeat(panelWidth - 2)}\u255D\x1B[0m`;
15323
+ const winnerChar = winner === 1 ? p1Character : p2Character;
15324
+ const winText = winner === 1 ? `${winnerChar.name} WINS!` : `${winnerChar.name} WINS!`;
15325
+ const winColor = winner === 1 ? "\x1B[1;92m" : "\x1B[1;91m";
15326
+ const winX = Math.max(1, centerX - Math.floor(winText.length / 2));
15327
+ output += `\x1B[${panelTop + 2};${winX}H${winColor}${winText}\x1B[0m`;
15328
+ const scoreLine = `${p1Character.name}: ${p1.score} | ${p2Character.name}: ${p2.score}`;
15329
+ const sX = Math.max(1, centerX - Math.floor(scoreLine.length / 2));
15330
+ output += `\x1B[${panelTop + 4};${sX}H${themeColor}${scoreLine}\x1B[0m`;
15331
+ const menuItems = [
15332
+ { label: "RESTART", shortcut: "R" },
15333
+ { label: "QUIT", shortcut: "Q" },
15334
+ { label: "LIST GAMES", shortcut: "L" },
15335
+ { label: "NEXT GAME", shortcut: "N" }
15336
+ ];
15337
+ output += renderSimpleMenu(menuItems, pauseMenuSelection, {
15338
+ centerX,
15339
+ startY: panelTop + 6,
15340
+ showShortcuts: true
15341
+ });
15342
+ return output;
15343
+ }
15344
+ const keyListener = terminal.onKey(({ domEvent }) => {
15345
+ if (!running) return;
15346
+ domEvent.preventDefault();
15347
+ domEvent.stopPropagation();
15348
+ const key = domEvent.key.toLowerCase();
15349
+ switch (gameState) {
15350
+ case "difficulty":
15351
+ handleDifficultyInput(key, domEvent);
15352
+ break;
15353
+ case "characterSelect":
15354
+ handleCharacterSelectInput(key, domEvent);
15355
+ break;
15356
+ case "running":
15357
+ handleGameInput(key, domEvent);
15358
+ break;
15359
+ case "paused":
15360
+ handlePauseInput(key, domEvent);
15361
+ break;
15362
+ case "gameOver":
15363
+ handleGameOverInput(key, domEvent);
15364
+ break;
15365
+ }
15366
+ });
15367
+ function handleDifficultyInput(key, domEvent) {
15368
+ if (domEvent.key === "ArrowUp" || key === "w") {
15369
+ difficultySelection = (difficultySelection - 1 + 3) % 3;
15370
+ } else if (domEvent.key === "ArrowDown" || key === "s") {
15371
+ difficultySelection = (difficultySelection + 1) % 3;
15372
+ } else if (domEvent.key === "Enter" || domEvent.key === " ") {
15373
+ const diffs = ["easy", "normal", "hard"];
15374
+ selectedDifficulty = DIFFICULTIES2[diffs[difficultySelection]];
15375
+ enterCharacterSelect();
15376
+ } else if (key === "1") {
15377
+ selectedDifficulty = DIFFICULTIES2.easy;
15378
+ difficultySelection = 0;
15379
+ enterCharacterSelect();
15380
+ } else if (key === "2") {
15381
+ selectedDifficulty = DIFFICULTIES2.normal;
15382
+ difficultySelection = 1;
15383
+ enterCharacterSelect();
15384
+ } else if (key === "3") {
15385
+ selectedDifficulty = DIFFICULTIES2.hard;
15386
+ difficultySelection = 2;
15387
+ enterCharacterSelect();
15388
+ } else if (key === "q") {
15389
+ cleanup();
15390
+ dispatchGameQuit(terminal);
15391
+ }
15392
+ }
15393
+ function enterCharacterSelect() {
15394
+ gameState = "characterSelect";
15395
+ charGridRow = 0;
15396
+ charGridCol = 0;
15397
+ }
15398
+ function handleCharacterSelectInput(key, domEvent) {
15399
+ if (key === "q" || key === "escape") {
15400
+ gameState = "difficulty";
15401
+ return;
15402
+ }
15403
+ if (domEvent.key === "ArrowLeft" || key === "a") {
15404
+ const row = CHAR_GRID[charGridRow];
15405
+ charGridCol = (charGridCol - 1 + row.length) % row.length;
15406
+ } else if (domEvent.key === "ArrowRight" || key === "d") {
15407
+ const row = CHAR_GRID[charGridRow];
15408
+ charGridCol = (charGridCol + 1) % row.length;
15409
+ } else if (domEvent.key === "ArrowUp" || key === "w") {
15410
+ charGridRow = (charGridRow - 1 + CHAR_GRID.length) % CHAR_GRID.length;
15411
+ charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
15412
+ } else if (domEvent.key === "ArrowDown" || key === "s") {
15413
+ charGridRow = (charGridRow + 1) % CHAR_GRID.length;
15414
+ charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
15415
+ } else if (domEvent.key === "Enter" || domEvent.key === " ") {
15416
+ const idx = CHAR_GRID[charGridRow][charGridCol];
15417
+ p1Character = CHARACTERS[idx];
15418
+ p2Character = getRandomCharacter();
15419
+ gameState = "running";
15420
+ initGame();
15421
+ }
15422
+ }
15423
+ function handleGameInput(key, domEvent) {
15424
+ if (key === "escape") {
15425
+ gameState = "paused";
15426
+ pauseMenuSelection = 0;
15427
+ return;
15428
+ }
15429
+ if (!p1.currentPair || p1Phase !== PHASE_NONE) return;
15430
+ switch (domEvent.key) {
15431
+ case "ArrowLeft":
15432
+ case "a":
15433
+ movePair(p1.currentPair, p1.board, -1);
15434
+ break;
15435
+ case "ArrowRight":
15436
+ case "d":
15437
+ movePair(p1.currentPair, p1.board, 1);
15438
+ break;
15439
+ case "ArrowUp":
15440
+ case "w":
15441
+ rotatePair(p1.currentPair, p1.board, true);
15442
+ break;
15443
+ case "z":
15444
+ rotatePair(p1.currentPair, p1.board, false);
15445
+ break;
15446
+ case "ArrowDown":
15447
+ case "s":
15448
+ if (dropPair(p1.currentPair, p1.board)) {
15449
+ p1DropTimer = 0;
15450
+ } else {
15451
+ lockAndResolve(p1, true);
15452
+ }
15453
+ break;
15454
+ case " ":
15455
+ hardDrop(p1.currentPair, p1.board);
15456
+ lockAndResolve(p1, true);
15457
+ break;
15458
+ }
15459
+ }
15460
+ function handlePauseInput(key, domEvent) {
15461
+ if (key === "escape") {
15462
+ gameState = "running";
15463
+ return;
15464
+ }
15465
+ const { newSelection, confirmed } = navigateMenu(
15466
+ pauseMenuSelection,
15467
+ PAUSE_MENU_ITEMS.length,
15468
+ key,
15469
+ domEvent
15470
+ );
15471
+ if (newSelection !== pauseMenuSelection) {
15472
+ pauseMenuSelection = newSelection;
15473
+ }
15474
+ const shortcutIdx = checkShortcut(PAUSE_MENU_ITEMS, key);
15475
+ if (confirmed || shortcutIdx >= 0) {
15476
+ const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
15477
+ const item = PAUSE_MENU_ITEMS[idx];
15478
+ switch (item.label) {
15479
+ case "RESUME":
15480
+ gameState = "running";
15481
+ break;
15482
+ case "RESTART":
15483
+ gameState = "running";
15484
+ initGame();
15485
+ break;
15486
+ case "QUIT":
15487
+ cleanup();
15488
+ dispatchGameQuit(terminal);
15489
+ break;
15490
+ case "LIST GAMES":
15491
+ cleanup();
15492
+ dispatchGamesMenu(terminal);
15493
+ break;
15494
+ case "NEXT GAME":
15495
+ cleanup();
15496
+ dispatchGameSwitch(terminal);
15497
+ break;
15498
+ }
15499
+ }
15500
+ }
15501
+ function handleGameOverInput(key, domEvent) {
15502
+ const menuItems = [
15503
+ { label: "RESTART", shortcut: "R" },
15504
+ { label: "QUIT", shortcut: "Q" },
15505
+ { label: "LIST GAMES", shortcut: "L" },
15506
+ { label: "NEXT GAME", shortcut: "N" }
15507
+ ];
15508
+ const { newSelection, confirmed } = navigateMenu(
15509
+ pauseMenuSelection,
15510
+ menuItems.length,
15511
+ key,
15512
+ domEvent
15513
+ );
15514
+ if (newSelection !== pauseMenuSelection) {
15515
+ pauseMenuSelection = newSelection;
15516
+ }
15517
+ const shortcutIdx = checkShortcut(menuItems, key);
15518
+ if (confirmed || shortcutIdx >= 0) {
15519
+ const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
15520
+ switch (menuItems[idx].label) {
15521
+ case "RESTART":
15522
+ gameState = "difficulty";
15523
+ pauseMenuSelection = 0;
15524
+ break;
15525
+ case "QUIT":
15526
+ cleanup();
15527
+ dispatchGameQuit(terminal);
15528
+ break;
15529
+ case "LIST GAMES":
15530
+ cleanup();
15531
+ dispatchGamesMenu(terminal);
15532
+ break;
15533
+ case "NEXT GAME":
15534
+ cleanup();
15535
+ dispatchGameSwitch(terminal);
15536
+ break;
15537
+ }
15538
+ }
15539
+ }
15540
+ const resizeListener = terminal.onResize(() => {
15541
+ calculateLayout();
15542
+ });
15543
+ function cleanup() {
15544
+ running = false;
15545
+ clearInterval(gameLoop);
15546
+ keyListener.dispose();
15547
+ resizeListener.dispose();
15548
+ }
15549
+ enterAlternateBuffer(terminal, "hyper-fighter");
15550
+ const gameLoop = setInterval(() => {
15551
+ if (!running) {
15552
+ clearInterval(gameLoop);
15553
+ return;
15554
+ }
15555
+ update();
15556
+ render();
15557
+ }, TICK_MS);
15558
+ const originalStop = controller.stop;
15559
+ controller.stop = () => {
15560
+ cleanup();
15561
+ exitAlternateBuffer(terminal, "hyper-fighter");
15562
+ originalStop();
15563
+ };
15564
+ return controller;
15565
+ }
15566
+
13081
15567
  // src/games/gamesMenu.ts
13082
15568
  function showGamesMenu(terminal, optionsOrCallback) {
13083
15569
  const options = typeof optionsOrCallback === "function" ? { onGameSelect: optionsOrCallback } : optionsOrCallback || {};
@@ -13097,8 +15583,8 @@ function showGamesMenu(terminal, optionsOrCallback) {
13097
15583
  }
13098
15584
  };
13099
15585
  const title = [
13100
- "\u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2580\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580",
13101
- "\u2588\u2584\u2588 \u2588\u2580\u2588 \u2588 \u2580 \u2588 \u2588\u2588\u2584 \u2584\u2588"
15586
+ "\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",
15587
+ "\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"
13102
15588
  ];
13103
15589
  function render() {
13104
15590
  let output = "";
@@ -13666,7 +16152,7 @@ function runRebootEffect(terminal) {
13666
16152
 
13667
16153
  // src/games/shared/effects.ts
13668
16154
  var MAX_PARTICLES2 = 100;
13669
- var PARTICLE_CHARS = {
16155
+ var PARTICLE_CHARS2 = {
13670
16156
  explosion: ["\u2717", "\xD7", "\xB7", "\u25CB", "\u2592", "\u2591"],
13671
16157
  success: ["\u2726", "\u2605", "\u25C6", "\u25CF", "\u2666"],
13672
16158
  fire: ["\u2593", "\u2592", "\u2591", "\u25CF", "\u25C6"],
@@ -13699,8 +16185,8 @@ function spawnParticles2(particles, x, y, count, color, chars = ["\u2726", "\u26
13699
16185
  });
13700
16186
  }
13701
16187
  }
13702
- function spawnFirework2(particles, x, y, intensity = 1) {
13703
- const chars = PARTICLE_CHARS.firework;
16188
+ function spawnFirework3(particles, x, y, intensity = 1) {
16189
+ const chars = PARTICLE_CHARS2.firework;
13704
16190
  for (let i = 0; i < 12 * intensity; i++) {
13705
16191
  const angle = Math.PI * 2 * i / (12 * intensity);
13706
16192
  const speed = 0.4 + Math.random() * 0.4;
@@ -13735,7 +16221,7 @@ function spawnSparkleTrail2(particles, x, y, count = 6, color = "\x1B[1;93m") {
13735
16221
  particles.push({
13736
16222
  x: x + (Math.random() - 0.5) * 4,
13737
16223
  y,
13738
- char: PARTICLE_CHARS.sparkle[Math.floor(Math.random() * PARTICLE_CHARS.sparkle.length)],
16224
+ char: PARTICLE_CHARS2.sparkle[Math.floor(Math.random() * PARTICLE_CHARS2.sparkle.length)],
13739
16225
  color,
13740
16226
  vx: (Math.random() - 0.5) * 0.2,
13741
16227
  vy: -0.3 - Math.random() * 0.3,
@@ -13743,7 +16229,7 @@ function spawnSparkleTrail2(particles, x, y, count = 6, color = "\x1B[1;93m") {
13743
16229
  });
13744
16230
  }
13745
16231
  }
13746
- function updateParticles2(particles, gravityMult = 1) {
16232
+ function updateParticles3(particles, gravityMult = 1) {
13747
16233
  for (let i = particles.length - 1; i >= 0; i--) {
13748
16234
  const p = particles[i];
13749
16235
  p.x += p.vx;
@@ -13767,7 +16253,7 @@ function updatePopups2(popups) {
13767
16253
  function createShakeState() {
13768
16254
  return { frames: 0, intensity: 0 };
13769
16255
  }
13770
- function triggerShake(state, frames, intensity) {
16256
+ function triggerShake2(state, frames, intensity) {
13771
16257
  state.frames = frames;
13772
16258
  state.intensity = intensity;
13773
16259
  }
@@ -13784,10 +16270,10 @@ function applyShake(state) {
13784
16270
  function createFlashState() {
13785
16271
  return { frames: 0 };
13786
16272
  }
13787
- function triggerFlash(state, frames) {
16273
+ function triggerFlash2(state, frames) {
13788
16274
  state.frames = frames;
13789
16275
  }
13790
- function updateFlash(state) {
16276
+ function updateFlash2(state) {
13791
16277
  if (state.frames > 0) {
13792
16278
  state.frames--;
13793
16279
  return true;
@@ -13818,7 +16304,8 @@ var games = [
13818
16304
  { id: "typingtest", name: "Typing Test", description: "Test your speed", run: runTypingTest },
13819
16305
  { id: "tron", name: "Tron", description: "Light cycle battle", run: runTronGame },
13820
16306
  { id: "crack", name: "Crack", description: "Hack the system", run: runCrackGame },
13821
- { id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame }
16307
+ { id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame },
16308
+ { id: "hyper-fighter", name: "Hyper Fighter", description: "Gem battle vs AI", run: runHyperFighterGame }
13822
16309
  ];
13823
16310
  function getGame(id) {
13824
16311
  return games.find((g) => g.id === id);
@@ -13835,7 +16322,7 @@ export {
13835
16322
  GAME_EVENTS,
13836
16323
  MAX_PARTICLES2 as MAX_PARTICLES,
13837
16324
  MODE_SELECT_ITEMS,
13838
- PARTICLE_CHARS,
16325
+ PARTICLE_CHARS2 as PARTICLE_CHARS,
13839
16326
  PAUSE_MENU_ITEMS,
13840
16327
  addScorePopup,
13841
16328
  applyShake,
@@ -13905,14 +16392,14 @@ export {
13905
16392
  runWordleGame,
13906
16393
  setTheme,
13907
16394
  showGamesMenu,
13908
- spawnFirework2 as spawnFirework,
16395
+ spawnFirework3 as spawnFirework,
13909
16396
  spawnParticles2 as spawnParticles,
13910
16397
  spawnSparkleTrail2 as spawnSparkleTrail,
13911
16398
  startMatrixRain,
13912
- triggerFlash,
13913
- triggerShake,
13914
- updateFlash,
13915
- updateParticles2 as updateParticles,
16399
+ triggerFlash2 as triggerFlash,
16400
+ triggerShake2 as triggerShake,
16401
+ updateFlash2 as updateFlash,
16402
+ updateParticles3 as updateParticles,
13916
16403
  updatePopups2 as updatePopups
13917
16404
  };
13918
16405
  //# sourceMappingURL=index.js.map