diff --git a/.gitignore b/.gitignore index 93d40db..497553e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,13 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* -tsconfig.app.tsbuildinfo +tsconfig.*.tsbuildinfo node_modules dist dist-ssr *.local +temp_dbs # Editor directories and files .vscode/* diff --git a/backend/controller/controller.ts b/backend/controller/controller.ts new file mode 100644 index 0000000..d1a70e2 --- /dev/null +++ b/backend/controller/controller.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; +import type { z } from "zod"; + +interface RequestContext { + user?: string; + db: BunSQLiteDatabase; +} + +export type Endpoint = { + validate: z.ZodType; + handler: (input: TInput, context: RequestContext) => Promise; +}; + +export type Request> = { + method: "POST"; + url: string; + body: z.infer; +}; + +export const createEndpoint = ( + validate: z.ZodType, + handler: (input: TInput, context: RequestContext) => Promise, +): Endpoint => { + return { validate, handler }; +}; + +export type Controller>> = + TEndpoints; + +export const createController = < + TEndpoints extends Record>, +>( + endpoints: TEndpoints, +): Controller => { + return endpoints; +}; diff --git a/backend/controller/gameController.ts b/backend/controller/gameController.ts new file mode 100644 index 0000000..484a560 --- /dev/null +++ b/backend/controller/gameController.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; +import { createController, createEndpoint } from "./controller"; +import { getGame, upsertGameState } from "../repositories/gameRepository"; +import { + serverGame, + serverToClientGame, + type ServerGame, +} from "../../shared/game"; +import crypto from "crypto"; +import { game } from "../entities/game"; +import { UnauthorizedError } from "../errors/UnauthorizedError"; +import { emit } from "../events"; + +export const gameController = createController({ + getGameState: createEndpoint(z.string(), async (uuid, ctx) => { + const game = await getGame(ctx.db, uuid); + const parsed = JSON.parse(game.gameState); + const gameState = await serverGame.parseAsync(parsed); + if (game.finished) return gameState; + return serverToClientGame(gameState); + }), + createGame: createEndpoint(z.undefined(), async (_, { user, db }) => { + if (!user) throw new UnauthorizedError("Unauthorized"); + const uuid = crypto.randomUUID() as string; + const newGame: ServerGame = game.createGame({ + uuid, + user: user, + mines: 2, + width: 4, + height: 4, + }); + upsertGameState(db, newGame); + emit({ + type: "new", + user, + }); + emit({ + type: "updateStage", + game: uuid, + stage: newGame.stage, + started: newGame.started, + }); + return newGame; + }), +}); diff --git a/backend/entities/game.ts b/backend/entities/game.ts new file mode 100644 index 0000000..783cfb7 --- /dev/null +++ b/backend/entities/game.ts @@ -0,0 +1,53 @@ +import type { ServerGame } from "../../shared/game"; + +interface CreateGameOptions { + uuid: string; + user: string; + width: number; + height: number; + mines: number; +} + +export const game = { + createGame: (options: CreateGameOptions): ServerGame => { + const { uuid, user, width, height, mines } = options; + if (mines > width * height) { + throw new Error("Too many mines"); + } + + const minesArray = Array.from({ length: width }, () => + new Array(height).fill(false), + ); + const isRevealedArray = Array.from({ length: width }, () => + new Array(height).fill(false), + ); + const isFlaggedArray = Array.from({ length: width }, () => + new Array(height).fill(false), + ); + + let remainingMines = mines; + while (remainingMines > 0) { + const x = Math.floor(Math.random() * width); + const y = Math.floor(Math.random() * height); + if (!minesArray[x][y]) { + minesArray[x][y] = true; + remainingMines--; + } + } + + return { + uuid, + user, + finished: 0, + started: Date.now(), + width, + height, + mines: minesArray, + isRevealed: isRevealedArray, + isFlagged: isFlaggedArray, + stage: 1, + lastClick: [-1, -1], + minesCount: mines, + }; + }, +}; diff --git a/backend/errors/BadRequestError.ts b/backend/errors/BadRequestError.ts new file mode 100644 index 0000000..2259c3e --- /dev/null +++ b/backend/errors/BadRequestError.ts @@ -0,0 +1,6 @@ +export class BadRequestError extends Error { + constructor(message: string) { + super(message); + this.name = "BadRequestError"; + } +} diff --git a/backend/errors/UnauthorizedError.ts b/backend/errors/UnauthorizedError.ts new file mode 100644 index 0000000..b2d50ff --- /dev/null +++ b/backend/errors/UnauthorizedError.ts @@ -0,0 +1,6 @@ +export class UnauthorizedError extends Error { + constructor(message: string) { + super(message); + this.name = "UnauthorizedError"; + } +} diff --git a/backend/events.ts b/backend/events.ts new file mode 100644 index 0000000..a64480d --- /dev/null +++ b/backend/events.ts @@ -0,0 +1,39 @@ +import type { ClientGame } from "../shared/game"; + +export type EventType = "new" | "finished" | "updateGame" | "updateStage"; + +type Events = + | { + type: "new"; + user: string; + } + | { + type: "loss"; + user: string; + stage: number; + } + | { + type: "updateGame"; + game: string; + data: ClientGame; + } + | { + type: "updateStage"; + game: string; + stage: number; + started: number; + }; + +const listeners = new Set<(event: Events) => void>(); + +export const on = (listener: (event: Events) => void) => { + listeners.add(listener); +}; + +export const off = (listener: (event: Events) => void) => { + listeners.delete(listener); +}; + +export const emit = (event: Events) => { + listeners.forEach((listener) => listener(event)); +}; diff --git a/backend/index.ts b/backend/index.ts index c211b8c..f5872d6 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,28 +1,12 @@ import type { ServerWebSocket } from "bun"; -interface Scoreboard { - stage: number; - user: string; -} - -const loadScoreboard = async (): Promise => { - try { - const scoreboardFile = Bun.file("./scoreboard.json"); - const scoreboard = await scoreboardFile.json(); - return scoreboard; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - return []; - } -}; - const allowCors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }; -const lastMessage = new WeakMap, number>(); +const userName = new WeakMap, string>(); const server = Bun.serve({ async fetch(request: Request) { if (request.method === "OPTIONS") { @@ -32,67 +16,23 @@ const server = Bun.serve({ if (request.url.endsWith("ws")) { if (server.upgrade(request)) return new Response("ok"); } - if (new URL(request.url).pathname === "/submit") { - const body = await request.text(); - const data = JSON.parse(body) as { stage: number; user: string }; - const scoreboardFile = Bun.file("./scoreboard.json"); - const scoreboard = await loadScoreboard(); - const currentScore = scoreboard.find((s) => s.user === data.user); - if (currentScore) { - if (currentScore.stage < data.stage) { - currentScore.stage = data.stage; - Bun.write(scoreboardFile, JSON.stringify(scoreboard)); - } - return new Response(JSON.stringify(currentScore), { - headers: { - "content-type": "application/json", - ...allowCors, - }, - }); - } - scoreboard.push(data); - Bun.write(scoreboardFile, JSON.stringify(scoreboard)); - return new Response(JSON.stringify(data), { - headers: { - "content-type": "application/json", - ...allowCors, - }, - }); - } - const scoreboard = await loadScoreboard(); - const sorted = scoreboard.sort((a, b) => b.stage - a.stage); - return new Response(JSON.stringify(sorted), { - headers: { - "content-type": "application/json", - ...allowCors, - }, - }); }, websocket: { message: (ws, message) => { if (typeof message !== "string") { return; } - const msg = JSON.parse(message); - const now = Date.now(); - if (lastMessage.has(ws) && now - lastMessage.get(ws)! < 200) { + const user = userName.get(ws); + try { + const msg = JSON.parse(message); + console.log(msg); + } catch (e) { + console.error("Faulty request", message, e); return; } - lastMessage.set(ws, now); - if (msg.type === "loss") { - server.publish( - "minesweeper", - JSON.stringify({ type: "loss", user: msg.user, stage: msg.stage }), - ); - } else if (msg.type === "new") { - server.publish( - "minesweeper", - JSON.stringify({ type: "new", user: msg.user }), - ); - } }, open: async (ws) => { - ws.subscribe("minesweeper"); + ws.subscribe("minesweeper-global"); }, }, port: 8076, diff --git a/backend/repositories/gameRepository.test.ts b/backend/repositories/gameRepository.test.ts new file mode 100644 index 0000000..76a9525 --- /dev/null +++ b/backend/repositories/gameRepository.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect } from "bun:test"; +import { getTestDb } from "../database/getTestDb"; +import { Game } from "../schema"; +import { + getCurrentGame, + getGame, + getGamesCount, + upsertGame, +} from "./gameRepository"; + +describe("GameRepository", () => { + it("should get game by uuid", async () => { + const db = getTestDb(); + const started = Date.now(); + await db.insert(Game).values({ + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + const game = await getGame(db, "TestUuid"); + expect(game).toEqual({ + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + }); + + it("should return undefined if game does not exist", async () => { + const db = getTestDb(); + const game = await getGame(db, "TestUuid"); + expect(game).toBeUndefined(); + }); + + it("should return latest game", async () => { + const db = getTestDb(); + const started = Date.now(); + await db.insert(Game).values({ + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + await db.insert(Game).values({ + uuid: "TestUuid2", + user: "TestUser", + stage: 2, + gameState: "ANY", + finished: 1, + started: started + 1, + }); + const game = await getCurrentGame(db, "TestUser"); + expect(game).toEqual({ + uuid: "TestUuid2", + user: "TestUser", + stage: 2, + gameState: "ANY", + finished: 1, + started: started + 1, + }); + }); + + it("should return game count", async () => { + const db = getTestDb(); + const started = Date.now(); + const uuids = ["TestUuid", "TestUuid2", "TestUuid3"]; + for (const uuid of uuids) { + await db.insert(Game).values({ + uuid, + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + } + const count = await getGamesCount(db, "TestUser"); + expect(count).toEqual(3); + }); + + it("should insert game", async () => { + const db = getTestDb(); + const started = Date.now(); + await upsertGame(db, { + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + const game = await getGame(db, "TestUuid"); + expect(game).toEqual({ + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + }); + + it("should update game", async () => { + const db = getTestDb(); + const started = Date.now(); + await db.insert(Game).values({ + uuid: "TestUuid", + user: "TestUser", + stage: 1, + gameState: "ANY", + finished: 1, + started, + }); + await upsertGame(db, { + uuid: "TestUuid", + user: "TestUser", + stage: 2, + gameState: "ANY", + finished: 1, + started: started + 1, + }); + const game = await getGame(db, "TestUuid"); + expect(game).toEqual({ + uuid: "TestUuid", + user: "TestUser", + stage: 2, + gameState: "ANY", + finished: 1, + started: started + 1, + }); + }); +}); diff --git a/backend/repositories/gameRepository.ts b/backend/repositories/gameRepository.ts new file mode 100644 index 0000000..23cf456 --- /dev/null +++ b/backend/repositories/gameRepository.ts @@ -0,0 +1,76 @@ +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; +import { Game, type GameType } from "../schema"; +import { eq, sql, desc, and, not } from "drizzle-orm"; +import type { ServerGame } from "../../shared/game"; + +export const getGame = async (db: BunSQLiteDatabase, uuid: string) => { + return (await db.select().from(Game).where(eq(Game.uuid, uuid)))[0]; +}; + +export const getGames = async (db: BunSQLiteDatabase, user: string) => { + return await db + .select() + .from(Game) + .where(and(eq(Game.user, user), not(eq(Game.finished, 0)))) + .orderBy(Game.started, sql`desc`); +}; + +export const getCurrentGame = async (db: BunSQLiteDatabase, user: string) => { + return ( + await db + .select() + .from(Game) + .where(eq(Game.user, user)) + .orderBy(desc(Game.started)) + .limit(1) + )[0]; +}; + +export const getGamesCount = async (db: BunSQLiteDatabase, user: string) => { + return ( + await db + .select({ count: sql`count(*)` }) + .from(Game) + .where(eq(Game.user, user)) + )[0].count; +}; + +export const upsertGame = async (db: BunSQLiteDatabase, game: GameType) => { + const { uuid, user, stage, gameState, finished, started } = game; + const games = await db.select().from(Game).where(eq(Game.uuid, uuid)); + if (games.length > 0) { + await db + .update(Game) + .set({ + stage, + gameState, + finished, + started, + }) + .where(eq(Game.uuid, uuid)); + } else { + await db.insert(Game).values({ + uuid, + user, + stage, + gameState, + finished, + started, + }); + } +}; + +export const upsertGameState = async ( + db: BunSQLiteDatabase, + game: ServerGame, +) => { + const { uuid, user, stage, finished, started } = game; + upsertGame(db, { + uuid, + user, + stage, + gameState: JSON.stringify(game), + finished, + started, + }); +}; diff --git a/backend/services/score.test.ts b/backend/repositories/scoreRepository.test.ts similarity index 79% rename from backend/services/score.test.ts rename to backend/repositories/scoreRepository.test.ts index 72fc1ed..c48391d 100644 --- a/backend/services/score.test.ts +++ b/backend/repositories/scoreRepository.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from "bun:test"; -import { getScoreBoard } from "./score.ts"; +import { getScoreBoard } from "./scoreRepository.ts"; import { getTestDb } from "../database/getTestDb.ts"; import { Game, User } from "../schema.ts"; -describe("Score", () => { - it("should return the score board", async () => { +describe("ScoreRepository", () => { + it("should return the scoreboard", async () => { const db = getTestDb(); await db.insert(User).values({ name: "TestUser", @@ -16,7 +16,7 @@ describe("Score", () => { stage: 1, gameState: "ANY", finished: 1, - timestamp: Date.now(), + started: Date.now(), }); await db.insert(Game).values({ user: "TestUser", @@ -24,7 +24,7 @@ describe("Score", () => { stage: 10, gameState: "ANY", finished: 1, - timestamp: Date.now(), + started: Date.now(), }); await db.insert(Game).values({ user: "TestUser", @@ -32,7 +32,7 @@ describe("Score", () => { stage: 20, gameState: "ANY", finished: 0, - timestamp: Date.now(), + started: Date.now(), }); const result = await getScoreBoard(db); expect(result).toEqual([{ stage: 10, user: "TestUser" }]); diff --git a/backend/services/score.ts b/backend/repositories/scoreRepository.ts similarity index 78% rename from backend/services/score.ts rename to backend/repositories/scoreRepository.ts index ea54bb3..64a9dfb 100644 --- a/backend/services/score.ts +++ b/backend/repositories/scoreRepository.ts @@ -1,4 +1,4 @@ -import { eq, sql } from "drizzle-orm"; +import { eq, sql, not } from "drizzle-orm"; import { Game } from "../schema"; import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; @@ -6,6 +6,6 @@ export const getScoreBoard = async (db: BunSQLiteDatabase) => { return await db .select({ stage: sql`max(${Game.stage})`, user: Game.user }) .from(Game) - .where(eq(Game.finished, 1)) + .where(not(eq(Game.finished, 0))) .groupBy(Game.user); }; diff --git a/backend/services/user.test.ts b/backend/repositories/userRepository.test.ts similarity index 93% rename from backend/services/user.test.ts rename to backend/repositories/userRepository.test.ts index de98f4c..f77ff82 100644 --- a/backend/services/user.test.ts +++ b/backend/repositories/userRepository.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "bun:test"; import { getTestDb } from "../database/getTestDb"; -import { getUser, loginUser, registerUser } from "./user"; +import { getUser, loginUser, registerUser } from "./userRepository"; -describe("User", () => { +describe("UserRepository", () => { it("should register a user", async () => { const db = getTestDb(); await registerUser(db, "TestUser", "test"); diff --git a/backend/services/user.ts b/backend/repositories/userRepository.ts similarity index 100% rename from backend/services/user.ts rename to backend/repositories/userRepository.ts diff --git a/backend/router.ts b/backend/router.ts new file mode 100644 index 0000000..8f6dff3 --- /dev/null +++ b/backend/router.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Controller, Endpoint } from "./controller/controller"; +import { gameController } from "./controller/gameController"; +import { db } from "./database/db"; +import { BadRequestError } from "./errors/BadRequestError"; + +const controllers = { + game: gameController, +} satisfies Record>; + +export const handleRequest = (message: unknown, sessionUser?: string) => { + const ctx = { + user: sessionUser, + db, + }; + if ( + !message || + !(typeof message === "object") || + !("type" in message) || + !("payload" in message) + ) + return; + const { type, payload } = message; + if (!(typeof type === "string")) return; + const [controllerName, action] = type.split("."); + if (!(controllerName in controllers)) return; + // @ts-expect-error controllers[controllerName] is a Controller + const endpoint = controllers[controllerName][action] as Endpoint; + const input = endpoint.validate.safeParse(payload); + if (input.success) { + const result = endpoint.handler(input.data, ctx); + return result; + } + throw new BadRequestError(input.error.message); +}; + +export type Routes = typeof controllers; diff --git a/backend/schema.ts b/backend/schema.ts index 7d5ad71..77bcd14 100644 --- a/backend/schema.ts +++ b/backend/schema.ts @@ -13,7 +13,7 @@ export const Game = sqliteTable("games", { gameState: text("gameState").notNull(), stage: integer("stage").notNull(), finished: integer("finished").notNull().default(0), - timestamp: integer("timestamp").notNull(), + started: integer("timestamp").notNull(), }); export type UserType = Omit & { diff --git a/bun.lockb b/bun.lockb index b702096..7a664db 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f735650..c461e0b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", "use-sound": "^4.0.3", + "zod": "^3.23.8", "zustand": "^4.5.5" }, "devDependencies": { diff --git a/shared/game.test.ts b/shared/game.test.ts new file mode 100644 index 0000000..5609c8f --- /dev/null +++ b/shared/game.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "bun:test"; +import { getValue, serverToClientGame } from "./game"; + +describe("Game", () => { + it("should get value", () => { + const mines = [ + [false, false, true, true, true], + [true, false, true, false, true], + [false, false, true, true, true], + [false, false, false, false, false], + ]; + expect(getValue(mines, 0, 0)).toEqual(1); + expect(getValue(mines, 0, 1)).toEqual(3); + expect(getValue(mines, 3, 0)).toEqual(0); + expect(getValue(mines, 1, 3)).toEqual(8); + }); + + it("should convert server to client game", () => { + const serverGame = { + mines: [ + [false, false, true, true, true], + [true, false, true, false, true], + [false, false, true, true, true], + [false, false, false, false, false], + ], + minesCount: 4, + isRevealed: [ + [false, false, true, false, true], + [true, false, true, false, true], + [false, false, true, false, true], + [false, false, false, false, false], + ], + isFlagged: [ + [false, false, true, false, true], + [true, false, true, false, true], + [false, false, true, false, true], + [true, false, false, false, false], + ], + isGameOver: false, + started: 1679599200000, + finished: 0, + lastClick: [0, 0] satisfies [number, number], + uuid: "C270D7CD-AF97-42CE-A6C9-CB765102CA17", + width: 5, + height: 4, + }; + expect(serverToClientGame(serverGame)).toEqual({ + minesCount: 4, + isRevealed: [ + [false, false, true, false, true], + [true, false, true, false, true], + [false, false, true, false, true], + [false, false, false, false, false], + ], + isFlagged: [ + [false, false, true, false, true], + [true, false, true, false, true], + [false, false, true, false, true], + [true, false, false, false, false], + ], + values: [ + [-1, -1, 2, -1, 2], + [0, -1, 4, -1, 4], + [-1, -1, 2, -1, 2], + [-1, -1, -1, -1, -1], + ], + lastClick: [0, 0], + started: 1679599200000, + uuid: "C270D7CD-AF97-42CE-A6C9-CB765102CA17", + width: 5, + height: 4, + }); + }); +}); diff --git a/shared/game.ts b/shared/game.ts new file mode 100644 index 0000000..335aa7c --- /dev/null +++ b/shared/game.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +export const clientGame = z.object({ + user: z.string(), + uuid: z.string(), + width: z.number(), + height: z.number(), + isRevealed: z.array(z.array(z.boolean())), + isFlagged: z.array(z.array(z.boolean())), + values: z.array(z.array(z.number())), + minesCount: z.number(), + lastClick: z.tuple([z.number(), z.number()]), + started: z.number(), + stage: z.number(), +}); + +export const serverGame = z.object({ + user: z.string(), + uuid: z.string(), + width: z.number(), + height: z.number(), + isRevealed: z.array(z.array(z.boolean())), + isFlagged: z.array(z.array(z.boolean())), + mines: z.array(z.array(z.boolean())), + minesCount: z.number(), + lastClick: z.tuple([z.number(), z.number()]), + started: z.number(), + finished: z.number().default(0), + stage: z.number(), +}); + +export type ClientGame = z.infer; +export type ServerGame = z.infer; + +export const getValue = (mines: boolean[][], x: number, y: number) => { + const neighbors = [ + mines[x - 1]?.[y - 1], + mines[x]?.[y - 1], + mines[x + 1]?.[y - 1], + mines[x - 1]?.[y], + mines[x + 1]?.[y], + mines[x - 1]?.[y + 1], + mines[x]?.[y + 1], + mines[x + 1]?.[y + 1], + ]; + return neighbors.filter((n) => n).length; +}; + +export const serverToClientGame = (game: ServerGame): ClientGame => { + return { + user: game.user, + uuid: game.uuid, + width: game.width, + height: game.height, + isRevealed: game.isRevealed, + isFlagged: game.isFlagged, + minesCount: game.minesCount, + values: game.mines.map((_, i) => + game.mines[0].map((_, j) => { + if (!game.isRevealed[i][j]) return -1; + return getValue(game.mines, i, j); + }), + ), + lastClick: game.lastClick, + started: game.started, + stage: game.stage, + }; +}; diff --git a/src/App.tsx b/src/App.tsx index 26d2d6c..805047a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,6 @@ function useMaxToasts(max: number) { }, [toasts, max]); } -useGameStore.getState().resetGame(4, 4, 2); function App() { const game = useGameStore(); const [scores, setScores] = useState([]); @@ -38,6 +37,10 @@ function App() { } // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.isGameOver]); + useEffect(() => { + game.resetGame(4, 4, 2); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { fetch("https://mb.gordon.business") @@ -75,6 +78,15 @@ function App() { onChange={(e) => game.setName(e.target.value)} />

+

+ Feed:{" "} + +

{scores.slice(0, 10).map((score) => ( diff --git a/src/GameState.ts b/src/GameState.ts index ad5cca8..6a6d132 100644 --- a/src/GameState.ts +++ b/src/GameState.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; import { newGame } from "./ws"; interface GameState { + showFeed: boolean; mines: boolean[][]; minesCount: number; isRevealed: boolean[][]; @@ -13,7 +14,6 @@ interface GameState { stage: number; name: string; - initializeGame: (width: number, height: number, mines: number) => void; flag: (x: number, y: number) => void; reveal: (x: number, y: number) => boolean; getValue: (x: number, y: number) => number; @@ -31,6 +31,7 @@ interface GameState { triggerPostGame: () => boolean; expandBoard: () => void; setName: (name: string) => void; + setShowFeed: (showFeed: boolean) => void; } const useGameStore = create((set, get) => ({ @@ -44,41 +45,9 @@ const useGameStore = create((set, get) => ({ height: 0, stage: 1, name: localStorage.getItem("name") || "No Name", - - initializeGame: (width, height, mines) => { - mines = Math.min(mines, width * height); - - const minesArray = Array.from({ length: width }, () => - new Array(height).fill(false), - ); - const isRevealedArray = Array.from({ length: width }, () => - new Array(height).fill(false), - ); - const isFlaggedArray = Array.from({ length: width }, () => - new Array(height).fill(false), - ); - - let remainingMines = mines; - while (remainingMines > 0) { - const x = Math.floor(Math.random() * width); - const y = Math.floor(Math.random() * height); - if (!minesArray[x][y]) { - minesArray[x][y] = true; - remainingMines--; - } - } - - set({ - width, - height, - mines: minesArray, - isRevealed: isRevealedArray, - isFlagged: isFlaggedArray, - minesCount: mines, - isGameOver: false, - startTime: Date.now(), - }); - }, + showFeed: !localStorage.getItem("showFeed") + ? true + : localStorage.getItem("showFeed") === "true", flag: (x, y) => { set((state) => { @@ -389,6 +358,10 @@ const useGameStore = create((set, get) => ({ localStorage.setItem("name", name); set({ name }); }, + setShowFeed: (showFeed) => { + localStorage.setItem("showFeed", showFeed.toString()); + set({ showFeed }); + }, })); export default useGameStore; diff --git a/src/ws.ts b/src/ws.ts index f1af94f..3ec0d1b 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -1,4 +1,5 @@ import toast from "react-hot-toast"; +import useGameStore from "./GameState"; let ws: WebSocket; @@ -11,6 +12,7 @@ export const connectWS = () => { if (data.user === name) { return; } + if (!useGameStore.getState().showFeed) return; switch (data.type) { case "new": toast(data.user + " started a new game", { diff --git a/tsconfig.app.json b/tsconfig.app.json index f0a2350..c79a069 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -20,5 +20,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["src", "shared"] } diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo deleted file mode 100644 index 070fc94..0000000 --- a/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./vite.config.ts","./backend/index.ts","./backend/migrate.ts","./backend/schema.ts","./backend/test-setup.ts","./backend/database/db.ts","./backend/database/getDb.ts","./backend/database/getTestDb.ts","./backend/services/score.test.ts","./backend/services/score.ts"],"version":"5.6.2"} \ No newline at end of file