void;
-}
+/**
+ * @typedef {Object} ToastProps
+ * @property {string} message
+ * @property {boolean} visible
+ * @property {() => void} onHide
+ */
-export function Toast({ message, visible, onHide }: ToastProps) {
+/**
+ * @param {ToastProps} props
+ * @returns {import("react").JSX.Element | null}
+ */
+export function Toast({ message, visible, onHide }) {
const [show, setShow] = useState(false);
useEffect(() => {
diff --git a/superpowers/src/game/board.ts b/superpowers/src/game/board.js
similarity index 68%
rename from superpowers/src/game/board.ts
rename to superpowers/src/game/board.js
index a59ddc6..be3c0cd 100644
--- a/superpowers/src/game/board.ts
+++ b/superpowers/src/game/board.js
@@ -1,17 +1,28 @@
-import { Board, Difficulty, TileData, Point } from "../types";
import { DIFFICULTY_CONFIGS } from "./constants";
import { getEmojisForDifficulty } from "./emoji";
import { hasAnyValidMove } from "./pathfinder";
+/**
+ * @typedef {import("../types").Board} Board
+ * @typedef {import("../types").Difficulty} Difficulty
+ * @typedef {import("../types").TileData} TileData
+ * @typedef {import("../types").Point} Point
+ */
+
let nextTileId = 0;
-export function createBoard(difficulty: Difficulty): Board {
+/**
+ * @param {Difficulty} difficulty
+ * @returns {Board}
+ */
+export function createBoard(difficulty) {
const config = DIFFICULTY_CONFIGS[difficulty];
const { rows, cols } = config;
const emojis = getEmojisForDifficulty(difficulty);
// Create pairs
- const tiles: TileData[] = [];
+ /** @type {TileData[]} */
+ const tiles = [];
nextTileId = 0;
for (const emoji of emojis) {
tiles.push({ emoji, id: nextTileId++ });
@@ -25,10 +36,12 @@ export function createBoard(difficulty: Difficulty): Board {
}
// Place into grid
- const board: Board = [];
+ /** @type {Board} */
+ const board = [];
let idx = 0;
for (let r = 0; r < rows; r++) {
- const row: (TileData | null)[] = [];
+ /** @type {(TileData | null)[]} */
+ const row = [];
for (let c = 0; c < cols; c++) {
row.push(tiles[idx++]);
}
@@ -37,7 +50,7 @@ export function createBoard(difficulty: Difficulty): Board {
// Validate at least one valid move exists; reshuffle if not
while (!hasAnyValidMove(board)) {
- const allTiles = board.flat().filter((t): t is TileData => t !== null);
+ const allTiles = board.flat().filter((t) => t !== null);
for (let i = allTiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[allTiles[i], allTiles[j]] = [allTiles[j], allTiles[i]];
@@ -53,8 +66,13 @@ export function createBoard(difficulty: Difficulty): Board {
return board;
}
-export function getRemainingTiles(board: Board): { tile: TileData; pos: Point }[] {
- const result: { tile: TileData; pos: Point }[] = [];
+/**
+ * @param {Board} board
+ * @returns {{ tile: TileData; pos: Point }[]}
+ */
+export function getRemainingTiles(board) {
+ /** @type {{ tile: TileData; pos: Point }[]} */
+ const result = [];
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[r].length; c++) {
const cell = board[r][c];
@@ -66,18 +84,25 @@ export function getRemainingTiles(board: Board): { tile: TileData; pos: Point }[
return result;
}
-export function shuffleBoard(board: Board): Board {
+/**
+ * @param {Board} board
+ * @returns {Board}
+ */
+export function shuffleBoard(board) {
const rows = board.length;
const cols = board[0].length;
- const tiles: TileData[] = [];
- const occupiedPositions: Point[] = [];
- const emptyPositions: Point[] = [];
+ /** @type {TileData[]} */
+ const tiles = [];
+ /** @type {Point[]} */
+ const occupiedPositions = [];
+ /** @type {Point[]} */
+ const emptyPositions = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] !== null) {
- tiles.push(board[r][c]!);
+ tiles.push(/** @type {TileData} */ (board[r][c]));
occupiedPositions.push({ row: r, col: c });
} else {
emptyPositions.push({ row: r, col: c });
@@ -92,7 +117,8 @@ export function shuffleBoard(board: Board): Board {
}
// Create new board with nulls
- const newBoard: Board = [];
+ /** @type {Board} */
+ const newBoard = [];
for (let r = 0; r < rows; r++) {
newBoard.push(new Array(cols).fill(null));
}
diff --git a/superpowers/src/game/constants.ts b/superpowers/src/game/constants.js
similarity index 68%
rename from superpowers/src/game/constants.ts
rename to superpowers/src/game/constants.js
index 3dea814..8890b80 100644
--- a/superpowers/src/game/constants.ts
+++ b/superpowers/src/game/constants.js
@@ -1,6 +1,10 @@
-import { Difficulty, DifficultyConfig } from "../types";
+/**
+ * @typedef {import("../types").Difficulty} Difficulty
+ * @typedef {import("../types").DifficultyConfig} DifficultyConfig
+ */
-export const DIFFICULTY_CONFIGS: Record
= {
+/** @type {Record} */
+export const DIFFICULTY_CONFIGS = {
easy: {
rows: 4,
cols: 6,
diff --git a/superpowers/src/game/emoji.ts b/superpowers/src/game/emoji.js
similarity index 75%
rename from superpowers/src/game/emoji.ts
rename to superpowers/src/game/emoji.js
index 007b84f..6cd2f6d 100644
--- a/superpowers/src/game/emoji.ts
+++ b/superpowers/src/game/emoji.js
@@ -1,7 +1,9 @@
-import { Difficulty } from "../types";
import { DIFFICULTY_CONFIGS } from "./constants";
-export const EMOJI_POOL: string[] = [
+/** @typedef {import("../types").Difficulty} Difficulty */
+
+/** @type {string[]} */
+export const EMOJI_POOL = [
"🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼",
"🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🐔",
"🐧", "🐦", "🦆", "🦅", "🦉", "🦇", "🐺", "🐗",
@@ -10,7 +12,11 @@ export const EMOJI_POOL: string[] = [
"🐟", "🐬", "🐳", "🐊",
];
-export function getEmojisForDifficulty(difficulty: Difficulty): string[] {
+/**
+ * @param {Difficulty} difficulty
+ * @returns {string[]}
+ */
+export function getEmojisForDifficulty(difficulty) {
const count = DIFFICULTY_CONFIGS[difficulty].pairCount;
const shuffled = [...EMOJI_POOL];
for (let i = shuffled.length - 1; i > 0; i--) {
diff --git a/superpowers/src/game/pathfinder.ts b/superpowers/src/game/pathfinder.js
similarity index 65%
rename from superpowers/src/game/pathfinder.ts
rename to superpowers/src/game/pathfinder.js
index 25362da..f1af5e7 100644
--- a/superpowers/src/game/pathfinder.ts
+++ b/superpowers/src/game/pathfinder.js
@@ -1,6 +1,16 @@
-import { Board, Point } from "../types";
+/**
+ * @typedef {import("../types").Board} Board
+ * @typedef {import("../types").Point} Point
+ * @typedef {import("../types").TileData} TileData
+ */
-function isEmpty(board: Board, row: number, col: number): boolean {
+/**
+ * @param {Board} board
+ * @param {number} row
+ * @param {number} col
+ * @returns {boolean}
+ */
+function isEmpty(board, row, col) {
const rows = board.length;
const cols = board[0].length;
if (row < 0 || row >= rows || col < 0 || col >= cols) {
@@ -9,21 +19,25 @@ function isEmpty(board: Board, row: number, col: number): boolean {
return board[row][col] === null;
}
-const DIRS: [number, number][] = [
+/** @type {[number, number][]} */
+const DIRS = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
-function raycast(
- board: Board,
- row: number,
- col: number,
- dr: number,
- dc: number
-): Point[] {
- const points: Point[] = [];
+/**
+ * @param {Board} board
+ * @param {number} row
+ * @param {number} col
+ * @param {number} dr
+ * @param {number} dc
+ * @returns {Point[]}
+ */
+function raycast(board, row, col, dr, dc) {
+ /** @type {Point[]} */
+ const points = [];
let r = row + dr;
let c = col + dc;
const rows = board.length;
@@ -36,11 +50,13 @@ function raycast(
return points;
}
-function canConnectStraight(
- board: Board,
- a: Point,
- b: Point
-): boolean {
+/**
+ * @param {Board} board
+ * @param {Point} a
+ * @param {Point} b
+ * @returns {boolean}
+ */
+function canConnectStraight(board, a, b) {
if (a.row === b.row) {
const minC = Math.min(a.col, b.col);
const maxC = Math.max(a.col, b.col);
@@ -60,11 +76,13 @@ function canConnectStraight(
return false;
}
-export function findPath(
- board: Board,
- a: Point,
- b: Point
-): Point[] | null {
+/**
+ * @param {Board} board
+ * @param {Point} a
+ * @param {Point} b
+ * @returns {Point[] | null}
+ */
+export function findPath(board, a, b) {
const tileA = board[a.row]?.[a.col];
const tileB = board[b.row]?.[b.col];
if (!tileA || !tileB || tileA.emoji !== tileB.emoji) return null;
@@ -76,7 +94,8 @@ export function findPath(
}
// 1 bend: check two possible corners
- const corner1: Point = { row: a.row, col: b.col };
+ /** @type {Point} */
+ const corner1 = { row: a.row, col: b.col };
if (
isEmpty(board, corner1.row, corner1.col) &&
canConnectStraight(board, a, corner1) &&
@@ -85,7 +104,8 @@ export function findPath(
return [a, corner1, b];
}
- const corner2: Point = { row: b.row, col: a.col };
+ /** @type {Point} */
+ const corner2 = { row: b.row, col: a.col };
if (
isEmpty(board, corner2.row, corner2.col) &&
canConnectStraight(board, a, corner2) &&
@@ -98,7 +118,8 @@ export function findPath(
for (const [dr, dc] of DIRS) {
const reachable = raycast(board, a.row, a.col, dr, dc);
for (const mid of reachable) {
- const cornerA: Point = { row: mid.row, col: b.col };
+ /** @type {Point} */
+ const cornerA = { row: mid.row, col: b.col };
if (
isEmpty(board, cornerA.row, cornerA.col) &&
canConnectStraight(board, mid, cornerA) &&
@@ -107,7 +128,8 @@ export function findPath(
return [a, mid, cornerA, b];
}
- const cornerB: Point = { row: b.row, col: mid.col };
+ /** @type {Point} */
+ const cornerB = { row: b.row, col: mid.col };
if (
isEmpty(board, cornerB.row, cornerB.col) &&
canConnectStraight(board, mid, cornerB) &&
@@ -125,20 +147,29 @@ export function findPath(
return null;
}
-export function hasAnyValidMove(board: Board): boolean {
- const tiles: { pos: Point; emoji: string }[] = [];
+/**
+ * @param {Board} board
+ * @returns {boolean}
+ */
+export function hasAnyValidMove(board) {
+ /** @type {{ pos: Point; emoji: string }[]} */
+ const tiles = [];
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[r].length; c++) {
if (board[r][c] !== null) {
- tiles.push({ pos: { row: r, col: c }, emoji: board[r][c]!.emoji });
+ tiles.push({
+ pos: { row: r, col: c },
+ emoji: /** @type {TileData} */ (board[r][c]).emoji,
+ });
}
}
}
- const groups = new Map();
+ /** @type {Map} */
+ const groups = new Map();
for (const t of tiles) {
if (!groups.has(t.emoji)) groups.set(t.emoji, []);
- groups.get(t.emoji)!.push(t.pos);
+ /** @type {Point[]} */ (groups.get(t.emoji)).push(t.pos);
}
for (const [, positions] of groups) {
diff --git a/superpowers/src/game/scoring.ts b/superpowers/src/game/scoring.js
similarity index 68%
rename from superpowers/src/game/scoring.ts
rename to superpowers/src/game/scoring.js
index 313b4b5..acfeb88 100644
--- a/superpowers/src/game/scoring.ts
+++ b/superpowers/src/game/scoring.js
@@ -1,9 +1,11 @@
import { BASE_MATCH_SCORE, SPEED_BONUS_MAX, SPEED_BONUS_WINDOW_MS } from "./constants";
-export function calculateMatchScore(
- msSinceLastMatch: number,
- combo: number
-): number {
+/**
+ * @param {number} msSinceLastMatch
+ * @param {number} combo
+ * @returns {number}
+ */
+export function calculateMatchScore(msSinceLastMatch, combo) {
let speedBonus = 0;
if (msSinceLastMatch < SPEED_BONUS_WINDOW_MS) {
const ratio = 1 - msSinceLastMatch / SPEED_BONUS_WINDOW_MS;
diff --git a/superpowers/src/game/state.ts b/superpowers/src/game/state.js
similarity index 54%
rename from superpowers/src/game/state.ts
rename to superpowers/src/game/state.js
index 86b6742..2a7f4a0 100644
--- a/superpowers/src/game/state.ts
+++ b/superpowers/src/game/state.js
@@ -1,24 +1,27 @@
-import { Difficulty, GameStatus } from "../types";
import { DIFFICULTY_CONFIGS } from "./constants";
-type Listener = () => void;
+/**
+ * @typedef {import("../types").Difficulty} Difficulty
+ * @typedef {import("../types").GameStatus} GameStatus
+ */
-interface GameState {
- status: GameStatus;
- difficulty: Difficulty | null;
- score: number;
- timerSeconds: number;
- hintsRemaining: number;
- shufflesRemaining: number;
- combo: number;
- lastMatchTime: number;
-}
+/** @typedef {() => void} Listener */
+
+/**
+ * @typedef {Object} GameState
+ * @property {GameStatus} status
+ * @property {Difficulty | null} difficulty
+ * @property {number} score
+ * @property {number} timerSeconds
+ * @property {number} hintsRemaining
+ * @property {number} shufflesRemaining
+ * @property {number} combo
+ * @property {number} lastMatchTime
+ */
export class GameStateManager {
- private state: GameState;
- private listeners: Map = new Map();
-
constructor() {
+ /** @type {GameState} */
this.state = {
status: "menu",
difficulty: null,
@@ -29,20 +32,33 @@ export class GameStateManager {
combo: 1,
lastMatchTime: 0,
};
+ /** @type {Map} */
+ this.listeners = new Map();
}
- getState(): Readonly {
+ /** @returns {Readonly} */
+ getState() {
return { ...this.state };
}
- on(event: string, listener: Listener): void {
+ /**
+ * @param {string} event
+ * @param {Listener} listener
+ * @returns {void}
+ */
+ on(event, listener) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
- this.listeners.get(event)!.push(listener);
+ /** @type {Listener[]} */ (this.listeners.get(event)).push(listener);
}
- off(event: string, listener: Listener): void {
+ /**
+ * @param {string} event
+ * @param {Listener} listener
+ * @returns {void}
+ */
+ off(event, listener) {
const listeners = this.listeners.get(event);
if (listeners) {
const idx = listeners.indexOf(listener);
@@ -50,14 +66,22 @@ export class GameStateManager {
}
}
- emit(event: string): void {
+ /**
+ * @param {string} event
+ * @returns {void}
+ */
+ emit(event) {
const listeners = this.listeners.get(event);
if (listeners) {
for (const l of listeners) l();
}
}
- startGame(difficulty: Difficulty): void {
+ /**
+ * @param {Difficulty} difficulty
+ * @returns {void}
+ */
+ startGame(difficulty) {
const config = DIFFICULTY_CONFIGS[difficulty];
this.state = {
status: "playing",
@@ -72,26 +96,33 @@ export class GameStateManager {
this.emit("stateChange");
}
- addScore(points: number): void {
+ /**
+ * @param {number} points
+ * @returns {void}
+ */
+ addScore(points) {
this.state.score += points;
this.emit("stateChange");
}
- useHint(): boolean {
+ /** @returns {boolean} */
+ useHint() {
if (this.state.hintsRemaining <= 0) return false;
this.state.hintsRemaining--;
this.emit("stateChange");
return true;
}
- useShuffle(): boolean {
+ /** @returns {boolean} */
+ useShuffle() {
if (this.state.shufflesRemaining <= 0) return false;
this.state.shufflesRemaining--;
this.emit("stateChange");
return true;
}
- tick(): void {
+ /** @returns {void} */
+ tick() {
if (this.state.status !== "playing") return;
this.state.timerSeconds--;
if (this.state.timerSeconds <= 0) {
@@ -101,22 +132,32 @@ export class GameStateManager {
this.emit("stateChange");
}
- incrementCombo(): void {
+ /** @returns {void} */
+ incrementCombo() {
this.state.combo++;
this.emit("stateChange");
}
- resetCombo(): void {
+ /** @returns {void} */
+ resetCombo() {
this.state.combo = 1;
this.emit("stateChange");
}
- setStatus(status: GameStatus): void {
+ /**
+ * @param {GameStatus} status
+ * @returns {void}
+ */
+ setStatus(status) {
this.state.status = status;
this.emit("stateChange");
}
- setLastMatchTime(time: number): void {
+ /**
+ * @param {number} time
+ * @returns {void}
+ */
+ setLastMatchTime(time) {
this.state.lastMatchTime = time;
}
}
diff --git a/superpowers/src/main.tsx b/superpowers/src/main.jsx
similarity index 69%
rename from superpowers/src/main.tsx
rename to superpowers/src/main.jsx
index 05c04dc..6bdcbe0 100644
--- a/superpowers/src/main.tsx
+++ b/superpowers/src/main.jsx
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./components/App";
-createRoot(document.getElementById("root")!).render(
+createRoot(/** @type {HTMLElement} */ (document.getElementById("root"))).render(
diff --git a/superpowers/src/phaser/config.ts b/superpowers/src/phaser/config.js
similarity index 65%
rename from superpowers/src/phaser/config.ts
rename to superpowers/src/phaser/config.js
index 92cc920..1c53ff0 100644
--- a/superpowers/src/phaser/config.ts
+++ b/superpowers/src/phaser/config.js
@@ -2,11 +2,13 @@ import Phaser from "phaser";
import { PreloadScene } from "./scenes/PreloadScene";
import { GameScene } from "./scenes/GameScene";
-export function createPhaserConfig(
- parent: HTMLElement,
- width: number,
- height: number
-): Phaser.Types.Core.GameConfig {
+/**
+ * @param {HTMLElement} parent
+ * @param {number} width
+ * @param {number} height
+ * @returns {Phaser.Types.Core.GameConfig}
+ */
+export function createPhaserConfig(parent, width, height) {
return {
type: Phaser.AUTO,
parent,
diff --git a/superpowers/src/phaser/scenes/GameScene.ts b/superpowers/src/phaser/scenes/GameScene.js
similarity index 77%
rename from superpowers/src/phaser/scenes/GameScene.ts
rename to superpowers/src/phaser/scenes/GameScene.js
index 5271173..0e8fc65 100644
--- a/superpowers/src/phaser/scenes/GameScene.ts
+++ b/superpowers/src/phaser/scenes/GameScene.js
@@ -1,35 +1,52 @@
import Phaser from "phaser";
-import { Board, Difficulty, Point } from "../../types";
-import { GameStateManager } from "../../game/state";
import { createBoard, shuffleBoard, getRemainingTiles } from "../../game/board";
import { findPath, hasAnyValidMove } from "../../game/pathfinder";
import { calculateMatchScore } from "../../game/scoring";
import { DIFFICULTY_CONFIGS } from "../../game/constants";
+/**
+ * @typedef {import("../../types").Board} Board
+ * @typedef {import("../../types").Difficulty} Difficulty
+ * @typedef {import("../../types").Point} Point
+ */
+/** @typedef {import("../../game/state").GameStateManager} GameStateManager */
+
const TILE_SIZE = 56;
const TILE_GAP = 4;
export class GameScene extends Phaser.Scene {
- private board!: Board;
- private difficulty!: Difficulty;
- private stateManager!: GameStateManager;
- private tileObjects: Map = new Map();
- private selectedTile: Point | null = null;
- private selectedHighlight: Phaser.GameObjects.Rectangle | null = null;
- private lineGraphics!: Phaser.GameObjects.Graphics;
- private isProcessing = false;
- private timerEvent: Phaser.Time.TimerEvent | null = null;
- private hintHandler!: () => void;
- private shuffleHandler!: () => void;
- private solveInterval: number | null = null;
-
constructor() {
super({ key: "GameScene" });
+
+ /** @type {Board} */
+ this.board = [];
+ /** @type {Difficulty} */
+ this.difficulty = "easy";
+ /** @type {GameStateManager} */
+ this.stateManager = /** @type {GameStateManager} */ (/** @type {unknown} */ (null));
+ /** @type {Map} */
+ this.tileObjects = new Map();
+ /** @type {Point | null} */
+ this.selectedTile = null;
+ /** @type {Phaser.GameObjects.Rectangle | null} */
+ this.selectedHighlight = null;
+ /** @type {Phaser.GameObjects.Graphics} */
+ this.lineGraphics = /** @type {Phaser.GameObjects.Graphics} */ (/** @type {unknown} */ (null));
+ this.isProcessing = false;
+ /** @type {Phaser.Time.TimerEvent | null} */
+ this.timerEvent = null;
+ /** @type {() => void} */
+ this.hintHandler = () => {};
+ /** @type {() => void} */
+ this.shuffleHandler = () => {};
+ /** @type {number | null} */
+ this.solveInterval = null;
}
- create(): void {
- this.difficulty = this.registry.get("difficulty") as Difficulty;
- this.stateManager = this.registry.get("stateManager") as GameStateManager;
+ /** @returns {void} */
+ create() {
+ this.difficulty = /** @type {Difficulty} */ (this.registry.get("difficulty"));
+ this.stateManager = /** @type {GameStateManager} */ (this.registry.get("stateManager"));
this.board = createBoard(this.difficulty);
this.lineGraphics = this.add.graphics();
@@ -54,10 +71,12 @@ export class GameScene extends Phaser.Scene {
});
// Expose solve() on window for console use
- (window as unknown as Record).solve = () => this.startSolve();
+ /** @type {Record} */ (/** @type {unknown} */ (window)).solve = () =>
+ this.startSolve();
}
- private renderBoard(): void {
+ /** @returns {void} */
+ renderBoard() {
for (const [, obj] of this.tileObjects) {
obj.destroy();
}
@@ -111,7 +130,11 @@ export class GameScene extends Phaser.Scene {
}
}
- private getTileScreenPos(point: Point): { x: number; y: number } {
+ /**
+ * @param {Point} point
+ * @returns {{ x: number; y: number }}
+ */
+ getTileScreenPos(point) {
const config = DIFFICULTY_CONFIGS[this.difficulty];
const totalWidth = config.cols * (TILE_SIZE + TILE_GAP) - TILE_GAP;
const totalHeight = config.rows * (TILE_SIZE + TILE_GAP) - TILE_GAP;
@@ -123,7 +146,11 @@ export class GameScene extends Phaser.Scene {
};
}
- private onTileClick(pos: Point): void {
+ /**
+ * @param {Point} pos
+ * @returns {void}
+ */
+ onTileClick(pos) {
if (this.stateManager.getState().status !== "playing") return;
const tile = this.board[pos.row][pos.col];
if (!tile) return;
@@ -151,7 +178,11 @@ export class GameScene extends Phaser.Scene {
}
}
- private highlightTile(pos: Point): void {
+ /**
+ * @param {Point} pos
+ * @returns {void}
+ */
+ highlightTile(pos) {
this.clearHighlight();
const screenPos = this.getTileScreenPos(pos);
this.selectedHighlight = this.add.rectangle(
@@ -164,19 +195,27 @@ export class GameScene extends Phaser.Scene {
this.selectedHighlight.setFillStyle(0xe94560, 0.15);
}
- private clearHighlight(): void {
+ /** @returns {void} */
+ clearHighlight() {
if (this.selectedHighlight) {
this.selectedHighlight.destroy();
this.selectedHighlight = null;
}
}
- private clearSelection(): void {
+ /** @returns {void} */
+ clearSelection() {
this.selectedTile = null;
this.clearHighlight();
}
- private handleMatch(a: Point, b: Point, path: Point[]): void {
+ /**
+ * @param {Point} a
+ * @param {Point} b
+ * @param {Point[]} path
+ * @returns {void}
+ */
+ handleMatch(a, b, path) {
this.drawPath(path);
const state = this.stateManager.getState();
@@ -206,7 +245,11 @@ export class GameScene extends Phaser.Scene {
});
}
- private handleMismatch(clickedPos: Point): void {
+ /**
+ * @param {Point} clickedPos
+ * @returns {void}
+ */
+ handleMismatch(clickedPos) {
this.stateManager.resetCombo();
const key = `${clickedPos.row},${clickedPos.col}`;
@@ -226,7 +269,11 @@ export class GameScene extends Phaser.Scene {
this.highlightTile(clickedPos);
}
- private removeTile(pos: Point): void {
+ /**
+ * @param {Point} pos
+ * @returns {void}
+ */
+ removeTile(pos) {
this.board[pos.row][pos.col] = null;
const key = `${pos.row},${pos.col}`;
const container = this.tileObjects.get(key);
@@ -242,7 +289,11 @@ export class GameScene extends Phaser.Scene {
}
}
- private drawPath(path: Point[]): void {
+ /**
+ * @param {Point[]} path
+ * @returns {void}
+ */
+ drawPath(path) {
this.lineGraphics.clear();
this.lineGraphics.lineStyle(4, 0xe94560, 0.8);
this.lineGraphics.beginPath();
@@ -259,7 +310,8 @@ export class GameScene extends Phaser.Scene {
this.lineGraphics.strokePath();
}
- private startTimer(): void {
+ /** @returns {void} */
+ startTimer() {
this.timerEvent = this.time.addEvent({
delay: 1000,
callback: () => {
@@ -272,7 +324,8 @@ export class GameScene extends Phaser.Scene {
});
}
- handleHint(): void {
+ /** @returns {void} */
+ handleHint() {
if (!this.stateManager.useHint()) return;
const remaining = getRemainingTiles(this.board);
@@ -294,7 +347,11 @@ export class GameScene extends Phaser.Scene {
}
}
- private pulseHint(pos: Point): void {
+ /**
+ * @param {Point} pos
+ * @returns {void}
+ */
+ pulseHint(pos) {
const key = `${pos.row},${pos.col}`;
const container = this.tileObjects.get(key);
if (container) {
@@ -309,17 +366,20 @@ export class GameScene extends Phaser.Scene {
}
}
- handleShuffle(): void {
+ /** @returns {void} */
+ handleShuffle() {
if (!this.stateManager.useShuffle()) return;
this.performShuffle();
}
- private autoShuffle(): void {
+ /** @returns {void} */
+ autoShuffle() {
this.performShuffle();
this.stateManager.emit("autoShuffle");
}
- private performShuffle(): void {
+ /** @returns {void} */
+ performShuffle() {
this.clearSelection();
do {
this.board = shuffleBoard(this.board);
@@ -327,7 +387,8 @@ export class GameScene extends Phaser.Scene {
this.renderBoard();
}
- private startSolve(): void {
+ /** @returns {void} */
+ startSolve() {
if (this.solveInterval !== null) {
this.stopSolve();
console.log("Auto-solve stopped.");
@@ -346,14 +407,16 @@ export class GameScene extends Phaser.Scene {
}, 1000);
}
- private stopSolve(): void {
+ /** @returns {void} */
+ stopSolve() {
if (this.solveInterval !== null) {
clearInterval(this.solveInterval);
this.solveInterval = null;
}
}
- private solveOneMove(): void {
+ /** @returns {void} */
+ solveOneMove() {
if (this.isProcessing) return;
const remaining = getRemainingTiles(this.board);
for (let i = 0; i < remaining.length; i++) {
@@ -377,7 +440,8 @@ export class GameScene extends Phaser.Scene {
}
}
- shutdown(): void {
+ /** @returns {void} */
+ shutdown() {
if (this.timerEvent) {
this.timerEvent.destroy();
this.timerEvent = null;
diff --git a/superpowers/src/phaser/scenes/PreloadScene.ts b/superpowers/src/phaser/scenes/PreloadScene.js
similarity index 75%
rename from superpowers/src/phaser/scenes/PreloadScene.ts
rename to superpowers/src/phaser/scenes/PreloadScene.js
index a8f4bd7..9a5e9fb 100644
--- a/superpowers/src/phaser/scenes/PreloadScene.ts
+++ b/superpowers/src/phaser/scenes/PreloadScene.js
@@ -5,11 +5,13 @@ export class PreloadScene extends Phaser.Scene {
super({ key: "PreloadScene" });
}
- preload(): void {
+ /** @returns {void} */
+ preload() {
// No assets to load — emoji are rendered as text
}
- create(): void {
+ /** @returns {void} */
+ create() {
this.scene.start("GameScene");
}
}
diff --git a/superpowers/src/types/index.ts b/superpowers/src/types/index.d.ts
similarity index 100%
rename from superpowers/src/types/index.ts
rename to superpowers/src/types/index.d.ts
diff --git a/superpowers/tests/game/board.test.ts b/superpowers/tests/game/board.test.js
similarity index 94%
rename from superpowers/tests/game/board.test.ts
rename to superpowers/tests/game/board.test.js
index 8191c4f..7cfb323 100644
--- a/superpowers/tests/game/board.test.ts
+++ b/superpowers/tests/game/board.test.js
@@ -22,7 +22,8 @@ describe("createBoard", () => {
it("every tile has a matching pair", () => {
const board = createBoard("easy");
- const emojiCounts = new Map();
+ /** @type {Map} */
+ const emojiCounts = new Map();
for (const row of board) {
for (const cell of row) {
if (cell) {
@@ -37,7 +38,8 @@ describe("createBoard", () => {
it("all tiles have unique ids", () => {
const board = createBoard("medium");
- const ids = new Set();
+ /** @type {Set} */
+ const ids = new Set();
for (const row of board) {
for (const cell of row) {
if (cell) {
diff --git a/superpowers/tests/game/emoji.test.ts b/superpowers/tests/game/emoji.test.js
similarity index 100%
rename from superpowers/tests/game/emoji.test.ts
rename to superpowers/tests/game/emoji.test.js
diff --git a/superpowers/tests/game/pathfinder.test.ts b/superpowers/tests/game/pathfinder.test.js
similarity index 84%
rename from superpowers/tests/game/pathfinder.test.ts
rename to superpowers/tests/game/pathfinder.test.js
index 1720a89..606ee88 100644
--- a/superpowers/tests/game/pathfinder.test.ts
+++ b/superpowers/tests/game/pathfinder.test.js
@@ -1,8 +1,13 @@
import { describe, it, expect } from "vitest";
import { findPath, hasAnyValidMove } from "../../src/game/pathfinder";
-import { Board } from "../../src/types";
-function makeBoard(grid: (string | null)[][]): Board {
+/** @typedef {import("../../src/types").Board} Board */
+
+/**
+ * @param {(string | null)[][]} grid
+ * @returns {Board}
+ */
+function makeBoard(grid) {
let id = 0;
return grid.map((row) =>
row.map((cell) => (cell !== null ? { emoji: cell, id: id++ } : null))
@@ -16,7 +21,7 @@ describe("findPath", () => {
]);
const path = findPath(board, { row: 0, col: 0 }, { row: 0, col: 2 });
expect(path).not.toBeNull();
- expect(path!.length).toBe(2);
+ expect(/** @type {NonNullable} */ (path).length).toBe(2);
});
it("finds direct vertical connection", () => {
@@ -27,7 +32,7 @@ describe("findPath", () => {
]);
const path = findPath(board, { row: 0, col: 0 }, { row: 2, col: 0 });
expect(path).not.toBeNull();
- expect(path!.length).toBe(2);
+ expect(/** @type {NonNullable} */ (path).length).toBe(2);
});
it("finds one-bend connection", () => {
@@ -37,7 +42,7 @@ describe("findPath", () => {
]);
const path = findPath(board, { row: 0, col: 0 }, { row: 1, col: 1 });
expect(path).not.toBeNull();
- expect(path!.length).toBe(3);
+ expect(/** @type {NonNullable} */ (path).length).toBe(3);
});
it("finds two-bend connection", () => {
@@ -48,7 +53,7 @@ describe("findPath", () => {
]);
const path = findPath(board, { row: 0, col: 0 }, { row: 2, col: 2 });
expect(path).not.toBeNull();
- expect(path!.length).toBe(4);
+ expect(/** @type {NonNullable} */ (path).length).toBe(4);
});
it("returns null when no valid path exists", () => {
@@ -83,7 +88,7 @@ describe("findPath", () => {
]);
const path = findPath(board, { row: 0, col: 0 }, { row: 0, col: 1 });
expect(path).not.toBeNull();
- expect(path!.length).toBe(2);
+ expect(/** @type {NonNullable} */ (path).length).toBe(2);
});
});
diff --git a/superpowers/tests/game/scoring.test.ts b/superpowers/tests/game/scoring.test.js
similarity index 100%
rename from superpowers/tests/game/scoring.test.ts
rename to superpowers/tests/game/scoring.test.js
diff --git a/superpowers/tests/game/state.test.ts b/superpowers/tests/game/state.test.js
similarity index 98%
rename from superpowers/tests/game/state.test.ts
rename to superpowers/tests/game/state.test.js
index a5796f9..c2c168c 100644
--- a/superpowers/tests/game/state.test.ts
+++ b/superpowers/tests/game/state.test.js
@@ -2,7 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { GameStateManager } from "../../src/game/state";
describe("GameStateManager", () => {
- let state: GameStateManager;
+ /** @type {GameStateManager} */
+ let state;
beforeEach(() => {
state = new GameStateManager();
diff --git a/superpowers/tsconfig.app.json b/superpowers/tsconfig.app.json
deleted file mode 100644
index 358ca9b..0000000
--- a/superpowers/tsconfig.app.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["src"]
-}
diff --git a/superpowers/tsconfig.json b/superpowers/tsconfig.json
deleted file mode 100644
index 1ffef60..0000000
--- a/superpowers/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
diff --git a/superpowers/tsconfig.node.json b/superpowers/tsconfig.node.json
deleted file mode 100644
index db0becc..0000000
--- a/superpowers/tsconfig.node.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/superpowers/vite.config.ts b/superpowers/vite.config.js
similarity index 100%
rename from superpowers/vite.config.ts
rename to superpowers/vite.config.js