working on refactor
This commit is contained in:
+1
-5
@@ -1,6 +1,2 @@
|
|||||||
import './game.css';
|
|
||||||
export { MazeGenerator } from "./MazeGenerator";
|
|
||||||
export { MazeRenderer } from "./MazeRenderer";
|
|
||||||
export * from './useGame';
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './useGame';
|
||||||
|
|||||||
+31
-31
@@ -1,43 +1,38 @@
|
|||||||
import { useScore } from "./useScore";
|
import type { RefObject } from 'react';
|
||||||
import { useGame } from "./useGame";
|
export * from './useGame';
|
||||||
export type UseGameType = ReturnType<typeof useGame>;
|
export * from './useBoard';
|
||||||
export type UseScoreType = ReturnType<typeof useScore>;
|
export * from './useCursor';
|
||||||
|
|
||||||
export type GamePhase = "idle" | "playing";
|
export type GameStatus = 'waiting' | 'started' | 'hasKey' | 'paused' | 'game-over' | 'game-won'
|
||||||
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 =>
|
export type GameManager = { //return type of useGame
|
||||||
status === "started" || status === "hasKey" || status === "game-over" || status === "game-won";
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
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;
|
gameStatus: GameStatus;
|
||||||
setGameStatus: (status: GameStatus) => void;
|
renderBoard: () => void;
|
||||||
stopGame: () => void;
|
startGame: () => void;
|
||||||
|
togglePause: (pause?: boolean) => void;
|
||||||
|
quitGame: () => void;
|
||||||
|
|
||||||
// Optional — only Maze-type games provide these
|
cursor: Cursor;
|
||||||
mazeManager?: {
|
|
||||||
mazeInstanceRef?: { current?: any };
|
|
||||||
};
|
|
||||||
playerManager?: {
|
|
||||||
playerPos?: { r: number; c: number } | null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UseScoreParams = {
|
export type Cursor = { //return type of useCursor (currently useHero)
|
||||||
gameContext: GameScoreContext;
|
position: () => Coord;
|
||||||
keyManager?: KeyLogProvider;
|
mode: () => CursorMode;
|
||||||
};
|
move: (dCols: number, dRows: number, count: number) => void;
|
||||||
|
moveLeft: (count?: number) => void;
|
||||||
|
moveRight: (count?: number) => void;
|
||||||
|
moveUp: (count?: number) => void;
|
||||||
|
moveDown: (count?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CursorMode = 'normal' | 'insert' | 'replace' | 'visual' | 'visual-line';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type PositionTag = "entrance" | "exit" | "key" | "hero";
|
export type PositionTag = "entrance" | "exit" | "key" | "hero";
|
||||||
|
|
||||||
export type Coord = [number, number];
|
export type Coord = { row: number, col: number };
|
||||||
|
|
||||||
export type CellTag = "wall" | "door" | PositionTag;
|
export type CellTag = "wall" | "door" | PositionTag;
|
||||||
|
|
||||||
@@ -52,3 +47,8 @@ export interface MazeData {
|
|||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type GameOptions = {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
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<MazeCell>([]);
|
||||||
|
|
||||||
|
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<T = unknown>(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<T>(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<number | undefined>();
|
||||||
|
const fromExit = this.initArray<number | undefined>();
|
||||||
|
|
||||||
|
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<boolean>(false);
|
||||||
|
const prev = this.initArray<Coord | null>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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<string, string>) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useBoard';
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { MazeGenerator } from "./MazeGenerator";
|
||||||
|
import { MazeRenderer } from "./MazeRenderer";
|
||||||
|
import type { MazeCell } from "../types";
|
||||||
|
|
||||||
|
export function useBoard(cols: number, rows: number) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const mazeRef = useRef<MazeCell[][]>([]);
|
||||||
|
const mazeInstanceRef = useRef<MazeGenerator | null>(null);
|
||||||
|
const rendererRef = useRef<MazeRenderer | null>(null);
|
||||||
|
|
||||||
|
function renderBoard() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerRef,
|
||||||
|
mazeRef,
|
||||||
|
mazeInstanceRef,
|
||||||
|
rendererRef,
|
||||||
|
renderBoard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoardManager = {
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>
|
||||||
|
mazeRef: RefObject<MazeCell[][]>
|
||||||
|
mazeInstanceRef: RefObject<MazeGenerator | null>
|
||||||
|
rendererRef: RefObject<MazeRenderer | null>
|
||||||
|
renderBoard: () => void
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useCursor';
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import type { Cursor, CursorMode, Coord, BoardManager } from '../types'
|
||||||
|
|
||||||
|
export function useCursor(board: BoardManager): Cursor {
|
||||||
|
const position = useRef<Coord>({ row: 0, col: 0 })
|
||||||
|
const mode = useRef<CursorMode>('normal')
|
||||||
|
|
||||||
|
function move(dCols: number, dRows: number, count: number = 1) {
|
||||||
|
|
||||||
|
}
|
||||||
|
function moveLeft(count = 1) {
|
||||||
|
// TODO: implement move left
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRight(count = 1) {
|
||||||
|
// TODO: implement move right
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp(count = 1) {
|
||||||
|
// TODO: implement move up
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown(count = 1) {
|
||||||
|
// TODO: implement move down
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: () => position.current,
|
||||||
|
mode: () => mode.current,
|
||||||
|
move,
|
||||||
|
moveLeft,
|
||||||
|
moveRight,
|
||||||
|
moveUp,
|
||||||
|
moveDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { GameManager, GameOptions, GameStatus } from './types';
|
||||||
|
import { useBoard } from './useBoard';
|
||||||
|
import { useCursor } from './useCursor';
|
||||||
|
|
||||||
|
export function useGame(options: GameOptions, platformHook?: unknown): GameManager {
|
||||||
|
const { cols, rows } = options;
|
||||||
|
const [gameStatus, setGameStatus] = useState<GameStatus>('waiting');
|
||||||
|
const boardManager = useBoard(cols, rows);
|
||||||
|
const { containerRef, renderBoard } = boardManager;
|
||||||
|
const cursor = useCursor(boardManager);
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
setGameStatus('started');
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePause(pause?: boolean) {
|
||||||
|
if (typeof pause === 'boolean') {
|
||||||
|
setGameStatus(pause ? 'paused' : 'started');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGameStatus(prevStatus => prevStatus === 'paused' ? 'started' : 'paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
function quitGame() {
|
||||||
|
setGameStatus('waiting');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const gameManager = {
|
||||||
|
containerRef,
|
||||||
|
gameStatus,
|
||||||
|
renderBoard,
|
||||||
|
startGame,
|
||||||
|
togglePause,
|
||||||
|
quitGame,
|
||||||
|
cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof platformHook === 'function') {
|
||||||
|
platformHook(gameManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameManager;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import './game.css';
|
||||||
|
export { MazeGenerator } from "./MazeGenerator";
|
||||||
|
export { MazeRenderer } from "./MazeRenderer";
|
||||||
|
export * from './useGame';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useScore } from "./useScore";
|
||||||
|
import { useGame } from "./useGame";
|
||||||
|
export type UseGameType = ReturnType<typeof useGame>;
|
||||||
|
export type UseScoreType = ReturnType<typeof useScore>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useMaze } from "./useMaze";
|
import { useMaze } from "./useMaze";
|
||||||
import { usePlayer } from "./usePlayer";
|
import { usePlayer } from "./usePlayer";
|
||||||
import { useGameStatus } from "./useGameStatus";
|
import { useGameStatus } from "./useGameStatus";
|
||||||
@@ -13,24 +12,11 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
|||||||
const gameManager = useGameStatus(mazeManager, playerManager);
|
const gameManager = useGameStatus(mazeManager, playerManager);
|
||||||
useHeroRender({ mazeManager, playerManager });
|
useHeroRender({ mazeManager, playerManager });
|
||||||
|
|
||||||
const { initMaze } = mazeManager;
|
const { render } = mazeManager;
|
||||||
const { movePlayer } = playerManager;
|
const { movePlayer } = playerManager;
|
||||||
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
||||||
const { containerRef } = mazeManager;
|
const { containerRef } = mazeManager;
|
||||||
|
|
||||||
// Initialize maze on mount
|
|
||||||
useEffect(() => {
|
|
||||||
initMaze();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startNewGame = (autoStart = false) => {
|
|
||||||
if (autoStart) {
|
|
||||||
initMaze(true, startGame);
|
|
||||||
} else {
|
|
||||||
initMaze();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 🎮 key bindings integrated
|
// 🎮 key bindings integrated
|
||||||
const keyBindings: UseKeyBindingsType = useKeyBindings({
|
const keyBindings: UseKeyBindingsType = useKeyBindings({
|
||||||
@@ -56,18 +42,16 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fullGameManager = {
|
const fullGameManager = {
|
||||||
// core game
|
|
||||||
containerRef,
|
containerRef,
|
||||||
gameStatus,
|
gameStatus,
|
||||||
setGameStatus,
|
|
||||||
mazeManager,
|
mazeManager,
|
||||||
playerManager,
|
playerManager,
|
||||||
|
render,
|
||||||
startGame,
|
startGame,
|
||||||
initGame: startNewGame,
|
togglePause,
|
||||||
movePlayer: (dr: number, dc: number, count: number = 1) =>
|
movePlayer: (dr: number, dc: number, count: number = 1) =>
|
||||||
movePlayer(dr, dc, count, gameStatus, setGameStatus),
|
movePlayer(dr, dc, count, gameStatus, setGameStatus),
|
||||||
stopGame,
|
stopGame,
|
||||||
reset: () => initMaze(),
|
|
||||||
|
|
||||||
// key bindings
|
// key bindings
|
||||||
...keyBindings,
|
...keyBindings,
|
||||||
Reference in New Issue
Block a user