From 1b9d963f46ec33e586f4f48ffb146ccbde945a27 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Thu, 23 Oct 2025 20:21:58 +0100 Subject: [PATCH] removed src_old --- src_old/MazeGenerator.ts | 346 ----------------------------- src_old/MazeRenderer.ts | 115 ---------- src_old/game.css | 232 ------------------- src_old/hooks/useTimer.ts | 57 ----- src_old/index.ts | 6 - src_old/types.ts | 54 ----- src_old/useGame/index.ts | 2 - src_old/useGame/useGame.ts | 70 ------ src_old/useGame/useGameStatus.ts | 55 ----- src_old/useGame/useHeroRender.ts | 81 ------- src_old/useGame/useKeyBindings.ts | 259 --------------------- src_old/useGame/useMaze.ts | 45 ---- src_old/useGame/usePlayer.ts | 202 ----------------- src_old/useScore/hooks/useTimer.ts | 85 ------- src_old/useScore/index.ts | 1 - src_old/useScore/useScore.ts | 143 ------------ src_old/useScore/useScorePaths.ts | 58 ----- src_old/useScore/useScoreTime.ts | 38 ---- 18 files changed, 1849 deletions(-) delete mode 100644 src_old/MazeGenerator.ts delete mode 100644 src_old/MazeRenderer.ts delete mode 100644 src_old/game.css delete mode 100644 src_old/hooks/useTimer.ts delete mode 100644 src_old/index.ts delete mode 100644 src_old/types.ts delete mode 100644 src_old/useGame/index.ts delete mode 100644 src_old/useGame/useGame.ts delete mode 100644 src_old/useGame/useGameStatus.ts delete mode 100644 src_old/useGame/useHeroRender.ts delete mode 100644 src_old/useGame/useKeyBindings.ts delete mode 100644 src_old/useGame/useMaze.ts delete mode 100644 src_old/useGame/usePlayer.ts delete mode 100644 src_old/useScore/hooks/useTimer.ts delete mode 100644 src_old/useScore/index.ts delete mode 100644 src_old/useScore/useScore.ts delete mode 100644 src_old/useScore/useScorePaths.ts delete mode 100644 src_old/useScore/useScoreTime.ts diff --git a/src_old/MazeGenerator.ts b/src_old/MazeGenerator.ts deleted file mode 100644 index 611629c..0000000 --- a/src_old/MazeGenerator.ts +++ /dev/null @@ -1,346 +0,0 @@ -import type { PositionTag, Coord, CellTag, MazeCell, MazeData } from "./types"; - -export class MazeGenerator { - width: number; - height: number; - cols: number; - rows: number; - maze: MazeCell[][]; - totalSteps: number = 0; - - constructor(userCols: number, userRows: number) { - const cellsWide = Math.max(1, Math.floor(userCols / 2)); - const cellsHigh = Math.max(1, Math.floor(userRows / 2)); - - this.width = cellsWide; - this.height = cellsHigh; - - this.cols = 2 * this.width + 1; - this.rows = 2 * this.height + 1; - - this.maze = this.initArray([]); - - this.maze.forEach((row, r) => { - row.forEach((_cell, c) => { - switch (r) { - case 0: - case this.rows - 1: - this.maze[r][c] = ["wall"]; - break; - - default: - if ((r % 2) === 1) { - if ((c === 0) || (c === this.cols - 1)) { - this.maze[r][c] = ["wall"]; - } - } else if (c % 2 === 0) { - this.maze[r][c] = ["wall"]; - } - } - }); - - if (r === 0) { - const doorPos = this.posToSpace(this.rand(1, this.width)); - this.maze[r][doorPos] = ["door", "exit"]; - } - - if (r === this.rows - 1) { - const doorPos = this.posToSpace(this.rand(1, this.width)); - this.maze[r][doorPos] = ["door", "entrance"]; - } - }); - - this.partition(1, this.height - 1, 1, this.width - 1); - } - - private initArray(value?: T): T[][] { - return new Array(this.rows) - .fill(null) - .map(() => new Array(this.cols).fill(value as T)); - } - - private rand(min: number, max: number): number { - return min + Math.floor(Math.random() * (1 + max - min)); - } - - private posToSpace(x: number): number { - return 2 * (x - 1) + 1; - } - - private posToWall(x: number): number { - return 2 * x; - } - - private inBounds(r: number, c: number): boolean { - if ( - typeof this.maze[r] === "undefined" || - typeof this.maze[r]?.[c] === "undefined" - ) { - return false; - } - return true; - } - - private shuffle(array: T[]): T[] { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - } - - private partition(r1: number, r2: number, c1: number, c2: number): void { - let horiz: number, vert: number; - let x: number, y: number, start: number, end: number; - - if (r2 < r1 || c2 < c1) { - return; - } - - if (r1 === r2) { - horiz = r1; - } else { - x = r1 + 1; - y = r2 - 1; - start = Math.round(x + (y - x) / 4); - end = Math.round(x + (y - x) * 3 / 4); - horiz = this.rand(start, end); - } - - if (c1 === c2) { - vert = c1; - } else { - x = c1 + 1; - y = c2 - 1; - start = Math.round(x + (y - x) / 3); - end = Math.round(x + (y - x) * 2 / 3); - vert = this.rand(start, end); - } - - for (let i = this.posToWall(r1) - 1; i <= this.posToWall(r2) + 1; i++) { - for (let j = this.posToWall(c1) - 1; j <= this.posToWall(c2) + 1; j++) { - if (i === this.posToWall(horiz) || j === this.posToWall(vert)) { - this.maze[i][j] = ["wall"]; - } - } - } - - const gaps = this.shuffle([true, true, true, false]); - - if (gaps[0]) { - const gapPosition = this.rand(c1, vert); - this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; - } - - if (gaps[1]) { - const gapPosition = this.rand(vert + 1, c2 + 1); - this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; - } - - if (gaps[2]) { - const gapPosition = this.rand(r1, horiz); - this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; - } - - if (gaps[3]) { - const gapPosition = this.rand(horiz + 1, r2 + 1); - this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; - } - - this.partition(r1, horiz - 1, c1, vert - 1); - this.partition(horiz + 1, r2, c1, vert - 1); - this.partition(r1, horiz - 1, vert + 1, c2); - this.partition(horiz + 1, r2, vert + 1, c2); - } - - private isGap(...cells: Array<[number, number]>): boolean { - return cells.every(([row, col]) => { - const cell = this.maze[row][col]; - if (cell.length > 0) { - if (!cell.includes("door")) return false; - } - return true; - }); - } - - private countSteps( - array: (number | undefined)[][], - r: number, - c: number, - val: number, - stop: CellTag - ): boolean { - if (!this.inBounds(r, c)) { - return false; - } - - if (array[r][c] !== undefined && (array[r][c] as number) <= val) { - return false; - } - - if (!this.isGap([r, c])) { - return false; - } - - array[r][c] = val; - - if (this.maze[r][c].includes(stop)) { - return true; - } - - this.countSteps(array, r - 1, c, val + 1, stop); - this.countSteps(array, r, c + 1, val + 1, stop); - this.countSteps(array, r + 1, c, val + 1, stop); - this.countSteps(array, r, c - 1, val + 1, stop); - - return false; - } - - private getKeyLocation(): Coord { - const fromEntrance = this.initArray(); - const fromExit = this.initArray(); - - this.totalSteps = -1; - - for (let j = 1; j < this.cols - 1; j++) { - if (this.maze[this.rows - 1][j].includes("entrance")) { - this.countSteps(fromEntrance, this.rows - 1, j, 0, "exit"); - } - if (this.maze[0][j].includes("exit")) { - this.countSteps(fromExit, 0, j, 0, "entrance"); - } - } - - let fc = -1, - fr = -1; - - this.maze.forEach((_row, r) => { - _row.forEach((_cell, c) => { - if (typeof fromEntrance[r]?.[c] === "undefined") { - return; - } - const stepCount = - (fromEntrance[r][c] ?? Infinity) + (fromExit[r][c] ?? Infinity); - if (stepCount > this.totalSteps) { - fr = r; - fc = c; - this.totalSteps = stepCount; - } - }); - }); - - return [fr, fc]; - } - - placeKey(): void { - const [fr, fc] = this.getKeyLocation(); - this.maze[fr][fc] = ["key"]; - } - - shortestPath( - fromTag: PositionTag, - toTag: PositionTag, - currentPosition?: Coord - ): Coord[] { - let start: Coord | undefined; - let end: Coord | undefined; - - if (fromTag === "hero" && currentPosition) { - start = currentPosition; - } else { - for (let r = 0; r < this.rows; r++) { - for (let c = 0; c < this.cols; c++) { - if (this.maze[r][c].includes(fromTag)) start = [r, c]; - } - } - } - - for (let r = 0; r < this.rows; r++) { - for (let c = 0; c < this.cols; c++) { - if (this.maze[r][c].includes(toTag)) end = [r, c]; - } - } - - if (!start || !end) return []; - - const visited = this.initArray(false); - const prev = this.initArray(null); - const queue: Coord[] = [[...start]]; - visited[start[0]][start[1]] = true; - - const dirs: Coord[] = [ - [-1, 0], - [1, 0], - [0, -1], - [0, 1], - ]; - - while (queue.length > 0) { - const [r, c] = queue.shift() as Coord; - - if (r === end[0] && c === end[1]) { - const path: Coord[] = []; - let curr: Coord | null = end; - while (curr) { - path.unshift(curr); - curr = prev[curr[0]][curr[1]]; - } - return path; - } - - for (const [dr, dc] of dirs) { - const nr = r + dr, - nc = c + dc; - if (!this.inBounds(nr, nc)) continue; - if (visited[nr][nc]) continue; - if (this.maze[nr][nc].includes("wall")) continue; - - visited[nr][nc] = true; - prev[nr][nc] = [r, c]; - queue.push([nr, nc]); - } - } - - return []; - } - - shortestPathToKey(currentPosition?: Coord): Coord[] { - return this.shortestPath( - currentPosition ? "hero" : "entrance", - "key", - currentPosition - ); - } - - shortestPathToExit(currentPosition?: Coord): Coord[] { - return this.shortestPath( - currentPosition ? "hero" : "entrance", - "exit", - currentPosition - ); - } - - getDistance( - fromTag: PositionTag, - toTag: PositionTag, - currentPosition?: Coord - ): number { - const path = this.shortestPath(fromTag, toTag, currentPosition); - return path.length ? path.length - 1 : Infinity; - } - - getData(): MazeData { - return { - maze: this.maze, - width: this.width, - height: this.height, - cols: this.cols, - rows: this.rows, - totalSteps: this.totalSteps, - }; - } - - toJSON(): MazeData { - return this.getData(); - } -} diff --git a/src_old/MazeRenderer.ts b/src_old/MazeRenderer.ts deleted file mode 100644 index a70558f..0000000 --- a/src_old/MazeRenderer.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { MazeData } from "./types"; - -export class MazeRenderer { - private mazeData: MazeData; - private numbered: boolean; - private parentDiv?: HTMLElement; - - constructor(mazeData: MazeData, numbered: boolean = false) { - this.mazeData = mazeData; - this.numbered = numbered; - } - - display(parentDiv: HTMLElement | null): boolean { - this.parentDiv = parentDiv ?? undefined; - - if (!this.parentDiv) { - console.error("Cannot initialise maze - parent element not provided."); - return false; - } - - while (this.parentDiv.firstChild) { - this.parentDiv.removeChild(this.parentDiv.firstChild); - } - - const container = document.createElement("div"); - container.id = "maze"; - container.dataset.steps = String(this.mazeData.totalSteps); - - const makeCell = (cls = "", text = "", attrs?: Record) => { - const d = document.createElement("div"); - d.className = cls; - if (text) d.textContent = text; - if (attrs) for (const k in attrs) d.setAttribute(k, attrs[k]); - return d; - }; - - const rowLabel = (r: number) => (r > 0 && r < this.mazeData.rows ? String(r) : ""); - const colLabel = (c: number) => (c > 0 && c < this.mazeData.cols ? String(c) : ""); - - if (this.numbered) { - const topStrip = document.createElement("div"); - topStrip.className = "number-strip number-strip-top"; - topStrip.appendChild(makeCell("number corner", "")); - for (let c = 0; c < this.mazeData.cols; c++) { - topStrip.appendChild(makeCell("number col", colLabel(c), { "data-c": String(c) })); - } - topStrip.appendChild(makeCell("number corner", "")); - container.appendChild(topStrip); - } - - this.mazeData.maze.forEach((row, r) => { - const rowDiv = document.createElement("div"); - rowDiv.className = "maze-row"; - rowDiv.setAttribute("data-r", String(r)); - - if (this.numbered) { - rowDiv.appendChild(makeCell("number row", rowLabel(r), { "data-r": String(r) })); - } - - row.forEach((cell, c) => { - const cellDiv = document.createElement("div"); - if (cell) cellDiv.className = cell.join(" "); - cellDiv.setAttribute("data-r", String(r)); - cellDiv.setAttribute("data-c", String(c)); - rowDiv.appendChild(cellDiv); - }); - - if (this.numbered) { - rowDiv.appendChild(makeCell("number row", rowLabel(r), { "data-r": String(r) })); - } - - container.appendChild(rowDiv); - }); - - if (this.numbered) { - const bottomStrip = document.createElement("div"); - bottomStrip.className = "number-strip number-strip-bottom"; - bottomStrip.appendChild(makeCell("number corner", "")); - for (let c = 0; c < this.mazeData.cols; c++) { - bottomStrip.appendChild(makeCell("number col", colLabel(c), { "data-c": String(c) })); - } - bottomStrip.appendChild(makeCell("number corner", "")); - container.appendChild(bottomStrip); - } - - this.parentDiv.appendChild(container); - return true; - } - - getMazeEl(container: HTMLElement): HTMLElement | null { - return container.querySelector("#maze"); - } - - getCellSelector(r: number, c: number): string { - return `.maze-row > div[data-r="${r}"][data-c="${c}"]`; - } - - getCellEl(container: HTMLElement, r: number, c: number): HTMLElement | null { - const maze = this.getMazeEl(container); - if (!maze) return null; - return maze.querySelector(this.getCellSelector(r, c)) as HTMLElement | null; - } - - addCellClass(container: HTMLElement, r: number, c: number, cls: string): void { - this.getCellEl(container, r, c)?.classList.add(cls); - } - - removeCellClass(container: HTMLElement, r: number, c: number, cls: string): void { - this.getCellEl(container, r, c)?.classList.remove(cls); - } - - toggleCellClass(container: HTMLElement, r: number, c: number, cls: string): void { - this.getCellEl(container, r, c)?.classList.toggle(cls); - } -} diff --git a/src_old/game.css b/src_old/game.css deleted file mode 100644 index 266b7c4..0000000 --- a/src_old/game.css +++ /dev/null @@ -1,232 +0,0 @@ -/* ========================================================= - ๐Ÿ”ง Controls (tweak these) - ========================================================= */ -:root { - --maze-cell-size: 24px; - --maze-number-font-size: 14px; -} - -/* ========================================================= - Container & Maze Base - ========================================================= */ -#maze_container { - position: relative; - margin: 1em auto; - width: fit-content; -} - -#maze { - position: relative; - background-color: var(--card, #1e2030); - background-image: - radial-gradient(circle at 0% 0%, transparent 50%, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 50%), - radial-gradient(circle at center, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 50%, transparent 50%), - radial-gradient(circle at 100% 100%, transparent 50%, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 50%), - radial-gradient(circle at 0% 100%, transparent 50%, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 50%), - radial-gradient(circle at 100% 0%, transparent 50%, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 50%); - background-size: 8em 8em; - border-radius: 0.5rem; - box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 5%, transparent); -} - -/* ========================================================= - Layout primitives - ========================================================= */ -#maze .maze-row, -#maze .number-strip { - display: flex; /* rows of equal-width cells */ -} - -/* Every grid box (maze cells + number cells) shares the same size */ -#maze .maze-row > div, -#maze .number { - width: var(--maze-cell-size); - height: var(--maze-cell-size); - display: flex; - align-items: center; - justify-content: center; -} - -/* Number cells (rows/cols) */ -#maze .number { - color: var(--muted-foreground, #828bb8); - font-weight: 600; - font-family: "Fira Code", monospace; - font-size: var(--maze-number-font-size); - line-height: 1; - opacity: 0.85; - user-select: none; -} - -#maze .number.corner { /* top-left/top-right/bottom corners */ - width: var(--maze-cell-size); -} - -/* Relative highlight (your tweak) */ -#maze.is-relative .number.is-active { - background: var(--accent, #ffc777); - border-radius: 2px; - font-size: 1.5rem; - color: var(--accent-foreground, #1b1d2b); -} - -/* ========================================================= - Maze cells visuals (โš ๏ธ scoped to real cells only) - ========================================================= */ -/* Base cell box */ -#maze .maze-row > div[data-r][data-c] { - position: relative; - transition: background-color 0.2s ease; -} - -/* ๐ŸŸข Corridors (non-wall/door/sentinel) */ -#maze .maze-row > div[data-r][data-c]:not(.wall):not(.door):not(.sentinel) { - background-color: var(--muted, #2f334d); - background-image: - linear-gradient(135deg, color-mix(in srgb, var(--primary, #82aaff) 8%, transparent) 25%, transparent 25%), - linear-gradient(225deg, color-mix(in srgb, var(--primary, #82aaff) 6%, transparent) 25%, transparent 25%), - linear-gradient(45deg, color-mix(in srgb, var(--primary, #82aaff) 4%, transparent) 25%, transparent 25%), - linear-gradient(315deg, color-mix(in srgb, var(--primary, #82aaff) 3%, transparent) 25%, transparent 25%); - background-size: var(--maze-cell-size) var(--maze-cell-size); - box-shadow: inset 0 0 3px rgba(27,29,43,0.4); -} - -/* ๐Ÿงฑ Walls and exits */ -#maze .maze-row > div[data-r][data-c].wall, -#maze .maze-row > div[data-r][data-c].nubbin.wall, -#maze .maze-row > div[data-r][data-c].door.exit { - background-color: var(--background, #222436); - background-image: - linear-gradient(45deg, rgba(27,29,43,0.5) 45%, transparent 55%), - linear-gradient(to bottom, rgba(27,29,43,0.4) 45%, transparent 55%); - background-size: calc(var(--maze-cell-size) * 0.5) calc(var(--maze-cell-size) * 0.5); - box-shadow: inset 0 0 3px rgba(27,29,43,0.6); -} - -/* ๐Ÿ”’ Door (exit) โ€” FIXED BACKGROUND */ -#maze .maze-row > div[data-r][data-c].door.exit { - background: radial-gradient(circle at center, var(--chart-4, #4fd6be) 10%, var(--card, #1e2030) 90%) !important; - box-shadow: - 0 0 6px color-mix(in srgb, var(--chart-4, #4fd6be) 50%, transparent), - inset 0 0 4px color-mix(in srgb, var(--chart-4, #4fd6be) 30%, transparent); - border-radius: 3px; - z-index: 2; -} - -#maze .maze-row > div[data-r][data-c].door.exit::after { - content: "\1F6AA"; - color: var(--chart-4, #4fd6be); - text-shadow: 0 0 6px color-mix(in srgb, var(--chart-4, #4fd6be) 60%, transparent); - position: absolute; - top: -4px; - left: -2px; - z-index: 3; -} - -/* ๐Ÿ’Ž Decorations (nubbins) */ -#maze .maze-row > div[data-r][data-c]::after { - position: absolute; - left: -3px; - top: -4px; - text-align: center; - text-shadow: 0 0 3px rgba(0,0,0,0.6); - font-size: calc(var(--maze-cell-size) * 0.75); /* scale with cell */ - z-index: 10; -} - -#maze .maze-row > div[data-r][data-c].nubbin::after { content: "\1F33C"; } -#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(3n)::after { content: "\1F344"; } -#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(5n)::after { content: "\1F33B"; } -#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(7n)::after { content: "\1F48E"; } -#maze .maze-row > div[data-r][data-c].nubbin.wall::after { content: ""; } - -/* ๐Ÿง Hero */ -#maze .maze-row > div[data-r][data-c].hero { - background-color: var(--primary, #82aaff); - border-radius: 50%; - box-shadow: 0 0 8px color-mix(in srgb, var(--primary, #82aaff) 70%, transparent); -} -#maze.face-right .maze-row > div[data-r][data-c].hero::after { transform: scale(-1, 1); } -#maze .maze-row > div[data-r][data-c].hero::after { content: "\1F6B6" !important; } - -/* ๐Ÿ Sentinels */ -#maze .maze-row > div[data-r][data-c].sentinel.wall { background: transparent; } -#maze .maze-row > div[data-r][data-c].sentinel.wall::after { - content: "\1F40A"; - color: var(--muted-foreground, #828bb8); -} -#maze .maze-row > div[data-r][data-c].sentinel.wall:nth-of-type(3n)::after { content: "\1F40D"; } - -/* ๐Ÿ—๏ธ Key */ -#maze .maze-row > div[data-r][data-c].key::after { - content: "\1F511"; - color: var(--accent, #ffc777); - text-shadow: 0 0 6px color-mix(in srgb, var(--accent, #ffc777) 80%, transparent); -} - -/* ========================================================= - Score / message bar (unchanged) - ========================================================= */ -#maze_output { - display: flex; - margin: 0.5em auto; -} -#maze_score, -#maze_message { - font-family: "Fira Code", monospace; - font-weight: 600; - font-size: 1em; - color: var(--foreground, #c8d3f5); -} -#maze_score { - flex: 1; - text-align: left; -} -#maze_score::before { - content: "Score: "; - color: var(--muted-foreground, #828bb8); -} -#maze_score.has-key::after { - content: "\00a0\1F511"; - color: var(--accent, #ffc777); -} -#maze_message { - flex: 3; - text-align: right; -} - -/* Subtle step trail effect for counted moves */ -#maze .maze-row > div[data-r][data-c].trail { - box-shadow: - inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 60%, transparent), - 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 35%, transparent); - animation: maze-trail-fade 16ms ease-out; -} - -/* ===== Invalid move "red mist" around hero ===== */ -#maze .maze-row > div[data-r][data-c].invalid-move { - animation: maze-invalid-pulse 180ms ease-out; - box-shadow: - 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 85%, transparent), - 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 45%, transparent); -} - -@keyframes maze-invalid-pulse { - 0% { - transform: scale(1.02); - box-shadow: - 0 0 16px color-mix(in srgb, var(--destructive, #c53b53) 95%, transparent), - 0 0 28px color-mix(in srgb, var(--destructive, #c53b53) 55%, transparent); - } - 100% { - transform: none; - box-shadow: none; - } -} - - -@keyframes maze-trail-fade { - from { background-color: color-mix(in srgb, var(--primary, #82aaff) 25%, transparent); } - to { background-color: transparent; } -} - diff --git a/src_old/hooks/useTimer.ts b/src_old/hooks/useTimer.ts deleted file mode 100644 index fb766d5..0000000 --- a/src_old/hooks/useTimer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useRef, useCallback, useEffect } from "react"; - -/** - * useTimer โ€“ a low-level timer that updates with requestAnimationFrame. - * - * @returns { timeValue, startTimer, stopTimer, resetTimer } - */ -export function useTimer() { - const [timeValue, setTimeValue] = useState(0); - const startTimeRef = useRef(null); - const frameRef = useRef(null); - const runningRef = useRef(false); - - // ๐Ÿ•’ Start timer - const startTimer = useCallback(() => { - if (runningRef.current) return; runningRef.current = true; - startTimeRef.current = performance.now(); - - const tick = () => { - if (!runningRef.current) return; - const now = performance.now(); - const elapsed = now - (startTimeRef.current ?? now); - setTimeValue(elapsed); - frameRef.current = requestAnimationFrame(tick); - }; - - frameRef.current = requestAnimationFrame(tick); - }, []); - - // โน Stop/pause timer - const stopTimer = useCallback(() => { - runningRef.current = false; - if (frameRef.current) { - cancelAnimationFrame(frameRef.current); - frameRef.current = null; - } - }, []); - - // ๐Ÿ”„ Reset timer - const resetTimer = useCallback(() => { - stopTimer(); - setTimeValue(0); - startTimeRef.current = null; - }, [stopTimer]); - - // ๐Ÿงน Cleanup on unmount - useEffect(() => { - return () => { - if (frameRef.current) cancelAnimationFrame(frameRef.current); - }; - }, []); - - return { timeValue, startTimer, stopTimer, resetTimer }; -} - -export type UseTimerReturn = ReturnType; - diff --git a/src_old/index.ts b/src_old/index.ts deleted file mode 100644 index 0c2a959..0000000 --- a/src_old/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './game.css'; -export { MazeGenerator } from "./MazeGenerator"; -export { MazeRenderer } from "./MazeRenderer"; -export * from './useGame'; -export * from './types'; - diff --git a/src_old/types.ts b/src_old/types.ts deleted file mode 100644 index 2e6c5d4..0000000 --- a/src_old/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useScore } from "./useScore"; -import { useGame } from "./useGame"; -export type UseGameType = ReturnType; -export type UseScoreType = ReturnType; - -export type GamePhase = "idle" | "playing"; -export type PlayStatus = "started" | "hasKey" | "game-over" | "game-won"; -export type GameStatus = "waiting" | "started" | "hasKey" | "game-over" | "game-won"; - -export const isPlaying = (status: GameStatus): status is PlayStatus => - status === "started" || status === "hasKey" || status === "game-over" || status === "game-won"; - -export const getGamePhase = (status: GameStatus): GamePhase => - status === "waiting" ? "idle" : "playing"; - -export type KeyLogEntry = { key: string; timestamp: number }; -export type KeyLogProvider = { keyLog: KeyLogEntry[] }; - -export type GameScoreContext = { - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; - stopGame: () => void; - - // Optional โ€” only Maze-type games provide these - mazeManager?: { - mazeInstanceRef?: { current?: any }; - }; - playerManager?: { - playerPos?: { r: number; c: number } | null; - }; -}; - -export type UseScoreParams = { - gameContext: GameScoreContext; - keyManager?: KeyLogProvider; -}; - -export type PositionTag = "entrance" | "exit" | "key" | "hero"; - -export type Coord = [number, number]; - -export type CellTag = "wall" | "door" | PositionTag; - -export type MazeCell = CellTag[]; - -export interface MazeData { - maze: MazeCell[][]; - width: number; - height: number; - cols: number; - rows: number; - totalSteps: number; -} - diff --git a/src_old/useGame/index.ts b/src_old/useGame/index.ts deleted file mode 100644 index 5b2f75f..0000000 --- a/src_old/useGame/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useGame'; -export * from './useKeyBindings'; diff --git a/src_old/useGame/useGame.ts b/src_old/useGame/useGame.ts deleted file mode 100644 index 81758bf..0000000 --- a/src_old/useGame/useGame.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useMaze } from "./useMaze"; -import { usePlayer } from "./usePlayer"; -import { useGameStatus } from "./useGameStatus"; -import { useHeroRender } from "./useHeroRender"; -import { useScore } from "../useScore"; -import type { UseScoreType } from '../types' -import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings"; - -export function useGame(cols: number, rows: number, platformHook?: unknown) { - const mazeManager = useMaze(cols, rows); - const playerManager = usePlayer(mazeManager); - const gameManager = useGameStatus(mazeManager, playerManager); - useHeroRender({ mazeManager, playerManager }); - - const { render } = mazeManager; - const { movePlayer } = playerManager; - const { gameStatus, setGameStatus, startGame, stopGame } = gameManager; - const { containerRef } = mazeManager; - - - // ๐ŸŽฎ key bindings integrated - const keyBindings: UseKeyBindingsType = useKeyBindings({ - gameManager: { - movePlayer: (dr: number, dc: number, count: number = 1) => - movePlayer(dr, dc, count, gameStatus, setGameStatus), - gameStatus, - mazeManager, - playerManager, - } as any, // minimal shape needed by useKeyBindings - }); - - // ๐Ÿ† scoring integrated โ€” pass the full context it needs (incl. maze/player) - const scoreManager: UseScoreType = useScore({ - gameContext: { - gameStatus, - setGameStatus, - stopGame, - mazeManager, // <-- provide for useScorePaths - playerManager, // <-- provide for step counting - }, - keyManager: { keyLog: keyBindings.keyLog }, // minimal info; avoids type cycles - }); - - const fullGameManager = { - containerRef, - gameStatus, - mazeManager, - playerManager, - render, - startGame, - togglePause, - movePlayer: (dr: number, dc: number, count: number = 1) => - movePlayer(dr, dc, count, gameStatus, setGameStatus), - stopGame, - - // key bindings - ...keyBindings, - - // scoring - scoreManager, - }; - - if (typeof platformHook === 'function') { - platformHook(fullGameManager); - } - - return fullGameManager; -} - -export type { GameStatus } from "../types"; diff --git a/src_old/useGame/useGameStatus.ts b/src_old/useGame/useGameStatus.ts deleted file mode 100644 index 8db99a4..0000000 --- a/src_old/useGame/useGameStatus.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback, useState, useMemo } from "react"; -import type { UseMazeType } from "./useMaze"; -import type { UsePlayerType } from "./usePlayer"; -import type { GameStatus } from "../types"; -import { getGamePhase, isPlaying } from "../types"; - -export type { GameStatus } from "../types"; - -export function useGameStatus(mazeManager: UseMazeType, playerManager: UsePlayerType) { - const { mazeRef, containerRef } = mazeManager; - const { setPlayerPos } = playerManager; - const [gameStatus, setGameStatus] = useState("waiting"); - - const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]); - const playStatus = useMemo( - () => (isPlaying(gameStatus) ? gameStatus : null), - [gameStatus] - ); - - // ๐Ÿ•น Start the game (place hero) - const startGame = useCallback(() => { - const m = mazeRef.current; - if (!m.length) return; - - for (let c = 0; c < m[0].length; c++) { - if (m[m.length - 1][c].includes("entrance")) { - setPlayerPos({ r: m.length - 1, c }); - break; - } - } - - setGameStatus("started"); - }, [mazeRef, setPlayerPos]); - - // ๐Ÿง Stop / exit game - const stopGame = useCallback(() => { - setPlayerPos(null); - setGameStatus("waiting"); - const container = containerRef.current; - container?.querySelector("#maze")?.classList.remove("finished"); - container?.querySelectorAll(".hero").forEach((el) => el.classList.remove("hero")); - }, [setPlayerPos, containerRef]); - - return { - gameStatus, - gamePhase, - playStatus, - setGameStatus, - startGame, - stopGame, - }; -} - -export type UseGameStatusType = ReturnType; - diff --git a/src_old/useGame/useHeroRender.ts b/src_old/useGame/useHeroRender.ts deleted file mode 100644 index 4c0ea88..0000000 --- a/src_old/useGame/useHeroRender.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useEffect } from "react"; -import type { UseMazeType } from "./useMaze"; -import type { UsePlayerType } from "./usePlayer"; - -type UseHeroRenderParams = { - mazeManager: UseMazeType; - playerManager: UsePlayerType; - relative?: boolean; -}; - -export function useHeroRender(params: UseHeroRenderParams) { - const { mazeManager, playerManager, relative = true } = params; - const { containerRef } = mazeManager; - const { playerPos } = playerManager; - - const applyRelativeNumbers = (mazeDiv: HTMLElement, R: number, C: number) => { - // counts provided by caller - const rows = Number(mazeDiv.dataset.rows || 0); - const cols = Number(mazeDiv.dataset.cols || 0); - - // rows: borders blank; active row shows its ABSOLUTE number (r), others show |r-R| - const rowNums = mazeDiv.querySelectorAll(".number.row[data-r]"); - rowNums.forEach((el) => { - const r = Number(el.getAttribute("data-r")); - if (r <= 0 || r >= rows) { - el.textContent = ""; - el.classList.remove("is-active"); - return; - } - el.textContent = r === R ? String(r) : String(Math.abs(r - R)); - el.classList.toggle("is-active", r === R); - }); - - // cols (top & bottom): borders blank; active col shows ABSOLUTE number (c), others show |c-C| - const colNums = mazeDiv.querySelectorAll(".number.col[data-c]"); - colNums.forEach((el) => { - const c = Number(el.getAttribute("data-c")); - if (c <= 0 || c >= cols) { - el.textContent = ""; - el.classList.remove("is-active"); - return; - } - el.textContent = c === C ? String(c) : String(Math.abs(c - C)); - el.classList.toggle("is-active", c === C); - }); - - mazeDiv.classList.add("is-relative"); - }; - - useEffect(() => { - if (!playerPos) return; - - const container = containerRef.current; - if (!container) return; - - const mazeDiv = container.querySelector("#maze") as HTMLElement | null; - if (!mazeDiv) return; - - // Remove any previous hero marker - mazeDiv.querySelectorAll(".hero").forEach((el) => el.classList.remove("hero")); - - // Place hero (select exact cell by coordinates) - const selector = `.maze-row > div[data-r="${playerPos.r}"][data-c="${playerPos.c}"]`; - const cell = mazeDiv.querySelector(selector) as HTMLElement | null; - if (cell) cell.classList.add("hero"); - - // Relative numbering (absolute on the active line/col) - if (relative) { - // Provide counts so helper can know borders - mazeDiv.dataset.rows = String(mazeDiv.querySelectorAll(".maze-row").length); - mazeDiv.dataset.cols = String( - mazeDiv - .querySelector(".maze-row > div[data-r][data-c]") - ?.parentElement?.querySelectorAll(':scope > div[data-r][data-c]').length || 0 - ); - applyRelativeNumbers(mazeDiv, playerPos.r, playerPos.c); - } - }, [playerPos, containerRef]); -} - -export type UseHeroRenderType = ReturnType; diff --git a/src_old/useGame/useKeyBindings.ts b/src_old/useGame/useKeyBindings.ts deleted file mode 100644 index 87cf73d..0000000 --- a/src_old/useGame/useKeyBindings.ts +++ /dev/null @@ -1,259 +0,0 @@ -import type { GameStatus } from "../types"; -import { useEffect, useRef, useState } from "react"; -import type { KeyLogEntry } from "../types"; - -export type GameBindingContext = { - movePlayer: (dr: number, dc: number, count?: number) => void; - gameStatus: GameStatus; - mazeManager: any; - playerManager: any; -}; - -type UseMazeKeyBindingsParams = { - gameManager: GameBindingContext; -}; - -type Motion = { dr: number; dc: number; steps: number }; - -const RELEVANT_KEYS = new Set([ - "h", "H", "j", "J", "k", "K", "l", "L", ".", "^", "$", "g", "G", "0", - "1", "2", "3", "4", "5", "6", "7", "8", "9" -]); - -export const useKeyBindings = ({ gameManager }: UseMazeKeyBindingsParams) => { - const { movePlayer, gameStatus } = gameManager; - - const [keyLog, setKeyLog] = useState([]); - const logRef = useRef([]); - const countRef = useRef(""); - const lastMotionRef = useRef(null); - const lastKeyRef = useRef(""); - - const relevantKeys = RELEVANT_KEYS; - - const clearLog = () => { - logRef.current = []; - setKeyLog([]); - }; - - const recordKey = (key: string) => { - const entry = { key, timestamp: performance.now() }; - logRef.current.push(entry); - setKeyLog([...logRef.current]); - }; - - const resetCount = () => { countRef.current = ""; }; - - const findAnchorTarget = (dir: "left" | "right") => { - const m = gameManager.mazeManager.mazeRef.current; - const pos = gameManager.playerManager.playerPos; - if (!m.length || !pos) return null; - - const colsCount = m[0]?.length ?? 0; - const r = pos.r; - const c = pos.c; - - if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; - - const step = dir === "left" ? -1 : 1; - let cc = c; - - while (true) { - const next = cc + step; - if (next < 0 || next >= colsCount) return null; - if (m[r][next].includes("wall")) { - const wallIdx = next; - const expectedWallIdx = dir === "left" ? 0 : colsCount - 1; - const expectedCellIdx = dir === "left" ? 1 : colsCount - 2; - - if (wallIdx !== expectedWallIdx) return null; - if (cc !== expectedCellIdx) return null; - if (cc === c) return null; - - return { r, cTarget: cc }; - } - cc = next; - } - }; - - const findColumnAnchorTarget = (dir: "top" | "bottom") => { - const m = gameManager.mazeManager.mazeRef.current; - const pos = gameManager.playerManager.playerPos; - if (!m.length || !pos) return null; - - const rowsCount = m.length; - const r = pos.r; - const c = pos.c; - - if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; - - const step = dir === "top" ? -1 : 1; - let rr = r; - - while (true) { - const next = rr + step; - if (next < 0 || next >= rowsCount) return null; - if (m[next][c].includes("wall")) { - const wallIdx = next; - const expectedWallIdx = dir === "top" ? 0 : rowsCount - 1; - const expectedCellIdx = dir === "top" ? 1 : rowsCount - 2; - - if (wallIdx !== expectedWallIdx) return null; - if (rr !== expectedCellIdx) return null; - if (rr === r) return null; - - return { rTarget: rr, c }; - } - rr = next; - } - }; - - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (["waiting", "game-over", "game-won"].includes(gameStatus)) return; - - // "0" behaves like Vim: beginning of line - if (e.key === "0" && countRef.current === "") { - recordKey("0"); - resetCount(); - - const anchor = findAnchorTarget("left"); - if (!anchor) { - window.dispatchEvent(new Event("maze-invalid")); - return; - } - - const pos = gameManager.playerManager.playerPos!; - const dr = 0; - const dc = anchor.cTarget > pos.c ? 1 : -1; - const steps = Math.abs(anchor.cTarget - pos.c); - - movePlayer(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - return; - } - - const isDigit = e.key >= "0" && e.key <= "9"; - if (isDigit) { - if (e.key === "0" && countRef.current === "") return; - recordKey(e.key); - countRef.current += e.key; - return; - } - - if (e.key === ".") { - recordKey("."); - const last = lastMotionRef.current; - if (!last) return; - const steps = Math.max(1, parseInt(countRef.current || String(last.steps), 10)); - resetCount(); - movePlayer(last.dr, last.dc, steps); - lastMotionRef.current = { dr: last.dr, dc: last.dc, steps }; - return; - } - - if (e.key === "^" || e.key === "$") { - recordKey(e.key); - resetCount(); - - const dir = e.key === "^" ? "left" : "right"; - const anchor = findAnchorTarget(dir); - if (!anchor) { - window.dispatchEvent(new Event("maze-invalid")); - return; - } - - const pos = gameManager.playerManager.playerPos!; - const dr = 0; - const dc = anchor.cTarget > pos.c ? 1 : -1; - const steps = Math.abs(anchor.cTarget - pos.c); - - movePlayer(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - return; - } - - if (e.key === "g" || e.key === "G") { - if (e.key === "g") { - if (lastKeyRef.current === "g") { - recordKey(e.key); - resetCount(); - lastKeyRef.current = ""; - - const anchor = findColumnAnchorTarget("top"); - if (!anchor) { - window.dispatchEvent(new Event("maze-invalid")); - return; - } - - const pos = gameManager.playerManager.playerPos!; - const dr = anchor.rTarget > pos.r ? 1 : -1; - const dc = 0; - const steps = Math.abs(anchor.rTarget - pos.r); - - movePlayer(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - return; - } else { - recordKey(e.key); - lastKeyRef.current = "g"; - return; - } - } else { - recordKey(e.key); - resetCount(); - lastKeyRef.current = ""; - - const anchor = findColumnAnchorTarget("bottom"); - if (!anchor) { - window.dispatchEvent(new Event("maze-invalid")); - return; - } - - const pos = gameManager.playerManager.playerPos!; - const dr = anchor.rTarget > pos.r ? 1 : -1; - const dc = 0; - const steps = Math.abs(anchor.rTarget - pos.r); - - movePlayer(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - return; - } - } - - if (!relevantKeys.has(e.key)) return; - - recordKey(e.key); - - let dr = 0, dc = 0; - switch (e.key) { - case "h": - case "H": dr = 0; dc = -1; break; - case "j": - case "J": dr = 1; dc = 0; break; - case "k": - case "K": dr = -1; dc = 0; break; - case "l": - case "L": dr = 0; dc = 1; break; - default: return; - } - - const steps = Math.max(1, parseInt(countRef.current || "1", 10)); - resetCount(); - movePlayer(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - }; - - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [movePlayer, gameStatus, gameManager]); - - return { - keyLog, - clearLog, - resetCount, - getLog: () => logRef.current, - }; -}; - -export type UseKeyBindingsType = ReturnType; diff --git a/src_old/useGame/useMaze.ts b/src_old/useGame/useMaze.ts deleted file mode 100644 index e037e61..0000000 --- a/src_old/useGame/useMaze.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef, useCallback } from "react"; -import { MazeGenerator } from "../MazeGenerator"; -import { MazeRenderer } from "../MazeRenderer"; -import type { MazeCell } from "../types"; - -export function useMaze(cols: number, rows: number) { - const containerRef = useRef(null); - const mazeRef = useRef([]); - const mazeInstanceRef = useRef(null); - const rendererRef = useRef(null); - - const initMaze = useCallback( - (autoStart = false, startCallback?: () => void) => { - const container = containerRef.current; - if (!container) return; - - container.innerHTML = ""; - - const generator = new MazeGenerator(cols, rows); - generator.placeKey(); - - const renderer = new MazeRenderer(generator.getData(), true); - renderer.display(container); - - mazeRef.current = generator.maze; - mazeInstanceRef.current = generator; - rendererRef.current = renderer; - - if (autoStart && startCallback) { - setTimeout(() => startCallback(), 0); - } - }, - [cols, rows] - ); - - return { - containerRef, - mazeRef, - mazeInstanceRef, - rendererRef, - initMaze, - }; -} - -export type UseMazeType = ReturnType; diff --git a/src_old/useGame/usePlayer.ts b/src_old/useGame/usePlayer.ts deleted file mode 100644 index 3098699..0000000 --- a/src_old/useGame/usePlayer.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { useRef, useState, useCallback, useEffect } from "react"; -import type { UseMazeType } from "./useMaze"; -import type { GameStatus } from "./useGame"; - -export function usePlayer(mazeManager: UseMazeType) { - const { mazeRef, containerRef } = mazeManager; - const [playerPos, setPlayerPos] = useState<{ r: number; c: number } | null>(null); - - // animation gate so we don't accept new motions mid-anim - const animatingRef = useRef(false); - - // Helper: get the maze cell element by [r,c] via data attributes - const getCellEl = useCallback((r: number, c: number): HTMLElement | null => { - const container = containerRef.current; - if (!container) return null; - const mazeDiv = container.querySelector("#maze"); - if (!mazeDiv) return null; - const selector = `.maze-row > div[data-r="${r}"][data-c="${c}"]`; - return mazeDiv.querySelector(selector) as HTMLElement | null; - }, [containerRef]); - - // Visual: briefly highlight a visited cell - const leaveTrail = useCallback((r: number, c: number) => { - const el = getCellEl(r, c); - if (!el) return; - el.classList.add("trail"); - setTimeout(() => el.classList.remove("trail"), 220); - }, [getCellEl]); - - // Visual: flash an invalid move on the hero's current cell - const flashInvalid = useCallback(() => { - if (!playerPos) return; - const el = getCellEl(playerPos.r, playerPos.c); - if (!el) return; - el.classList.add("invalid-move"); - setTimeout(() => el.classList.remove("invalid-move"), 180); - }, [playerPos, getCellEl]); - - // Allow other modules (e.g., keybindings) to signal an invalid attempt - useEffect(() => { - const onInvalid = () => flashInvalid(); - window.addEventListener("maze-invalid", onInvalid); - return () => window.removeEventListener("maze-invalid", onInvalid); - }, [flashInvalid]); - - /** - * movePlayer โ€” supports counted motions with an all-or-nothing rule. - * If steps > 1 and the path is valid, animates quickly (many steps per frame), - * then applies interactions (key/exit) on the final tile. - */ - const movePlayer = useCallback( - ( - dr: number, - dc: number, - steps: number | undefined, - gameStatus: GameStatus, - setGameStatus: (s: GameStatus) => void - ) => { - const m = mazeRef.current; - if (!playerPos || !m.length) return; - if (gameStatus !== "started" && gameStatus !== "hasKey") return; - if (animatingRef.current) return; // ignore input mid-animation - - const n = Math.max(1, steps ?? 1); - - // ----- Preflight: simulate n steps; abort if any step is invalid ----- - const r0 = playerPos.r; - const c0 = playerPos.c; - let r = r0; - let c = c0; - let valid = true; - - for (let i = 1; i <= n; i++) { - r += dr; - c += dc; - if (!m[r] || !m[r][c] || m[r][c].includes("wall")) { - valid = false; - break; - } - if (m[r][c].includes("exit") && gameStatus !== "hasKey") { - valid = false; - break; - } - } - if (!valid) { - flashInvalid(); // โŒ all-or-nothing: show feedback and do nothing - return; - } - - // ----- Animated multi-step (RAF, fast: multiple steps per frame, short duration) ----- - if (n > 1) { - animatingRef.current = true; - - let i = 0; - let rNow = r0; - let cNow = c0; - let cancelled = false; - let rafId: number | null = null; - - // Target total duration (tweak): snappy/fast - const TOTAL_MS = 64; // 48 or 32 for ultra-fast - const framesTarget = Math.max(1, Math.round(TOTAL_MS / 16)); // ~60fps frame estimate - const stepsPerFrame = Math.max(1, Math.ceil(n / framesTarget)); - - const cancel = () => { - cancelled = true; - if (rafId !== null) cancelAnimationFrame(rafId); - window.removeEventListener("keydown", onEsc); - animatingRef.current = false; - }; - const onEsc = (ev: KeyboardEvent) => { - if (ev.key === "Escape") cancel(); - }; - window.addEventListener("keydown", onEsc); - - const tick = () => { - if (cancelled) return; - - // churn through multiple steps in one frame - let processed = 0; - while (processed < stepsPerFrame && i < n) { - i += 1; - rNow += dr; - cNow += dc; - leaveTrail(rNow, cNow); // visual only - processed += 1; - } - - // move hero to the latest stepped cell once per frame - setPlayerPos({ r: rNow, c: cNow }); - window.dispatchEvent(new CustomEvent("maze-logic-tick", { detail: { t: performance.now() } })); - - if (i < n) { - rafId = requestAnimationFrame(tick); - } else { - // final-tile interactions - if (m[rNow][cNow].includes("key")) { - setGameStatus("hasKey"); - m[rNow][cNow] = []; - const container = containerRef.current; - const mazeDiv = container?.querySelector("#maze"); - const keyCell = mazeDiv?.querySelector( - `.maze-row > div[data-r="${rNow}"][data-c="${cNow}"]` - ); - keyCell?.classList.remove("key"); - } - if (m[rNow][cNow].includes("exit")) { - setGameStatus("game-won"); - } - window.removeEventListener("keydown", onEsc); - animatingRef.current = false; - } - }; - - rafId = requestAnimationFrame(tick); - return; - } - - // ----- Single-step (no animation) ----- - const newR = r0 + dr; - const newC = c0 + dc; - - if (!m[newR] || !m[newR][newC]) { - flashInvalid(); - return; - } - if (m[newR][newC].includes("wall")) { - flashInvalid(); - return; - } - if (m[newR][newC].includes("exit") && gameStatus !== "hasKey") { - flashInvalid(); - return; - } - - if (m[newR][newC].includes("key")) { - setGameStatus("hasKey"); - m[newR][newC] = []; - const container = containerRef.current; - const mazeDiv = container?.querySelector("#maze"); - const cell = mazeDiv?.querySelector( - `.maze-row > div[data-r="${newR}"][data-c="${newC}"]` - ); - cell?.classList.remove("key"); - } - if (m[newR][newC].includes("exit")) { - setGameStatus("game-won"); - } - - setPlayerPos({ r: newR, c: newC }); - }, - [playerPos, mazeRef, containerRef, flashInvalid, leaveTrail] - ); - - return { - playerPos, - setPlayerPos, - movePlayer, - }; -} - -export type UsePlayerType = ReturnType; diff --git a/src_old/useScore/hooks/useTimer.ts b/src_old/useScore/hooks/useTimer.ts deleted file mode 100644 index 44b0e9e..0000000 --- a/src_old/useScore/hooks/useTimer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -export type UseTimerReturn = { - timeValue: number; - startTimer: () => void; - stopTimer: () => void; - resetTimer: () => void; -}; - -export type UseTimerOptions = { - /** - * Interval (in ms) between updates. Defaults to 100ms. - */ - interval?: number; -}; - -export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { - const { interval = 100 } = options; - const [timeValue, setTimeValue] = useState(0); - - const intervalIdRef = useRef | null>(null); - const offsetRef = useRef(0); - const startTimeRef = useRef(null); - - const update = useCallback(() => { - if (startTimeRef.current === null) { - return; - } - - const now = performance.now(); - setTimeValue(offsetRef.current + (now - startTimeRef.current)); - }, []); - - const startTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - return; - } - - if (startTimeRef.current === null) { - startTimeRef.current = performance.now(); - } - - update(); - intervalIdRef.current = setInterval(update, Math.max(16, interval)); - }, [interval, update]); - - const stopTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - intervalIdRef.current = null; - } - - if (startTimeRef.current !== null) { - update(); - offsetRef.current += performance.now() - startTimeRef.current; - startTimeRef.current = null; - } - }, [update]); - - const resetTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - intervalIdRef.current = null; - } - - startTimeRef.current = null; - offsetRef.current = 0; - setTimeValue(0); - }, []); - - useEffect(() => { - return () => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - } - }; - }, []); - - return { - timeValue, - startTimer, - stopTimer, - resetTimer, - }; -} diff --git a/src_old/useScore/index.ts b/src_old/useScore/index.ts deleted file mode 100644 index 525ce75..0000000 --- a/src_old/useScore/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useScore'; diff --git a/src_old/useScore/useScore.ts b/src_old/useScore/useScore.ts deleted file mode 100644 index 8514393..0000000 --- a/src_old/useScore/useScore.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTimer } from "../hooks/useTimer"; -import { useScoreTime } from "./useScoreTime"; -import { useScorePaths } from "./useScorePaths"; -import type { KeyLogProvider, GameScoreContext } from "../types"; - -type UseScoreParams = { - gameContext: GameScoreContext; - keyManager?: KeyLogProvider; -}; - -export function useScore({ gameContext, keyManager }: UseScoreParams) { - const timer = useTimer(); - const { gameStatus, setGameStatus } = gameContext; - const { timeValue, startTimer, stopTimer, resetTimer } = timer; - - // โœ… useScorePaths always returns distances under these names - const { - entranceToKey = 0, - entranceToExit = 0, - heroToKey = 0, - heroToExit = 0, - } = gameContext.mazeManager - ? useScorePaths({ gameManager: gameContext as any }) - : {}; - - // โœ… use unified naming for internal logic - const distToKey = entranceToKey || heroToKey; - const distToExit = entranceToExit || heroToExit; - - const [finalScore, setFinalScore] = useState(null); - const optimalRef = useRef(0); - const keystrokes = keyManager ? keyManager.keyLog.length : 0; - - // 1๏ธโƒฃ Manage visible wall-clock timer - useScoreTime({ gameStatus, timer }); - - // 2๏ธโƒฃ Compute static optimal path when maze (re)starts - useEffect(() => { - const maze = gameContext.mazeManager?.mazeInstanceRef?.current; - if (!maze) return; - - const k = maze.getDistance("entrance", "key"); - const e = maze.getDistance("key", "exit"); - const total = k + e; - - if (Number.isFinite(total) && total > 0) { - optimalRef.current = total; - } - }, [gameStatus, gameContext.mazeManager?.mazeInstanceRef]); - - const optimalSteps = optimalRef.current; - - // 3๏ธโƒฃ Efficiency (keystrokes vs optimal steps) - const efficiency = - optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0; - - // 4๏ธโƒฃ Logic time (internal motion time) - const [logicFirst, setLogicFirst] = useState(null); - const [logicLast, setLogicLast] = useState(null); - - useEffect(() => { - if (gameStatus === "started") { - setLogicFirst(null); - setLogicLast(null); - setPlayerSteps(0); - prevPosRef.current = null; - } - }, [gameStatus]); - - useEffect(() => { - const onTick = (e: Event) => { - const t = - (e as CustomEvent<{ t?: number }>).detail?.t ?? performance.now(); - setLogicFirst((prev) => prev ?? t); - setLogicLast(t); - }; - window.addEventListener("maze-logic-tick", onTick as EventListener); - return () => - window.removeEventListener("maze-logic-tick", onTick as EventListener); - }, []); - - // 5๏ธโƒฃ Actual player steps - const [playerSteps, setPlayerSteps] = useState(0); - const prevPosRef = useRef<{ r: number; c: number } | null>(null); - - useEffect(() => { - const pos = gameContext.playerManager?.playerPos; - if (!pos) { - prevPosRef.current = null; - return; - } - - if (!["started", "hasKey"].includes(gameStatus)) { - prevPosRef.current = pos; - return; - } - - const prev = prevPosRef.current; - prevPosRef.current = pos; - if (!prev) return; - - const delta = Math.abs(pos.r - prev.r) + Math.abs(pos.c - prev.c); - if (delta > 0) setPlayerSteps((s) => s + delta); - }, [gameStatus, gameContext.playerManager?.playerPos]); - - // 6๏ธโƒฃ Auto game-over on inefficiency - useEffect(() => { - if (gameStatus === "started" && efficiency > 150) { - setGameStatus("game-over"); - } - }, [efficiency, gameStatus, setGameStatus]); - - // 7๏ธโƒฃ Final score on win - useEffect(() => { - if (gameStatus !== "game-won") return; - - const logicMs = - logicFirst != null && logicLast != null - ? Math.max(1, logicLast - logicFirst) - : timeValue; - - const optimalMs = optimalSteps * 200; - const ratio = Math.max(0.1, optimalMs / logicMs); - const score = Math.min(100000, Math.round(ratio * 100000)); - - setFinalScore(score); - }, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps, playerSteps]); - - // โœ… Return clean, typed object - return { - timeValue, - startTimer, - stopTimer, - resetTimer, - distToKey, - distToExit, - keystrokes, - optimalSteps, - efficiency, - finalScore, - }; -} diff --git a/src_old/useScore/useScorePaths.ts b/src_old/useScore/useScorePaths.ts deleted file mode 100644 index 880dc6b..0000000 --- a/src_old/useScore/useScorePaths.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useState } from "react"; -import type { GameStatus } from "../types"; - -// explicit so as not to break with circular deps, but should be UseGameType -type UseScorePathsParams = { - gameManager: { - gameStatus: GameStatus; - mazeManager: { - mazeInstanceRef: { current: any }; - }; - playerManager: { - playerPos: { r: number; c: number } | null; - }; - }; -}; - -export function useScorePaths({ gameManager }: UseScorePathsParams) { - const { gameStatus } = gameManager; - const { mazeInstanceRef } = gameManager.mazeManager; - const { playerPos } = gameManager.playerManager; - - const [distances, setDistances] = useState({ - entranceToKey: Infinity, - entranceToExit: Infinity, - heroToKey: Infinity, - heroToExit: Infinity, - }); - - // ๐Ÿงญ 1๏ธโƒฃ Calculate static distances (entrance-based) when game starts - useEffect(() => { - if (gameStatus !== "started") return; - - const maze = mazeInstanceRef.current; - if (!maze) return; - - setDistances((prev) => ({ - ...prev, - entranceToKey: maze.getDistance("entrance", "key"), - entranceToExit: maze.getDistance("entrance", "exit"), - })); - }, [gameStatus, mazeInstanceRef]); - - // ๐Ÿšถ 2๏ธโƒฃ Update dynamic distances (hero-based) on every move - useEffect(() => { - const maze = mazeInstanceRef.current; - if (!maze || !playerPos) return; - - setDistances((prev) => ({ - ...prev, - heroToKey: maze.getDistance("hero", "key", [playerPos.r, playerPos.c]), - heroToExit: maze.getDistance("hero", "exit", [playerPos.r, playerPos.c]), - })); - }, [playerPos, mazeInstanceRef]); - - return distances; -} - -export type UseScorePathsReturn = ReturnType; diff --git a/src_old/useScore/useScoreTime.ts b/src_old/useScore/useScoreTime.ts deleted file mode 100644 index fe2514b..0000000 --- a/src_old/useScore/useScoreTime.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { UseTimerReturn } from "./hooks/useTimer"; -import type { GameStatus } from "../types"; - -type UseScoreTimeParams = { - gameStatus: GameStatus; - timer: UseTimerReturn; -}; - -export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { - const prevStatusRef = useRef(null); - const { startTimer, stopTimer, resetTimer } = timer; - - useEffect(() => { - // const prevStatus = prevStatusRef.current; - prevStatusRef.current = gameStatus; - - switch (gameStatus) { - case "started": - // Always reset and start a fresh timer when a new game begins - resetTimer(); - startTimer(); - break; - - case "hasKey": - // Keep running - break; - - case "game-over": - case "game-won": - case "waiting": - // Stop timer when game finishes or is exited - stopTimer(); - break; - } - }, [gameStatus, startTimer, stopTimer, resetTimer]); -} -