From 8f246aaefa15ad3d082686b7b724a8ebd60948d9 Mon Sep 17 00:00:00 2001 From: Anuraj Jit Saikia Date: Wed, 30 Sep 2026 10:51:21 +0530 Subject: [PATCH 1/2] test(frontend): add a localStorage shim so tests pass on Node 26 --- frontend/src/game/arcade/mergedrop.test.ts | 4 +- frontend/src/game/maps.test.ts | 6 +- frontend/src/vitest.setup.ts | 81 ++++++++++++++++++++++ frontend/vitest.config.ts | 1 + shared/src/rest.test.ts | 2 + 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 frontend/src/vitest.setup.ts diff --git a/frontend/src/game/arcade/mergedrop.test.ts b/frontend/src/game/arcade/mergedrop.test.ts index 2dbf8b6..15fec8d 100644 --- a/frontend/src/game/arcade/mergedrop.test.ts +++ b/frontend/src/game/arcade/mergedrop.test.ts @@ -212,7 +212,7 @@ describe("physics", () => { } } expect(s.bodies.length).toBeGreaterThan(0); - }); + }, 15_000); it("never mutates the state it is given", () => { const s = stepMergeDrop(initMergeDrop(5), drop(120)); @@ -514,7 +514,7 @@ describe("solver equivalence (broad phase vs all-pairs reference)", () => { ); expect(matched).toBe(1_500); } - }); + }, 15_000); }); describe("pressure phases", () => { diff --git a/frontend/src/game/maps.test.ts b/frontend/src/game/maps.test.ts index f335113..8aad5bb 100644 --- a/frontend/src/game/maps.test.ts +++ b/frontend/src/game/maps.test.ts @@ -229,7 +229,7 @@ describe("campus arcade cabinets (PRD 11)", () => { const ARCADE_GAME_SET = new Set(ARCADE_GAMES); - it("places exactly three arcade interactables, each with a valid game id + label", () => { + it("places one arcade interactable per registered game, each with a valid game id + label", () => { const arcades = objects("interactables").filter((o) => prop(o, "interactType") === "arcade"); expect(arcades).toHaveLength(ARCADE_GAME_SET.size); const games = arcades.map((o) => prop(o, "game")); @@ -285,8 +285,8 @@ describe("campus arcade hall (PRD 16)", () => { it("relocates the cabinets together into the southern hall (well clear of the plaza)", () => { const cabinets = objects("furniture").filter((o) => o.name.startsWith("f_arcade_")); - // One solid cabinet sprite per registered game (Arcade 2.0 added a third). - expect(cabinets.length).toBe(3); + // One solid cabinet sprite per registered game, all on the north wall. + expect(cabinets.length).toBe(ARCADE_GAMES.length); for (const c of cabinets) { // Deep south of spawn (row 44 = 704px) — the cabinets moved out of the // old plaza cluster (~row 50) into the far-south hall (row 96 = 1536px). diff --git a/frontend/src/vitest.setup.ts b/frontend/src/vitest.setup.ts new file mode 100644 index 0000000..8426f79 --- /dev/null +++ b/frontend/src/vitest.setup.ts @@ -0,0 +1,81 @@ +/** + * Node 25+ (this host is 26) exposes experimental `localStorage` / + * `sessionStorage` globals that stay `undefined` unless the process is started + * with `--localstorage-file`. jsdom still installs Storage on `window`, but the + * bare `localStorage` identifier tests use stays the Node binding, so + * `localStorage.clear()` throws and ~38 frontend tests fail. + * + * Rebind both names to a working Storage implementation before any suite runs. + */ +function memoryStorage(): Storage { + const map = new Map(); + return { + get length() { + return map.size; + }, + clear() { + map.clear(); + }, + getItem(key: string) { + return map.get(key) ?? null; + }, + key(index: number) { + return [...map.keys()][index] ?? null; + }, + removeItem(key: string) { + map.delete(key); + }, + setItem(key: string, value: string) { + map.set(String(key), String(value)); + }, + }; +} + +function isStorage(value: unknown): value is Storage { + if (typeof value !== "object" || value === null) return false; + if (!("getItem" in value) || typeof value.getItem !== "function") return false; + if (!("setItem" in value) || typeof value.setItem !== "function") return false; + if (!("clear" in value) || typeof value.clear !== "function") return false; + return true; +} + +function readNamed(name: "localStorage" | "sessionStorage"): unknown { + if (name === "localStorage") return globalThis.localStorage; + return globalThis.sessionStorage; +} + +function readWindowNamed(name: "localStorage" | "sessionStorage"): unknown { + if (typeof window === "undefined") return undefined; + return name === "localStorage" ? window.localStorage : window.sessionStorage; +} + +function workingStorage(name: "localStorage" | "sessionStorage"): Storage { + const current = readNamed(name); + if (isStorage(current)) return current; + const fromWindow = readWindowNamed(name); + if (isStorage(fromWindow)) return fromWindow; + return memoryStorage(); +} + +function install(name: "localStorage" | "sessionStorage"): void { + const storage = workingStorage(name); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: true, + writable: true, + value: storage, + }); + if (typeof window !== "undefined") { + Object.defineProperty(window, name, { + configurable: true, + enumerable: true, + writable: true, + value: storage, + }); + } +} + +install("localStorage"); +install("sessionStorage"); + +export {}; diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index d636e3c..ad11e57 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -7,5 +7,6 @@ export default defineConfig({ test: { environment: "jsdom", include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + setupFiles: ["./src/vitest.setup.ts"], }, }); diff --git a/shared/src/rest.test.ts b/shared/src/rest.test.ts index a7e42e8..34753fb 100644 --- a/shared/src/rest.test.ts +++ b/shared/src/rest.test.ts @@ -302,6 +302,8 @@ describe("arcade score submission", () => { expect(arcadeScoreSchema.safeParse({ game: "snake-v2", score: 12 }).success).toBe(true); expect(arcadeScoreSchema.safeParse({ game: "flappy", score: 0 }).success).toBe(true); expect(arcadeScoreSchema.safeParse({ game: "merge-drop", score: 4096 }).success).toBe(true); + expect(arcadeScoreSchema.safeParse({ game: "breakout", score: 120 }).success).toBe(true); + expect(arcadeScoreSchema.safeParse({ game: "minesweeper", score: 2400 }).success).toBe(true); }); it("rejects an unknown game", () => { expect(arcadeScoreSchema.safeParse({ game: "pong", score: 1 }).success).toBe(false); From 690cca96b014543fbf9d4630a0c202c640575289 Mon Sep 17 00:00:00 2001 From: Anuraj Jit Saikia Date: Wed, 30 Sep 2026 10:51:21 +0530 Subject: [PATCH 2/2] feat(arcade): add Breakout and Minesweeper cabinets Two new arcade games with pure reducers in game/arcade and React UI in ui/arcade, placed at campus tiles (68,96) and (85,96) and registered in ARCADE_GAMES for the leaderboard. Also fixes key auto-repeat in the overlay and lets every game header wrap on narrow screens; Breakout uses a tall portrait court on mobile. --- backend/assets/campus.geometry.json | 2 +- docs/FEATURES.md | 14 +- frontend/ATTRIBUTIONS.md | 1 + frontend/README.md | 12 +- .../assets/furniture/arcade_breakout.png | Bin 0 -> 363 bytes .../assets/furniture/arcade_minesweeper.png | Bin 0 -> 366 bytes frontend/public/assets/maps/campus.json | 2 +- frontend/scripts/campus_decor/arcade_hall.py | 32 +- frontend/scripts/gen_arcade_sprites.py | 4 +- frontend/scripts/gen_campus.py | 31 +- frontend/src/game/arcade/breakout.test.ts | 187 +++++++ frontend/src/game/arcade/breakout.ts | 455 ++++++++++++++++++ frontend/src/game/arcade/minesweeper.test.ts | 203 ++++++++ frontend/src/game/arcade/minesweeper.ts | 344 +++++++++++++ frontend/src/game/scenes/BootScene.ts | 1 + frontend/src/ui/arcade/ArcadeOverlay.test.tsx | 36 ++ frontend/src/ui/arcade/ArcadeOverlay.tsx | 8 + frontend/src/ui/arcade/BreakoutGame.test.tsx | 89 ++++ frontend/src/ui/arcade/BreakoutGame.tsx | 286 +++++++++++ frontend/src/ui/arcade/FlappyGame.test.tsx | 28 ++ frontend/src/ui/arcade/FlappyGame.tsx | 2 + frontend/src/ui/arcade/MergeDropGame.tsx | 2 + .../src/ui/arcade/MinesweeperGame.test.tsx | 107 ++++ frontend/src/ui/arcade/MinesweeperGame.tsx | 239 +++++++++ frontend/src/ui/arcade/SnakeGame.tsx | 2 + frontend/src/ui/arcade/arcade.css | 153 ++++++ .../src/ui/arcade/breakout/render.test.ts | 16 + frontend/src/ui/arcade/breakout/render.ts | 149 ++++++ frontend/src/ui/arcade/flappy/hud.ts | 5 +- frontend/src/ui/arcade/terminalHold.ts | 2 + frontend/tsconfig.app.json | 2 +- shared/src/constants.test.ts | 2 + shared/src/constants.ts | 13 +- 33 files changed, 2395 insertions(+), 34 deletions(-) create mode 100644 frontend/public/assets/furniture/arcade_breakout.png create mode 100644 frontend/public/assets/furniture/arcade_minesweeper.png create mode 100644 frontend/src/game/arcade/breakout.test.ts create mode 100644 frontend/src/game/arcade/breakout.ts create mode 100644 frontend/src/game/arcade/minesweeper.test.ts create mode 100644 frontend/src/game/arcade/minesweeper.ts create mode 100644 frontend/src/ui/arcade/BreakoutGame.test.tsx create mode 100644 frontend/src/ui/arcade/BreakoutGame.tsx create mode 100644 frontend/src/ui/arcade/FlappyGame.test.tsx create mode 100644 frontend/src/ui/arcade/MinesweeperGame.test.tsx create mode 100644 frontend/src/ui/arcade/MinesweeperGame.tsx create mode 100644 frontend/src/ui/arcade/breakout/render.test.ts create mode 100644 frontend/src/ui/arcade/breakout/render.ts diff --git a/backend/assets/campus.geometry.json b/backend/assets/campus.geometry.json index bcc0e85..c8e6a4d 100644 --- a/backend/assets/campus.geometry.json +++ b/backend/assets/campus.geometry.json @@ -1 +1 @@ -{"version": 1, "tile": {"size": 16, "cols": 120, "rows": 118}, "world": {"width": 1920, "height": 1888}, "spawn": {"x": 960, "y": 704}, "rooms": [{"x": 512, "y": 48, "width": 176, "height": 128, "roomId": "4"}, {"x": 720, "y": 48, "width": 192, "height": 128, "roomId": "5"}, {"x": 944, "y": 48, "width": 192, "height": 128, "roomId": "6"}, {"x": 656, "y": 1616, "width": 176, "height": 128, "roomId": "1"}, {"x": 432, "y": 1616, "width": 208, "height": 128, "roomId": "2"}, {"x": 176, "y": 1616, "width": 240, "height": 144, "roomId": "3"}], "doors": [{"x": 576, "y": 176, "width": 32, "height": 16, "roomId": "4"}, {"x": 784, "y": 176, "width": 32, "height": 16, "roomId": "5"}, {"x": 1008, "y": 176, "width": 32, "height": 16, "roomId": "6"}, {"x": 720, "y": 1600, "width": 32, "height": 16, "roomId": "1"}, {"x": 512, "y": 1600, "width": 32, "height": 16, "roomId": "2"}, {"x": 272, "y": 1600, "width": 32, "height": 16, "roomId": "3"}], "seats": [{"roomId": "4", "seatId": 0, "x": 560, "y": 96, "facing": "right"}, {"roomId": "4", "seatId": 1, "x": 624, "y": 96, "facing": "left"}, {"roomId": "4", "seatId": 2, "x": 592, "y": 64, "facing": "down"}, {"roomId": "4", "seatId": 3, "x": 592, "y": 128, "facing": "up"}, {"roomId": "5", "seatId": 0, "x": 768, "y": 96, "facing": "right"}, {"roomId": "5", "seatId": 1, "x": 832, "y": 96, "facing": "left"}, {"roomId": "5", "seatId": 2, "x": 800, "y": 64, "facing": "down"}, {"roomId": "5", "seatId": 3, "x": 800, "y": 128, "facing": "up"}, {"roomId": "6", "seatId": 0, "x": 992, "y": 96, "facing": "right"}, {"roomId": "6", "seatId": 1, "x": 1056, "y": 96, "facing": "left"}, {"roomId": "6", "seatId": 2, "x": 1024, "y": 64, "facing": "down"}, {"roomId": "6", "seatId": 3, "x": 1024, "y": 128, "facing": "up"}, {"roomId": "1", "seatId": 0, "x": 704, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 1, "x": 736, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 2, "x": 768, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 3, "x": 720, "y": 1696, "facing": "up"}, {"roomId": "1", "seatId": 4, "x": 752, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 0, "x": 480, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 1, "x": 512, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 2, "x": 544, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 3, "x": 576, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 4, "x": 480, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 5, "x": 512, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 6, "x": 544, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 7, "x": 576, "y": 1696, "facing": "up"}, {"roomId": "3", "seatId": 0, "x": 208, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 1, "x": 240, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 2, "x": 272, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 3, "x": 304, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 4, "x": 336, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 5, "x": 368, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 6, "x": 208, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 7, "x": 240, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 8, "x": 272, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 9, "x": 304, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 10, "x": 336, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 11, "x": 368, "y": 1712, "facing": "up"}], "boardSeats": [{"tableId": "ttt-1", "seat": 0, "game": "tictactoe", "x": 1120, "y": 1632, "facing": "right"}, {"tableId": "ttt-1", "seat": 1, "game": "tictactoe", "x": 1184, "y": 1632, "facing": "left"}, {"tableId": "c4-1", "seat": 0, "game": "connect4", "x": 1120, "y": 1680, "facing": "right"}, {"tableId": "c4-1", "seat": 1, "game": "connect4", "x": 1184, "y": 1680, "facing": "left"}], "stageZones": [{"x": 1312, "y": 256, "width": 576, "height": 448, "name": "stage_zone", "zoneType": "stage"}, {"x": 1440, "y": 32, "width": 336, "height": 224, "name": "presenter_zone", "zoneType": "presenter"}], "portals": [{"x": 432, "y": 688, "width": 32, "height": 32, "id": 40001, "targetX": 1280, "targetY": 688}], "solidObjects": [{"key": "f_desk", "x": 264, "y": 616}, {"key": "f_plant_big", "x": 184, "y": 1512}, {"key": "f_plant_big", "x": 824, "y": 1512}, {"key": "f_lz_bookshelf", "x": 248, "y": 1624}, {"key": "f_lz_counter", "x": 360, "y": 1624}, {"key": "f_lz_fridge", "x": 392, "y": 1624}, {"key": "f_lz_table", "x": 216, "y": 1688}, {"key": "f_lz_table_books", "x": 232, "y": 1688}, {"key": "f_lz_table", "x": 248, "y": 1688}, {"key": "f_lz_table_cup", "x": 264, "y": 1688}, {"key": "f_lz_table", "x": 280, "y": 1688}, {"key": "f_lz_table_books", "x": 296, "y": 1688}, {"key": "f_lz_table", "x": 312, "y": 1688}, {"key": "f_lz_table_cup", "x": 328, "y": 1688}, {"key": "f_lz_table", "x": 344, "y": 1688}, {"key": "f_lz_table_books", "x": 360, "y": 1688}, {"key": "f_lz_table", "x": 376, "y": 1688}, {"key": "f_lz_sideboard", "x": 264, "y": 1752}, {"key": "f_lz_shelf_jars", "x": 360, "y": 1752}, {"key": "f_lz_table", "x": 520, "y": 264}, {"key": "f_lz_computer", "x": 536, "y": 264}, {"key": "f_lz_table_books", "x": 552, "y": 264}, {"key": "f_lz_table_books", "x": 1192, "y": 264}, {"key": "f_lz_computer", "x": 1208, "y": 264}, {"key": "f_lz_table", "x": 1224, "y": 264}, {"key": "f_lz_table_books", "x": 600, "y": 104}, {"key": "f_lz_table", "x": 808, "y": 104}, {"key": "f_lz_bookshelf", "x": 968, "y": 56}, {"key": "f_lz_shelf_white", "x": 1096, "y": 56}, {"key": "f_lz_globe", "x": 1128, "y": 72}, {"key": "f_lz_table_study", "x": 1032, "y": 104}, {"key": "f_lz_palm", "x": 440, "y": 1736}, {"key": "f_lz_table", "x": 488, "y": 1672}, {"key": "f_lz_table_books", "x": 504, "y": 1672}, {"key": "f_lz_table", "x": 520, "y": 1672}, {"key": "f_lz_table_cup", "x": 536, "y": 1672}, {"key": "f_lz_table", "x": 552, "y": 1672}, {"key": "f_lz_table_books", "x": 568, "y": 1672}, {"key": "f_lz_table", "x": 584, "y": 1672}, {"key": "f_lz_bookshelf", "x": 680, "y": 1736}, {"key": "f_lz_plant", "x": 808, "y": 1736}, {"key": "f_lz_table_books", "x": 712, "y": 1672}, {"key": "f_lz_table", "x": 728, "y": 1672}, {"key": "f_lz_table_cup", "x": 744, "y": 1672}, {"key": "f_lz_table", "x": 760, "y": 1672}, {"key": "f_lz_table_books", "x": 776, "y": 1672}, {"key": "f_lz_chair", "x": 1368, "y": 344}, {"key": "f_lz_chair", "x": 1416, "y": 344}, {"key": "f_lz_chair", "x": 1464, "y": 344}, {"key": "f_lz_chair", "x": 1512, "y": 344}, {"key": "f_lz_chair", "x": 1656, "y": 344}, {"key": "f_lz_chair", "x": 1704, "y": 344}, {"key": "f_lz_chair", "x": 1752, "y": 344}, {"key": "f_lz_chair", "x": 1800, "y": 344}, {"key": "f_lz_chair", "x": 1368, "y": 392}, {"key": "f_lz_chair", "x": 1416, "y": 392}, {"key": "f_lz_chair", "x": 1464, "y": 392}, {"key": "f_lz_chair", "x": 1512, "y": 392}, {"key": "f_lz_chair", "x": 1656, "y": 392}, {"key": "f_lz_chair", "x": 1704, "y": 392}, {"key": "f_lz_chair", "x": 1752, "y": 392}, {"key": "f_lz_chair", "x": 1800, "y": 392}, {"key": "f_lz_chair", "x": 1368, "y": 472}, {"key": "f_lz_chair", "x": 1416, "y": 472}, {"key": "f_lz_chair", "x": 1464, "y": 472}, {"key": "f_lz_chair", "x": 1512, "y": 472}, {"key": "f_lz_chair", "x": 1656, "y": 472}, {"key": "f_lz_chair", "x": 1704, "y": 472}, {"key": "f_lz_chair", "x": 1752, "y": 472}, {"key": "f_lz_chair", "x": 1800, "y": 472}, {"key": "f_lz_chair", "x": 1368, "y": 536}, {"key": "f_lz_chair", "x": 1416, "y": 536}, {"key": "f_lz_chair", "x": 1464, "y": 536}, {"key": "f_lz_chair", "x": 1512, "y": 536}, {"key": "f_lz_chair", "x": 1656, "y": 536}, {"key": "f_lz_chair", "x": 1704, "y": 536}, {"key": "f_lz_chair", "x": 1752, "y": 536}, {"key": "f_lz_chair", "x": 1800, "y": 536}, {"key": "f_lz_chair", "x": 1416, "y": 600}, {"key": "f_lz_chair", "x": 1464, "y": 600}, {"key": "f_lz_chair", "x": 1512, "y": 600}, {"key": "f_lz_chair", "x": 1656, "y": 600}, {"key": "f_lz_chair", "x": 1704, "y": 600}, {"key": "f_lz_chair", "x": 1752, "y": 600}, {"key": "f_lz_vending_shelf", "x": 1368, "y": 1544}, {"key": "f_lz_bench_gold", "x": 104, "y": 1096}, {"key": "f_lz_bench_gold", "x": 104, "y": 1144}, {"key": "f_lz_bin", "x": 136, "y": 1080}, {"key": "f_lz_plant_small", "x": 136, "y": 1160}, {"key": "f_lz_bench_gold", "x": 248, "y": 1064}, {"key": "f_lz_plant_small", "x": 200, "y": 1032}, {"key": "f_lz_plant_small", "x": 296, "y": 1032}, {"key": "f_lz_bench_gold", "x": 664, "y": 1208}, {"key": "f_lz_plant", "x": 616, "y": 1176}, {"key": "f_lz_plant", "x": 712, "y": 1176}, {"key": "f_lz_plant_small", "x": 456, "y": 1032}, {"key": "f_lz_plant_small", "x": 504, "y": 1032}, {"key": "f_lz_plant_small", "x": 1208, "y": 1032}, {"key": "f_lz_plant_small", "x": 1352, "y": 1032}, {"key": "f_lz_plant", "x": 760, "y": 424}, {"key": "f_lz_plant", "x": 840, "y": 424}, {"key": "f_lz_plant", "x": 1528, "y": 776}, {"key": "f_lz_plant", "x": 1640, "y": 776}, {"key": "f_arcade_snake", "x": 1144, "y": 1544}, {"key": "f_arcade_flappy", "x": 1224, "y": 1544}, {"key": "f_arcade_merge-drop", "x": 1320, "y": 1544}, {"key": "f_table_small", "x": 1160, "y": 1640}, {"key": "f_table_small", "x": 1160, "y": 1688}], "collision": {"cols": 120, "rows": 118, "blocked": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}} \ No newline at end of file +{"version": 1, "tile": {"size": 16, "cols": 120, "rows": 118}, "world": {"width": 1920, "height": 1888}, "spawn": {"x": 960, "y": 704}, "rooms": [{"x": 512, "y": 48, "width": 176, "height": 128, "roomId": "4"}, {"x": 720, "y": 48, "width": 192, "height": 128, "roomId": "5"}, {"x": 944, "y": 48, "width": 192, "height": 128, "roomId": "6"}, {"x": 656, "y": 1616, "width": 176, "height": 128, "roomId": "1"}, {"x": 432, "y": 1616, "width": 208, "height": 128, "roomId": "2"}, {"x": 176, "y": 1616, "width": 240, "height": 144, "roomId": "3"}], "doors": [{"x": 576, "y": 176, "width": 32, "height": 16, "roomId": "4"}, {"x": 784, "y": 176, "width": 32, "height": 16, "roomId": "5"}, {"x": 1008, "y": 176, "width": 32, "height": 16, "roomId": "6"}, {"x": 720, "y": 1600, "width": 32, "height": 16, "roomId": "1"}, {"x": 512, "y": 1600, "width": 32, "height": 16, "roomId": "2"}, {"x": 272, "y": 1600, "width": 32, "height": 16, "roomId": "3"}], "seats": [{"roomId": "4", "seatId": 0, "x": 560, "y": 96, "facing": "right"}, {"roomId": "4", "seatId": 1, "x": 624, "y": 96, "facing": "left"}, {"roomId": "4", "seatId": 2, "x": 592, "y": 64, "facing": "down"}, {"roomId": "4", "seatId": 3, "x": 592, "y": 128, "facing": "up"}, {"roomId": "5", "seatId": 0, "x": 768, "y": 96, "facing": "right"}, {"roomId": "5", "seatId": 1, "x": 832, "y": 96, "facing": "left"}, {"roomId": "5", "seatId": 2, "x": 800, "y": 64, "facing": "down"}, {"roomId": "5", "seatId": 3, "x": 800, "y": 128, "facing": "up"}, {"roomId": "6", "seatId": 0, "x": 992, "y": 96, "facing": "right"}, {"roomId": "6", "seatId": 1, "x": 1056, "y": 96, "facing": "left"}, {"roomId": "6", "seatId": 2, "x": 1024, "y": 64, "facing": "down"}, {"roomId": "6", "seatId": 3, "x": 1024, "y": 128, "facing": "up"}, {"roomId": "1", "seatId": 0, "x": 704, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 1, "x": 736, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 2, "x": 768, "y": 1632, "facing": "down"}, {"roomId": "1", "seatId": 3, "x": 720, "y": 1696, "facing": "up"}, {"roomId": "1", "seatId": 4, "x": 752, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 0, "x": 480, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 1, "x": 512, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 2, "x": 544, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 3, "x": 576, "y": 1632, "facing": "down"}, {"roomId": "2", "seatId": 4, "x": 480, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 5, "x": 512, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 6, "x": 544, "y": 1696, "facing": "up"}, {"roomId": "2", "seatId": 7, "x": 576, "y": 1696, "facing": "up"}, {"roomId": "3", "seatId": 0, "x": 208, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 1, "x": 240, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 2, "x": 272, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 3, "x": 304, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 4, "x": 336, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 5, "x": 368, "y": 1648, "facing": "down"}, {"roomId": "3", "seatId": 6, "x": 208, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 7, "x": 240, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 8, "x": 272, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 9, "x": 304, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 10, "x": 336, "y": 1712, "facing": "up"}, {"roomId": "3", "seatId": 11, "x": 368, "y": 1712, "facing": "up"}], "boardSeats": [{"tableId": "ttt-1", "seat": 0, "game": "tictactoe", "x": 1120, "y": 1632, "facing": "right"}, {"tableId": "ttt-1", "seat": 1, "game": "tictactoe", "x": 1184, "y": 1632, "facing": "left"}, {"tableId": "c4-1", "seat": 0, "game": "connect4", "x": 1120, "y": 1680, "facing": "right"}, {"tableId": "c4-1", "seat": 1, "game": "connect4", "x": 1184, "y": 1680, "facing": "left"}], "stageZones": [{"x": 1312, "y": 256, "width": 576, "height": 448, "name": "stage_zone", "zoneType": "stage"}, {"x": 1440, "y": 32, "width": 336, "height": 224, "name": "presenter_zone", "zoneType": "presenter"}], "portals": [{"x": 432, "y": 688, "width": 32, "height": 32, "id": 40001, "targetX": 1280, "targetY": 688}], "solidObjects": [{"key": "f_desk", "x": 264, "y": 616}, {"key": "f_plant_big", "x": 184, "y": 1512}, {"key": "f_plant_big", "x": 824, "y": 1512}, {"key": "f_lz_bookshelf", "x": 248, "y": 1624}, {"key": "f_lz_counter", "x": 360, "y": 1624}, {"key": "f_lz_fridge", "x": 392, "y": 1624}, {"key": "f_lz_table", "x": 216, "y": 1688}, {"key": "f_lz_table_books", "x": 232, "y": 1688}, {"key": "f_lz_table", "x": 248, "y": 1688}, {"key": "f_lz_table_cup", "x": 264, "y": 1688}, {"key": "f_lz_table", "x": 280, "y": 1688}, {"key": "f_lz_table_books", "x": 296, "y": 1688}, {"key": "f_lz_table", "x": 312, "y": 1688}, {"key": "f_lz_table_cup", "x": 328, "y": 1688}, {"key": "f_lz_table", "x": 344, "y": 1688}, {"key": "f_lz_table_books", "x": 360, "y": 1688}, {"key": "f_lz_table", "x": 376, "y": 1688}, {"key": "f_lz_sideboard", "x": 264, "y": 1752}, {"key": "f_lz_shelf_jars", "x": 360, "y": 1752}, {"key": "f_lz_table", "x": 520, "y": 264}, {"key": "f_lz_computer", "x": 536, "y": 264}, {"key": "f_lz_table_books", "x": 552, "y": 264}, {"key": "f_lz_table_books", "x": 1192, "y": 264}, {"key": "f_lz_computer", "x": 1208, "y": 264}, {"key": "f_lz_table", "x": 1224, "y": 264}, {"key": "f_lz_table_books", "x": 600, "y": 104}, {"key": "f_lz_table", "x": 808, "y": 104}, {"key": "f_lz_bookshelf", "x": 968, "y": 56}, {"key": "f_lz_shelf_white", "x": 1096, "y": 56}, {"key": "f_lz_globe", "x": 1128, "y": 72}, {"key": "f_lz_table_study", "x": 1032, "y": 104}, {"key": "f_lz_palm", "x": 440, "y": 1736}, {"key": "f_lz_table", "x": 488, "y": 1672}, {"key": "f_lz_table_books", "x": 504, "y": 1672}, {"key": "f_lz_table", "x": 520, "y": 1672}, {"key": "f_lz_table_cup", "x": 536, "y": 1672}, {"key": "f_lz_table", "x": 552, "y": 1672}, {"key": "f_lz_table_books", "x": 568, "y": 1672}, {"key": "f_lz_table", "x": 584, "y": 1672}, {"key": "f_lz_bookshelf", "x": 680, "y": 1736}, {"key": "f_lz_plant", "x": 808, "y": 1736}, {"key": "f_lz_table_books", "x": 712, "y": 1672}, {"key": "f_lz_table", "x": 728, "y": 1672}, {"key": "f_lz_table_cup", "x": 744, "y": 1672}, {"key": "f_lz_table", "x": 760, "y": 1672}, {"key": "f_lz_table_books", "x": 776, "y": 1672}, {"key": "f_lz_chair", "x": 1368, "y": 344}, {"key": "f_lz_chair", "x": 1416, "y": 344}, {"key": "f_lz_chair", "x": 1464, "y": 344}, {"key": "f_lz_chair", "x": 1512, "y": 344}, {"key": "f_lz_chair", "x": 1656, "y": 344}, {"key": "f_lz_chair", "x": 1704, "y": 344}, {"key": "f_lz_chair", "x": 1752, "y": 344}, {"key": "f_lz_chair", "x": 1800, "y": 344}, {"key": "f_lz_chair", "x": 1368, "y": 392}, {"key": "f_lz_chair", "x": 1416, "y": 392}, {"key": "f_lz_chair", "x": 1464, "y": 392}, {"key": "f_lz_chair", "x": 1512, "y": 392}, {"key": "f_lz_chair", "x": 1656, "y": 392}, {"key": "f_lz_chair", "x": 1704, "y": 392}, {"key": "f_lz_chair", "x": 1752, "y": 392}, {"key": "f_lz_chair", "x": 1800, "y": 392}, {"key": "f_lz_chair", "x": 1368, "y": 472}, {"key": "f_lz_chair", "x": 1416, "y": 472}, {"key": "f_lz_chair", "x": 1464, "y": 472}, {"key": "f_lz_chair", "x": 1512, "y": 472}, {"key": "f_lz_chair", "x": 1656, "y": 472}, {"key": "f_lz_chair", "x": 1704, "y": 472}, {"key": "f_lz_chair", "x": 1752, "y": 472}, {"key": "f_lz_chair", "x": 1800, "y": 472}, {"key": "f_lz_chair", "x": 1368, "y": 536}, {"key": "f_lz_chair", "x": 1416, "y": 536}, {"key": "f_lz_chair", "x": 1464, "y": 536}, {"key": "f_lz_chair", "x": 1512, "y": 536}, {"key": "f_lz_chair", "x": 1656, "y": 536}, {"key": "f_lz_chair", "x": 1704, "y": 536}, {"key": "f_lz_chair", "x": 1752, "y": 536}, {"key": "f_lz_chair", "x": 1800, "y": 536}, {"key": "f_lz_chair", "x": 1416, "y": 600}, {"key": "f_lz_chair", "x": 1464, "y": 600}, {"key": "f_lz_chair", "x": 1512, "y": 600}, {"key": "f_lz_chair", "x": 1656, "y": 600}, {"key": "f_lz_chair", "x": 1704, "y": 600}, {"key": "f_lz_chair", "x": 1752, "y": 600}, {"key": "f_lz_vending_shelf", "x": 1368, "y": 1640}, {"key": "f_lz_bench_gold", "x": 104, "y": 1096}, {"key": "f_lz_bench_gold", "x": 104, "y": 1144}, {"key": "f_lz_bin", "x": 136, "y": 1080}, {"key": "f_lz_plant_small", "x": 136, "y": 1160}, {"key": "f_lz_bench_gold", "x": 248, "y": 1064}, {"key": "f_lz_plant_small", "x": 200, "y": 1032}, {"key": "f_lz_plant_small", "x": 296, "y": 1032}, {"key": "f_lz_bench_gold", "x": 664, "y": 1208}, {"key": "f_lz_plant", "x": 616, "y": 1176}, {"key": "f_lz_plant", "x": 712, "y": 1176}, {"key": "f_lz_plant_small", "x": 456, "y": 1032}, {"key": "f_lz_plant_small", "x": 504, "y": 1032}, {"key": "f_lz_plant_small", "x": 1208, "y": 1032}, {"key": "f_lz_plant_small", "x": 1352, "y": 1032}, {"key": "f_lz_plant", "x": 760, "y": 424}, {"key": "f_lz_plant", "x": 840, "y": 424}, {"key": "f_lz_plant", "x": 1528, "y": 776}, {"key": "f_lz_plant", "x": 1640, "y": 776}, {"key": "f_arcade_breakout", "x": 1096, "y": 1544}, {"key": "f_arcade_snake", "x": 1144, "y": 1544}, {"key": "f_arcade_flappy", "x": 1224, "y": 1544}, {"key": "f_arcade_merge-drop", "x": 1320, "y": 1544}, {"key": "f_arcade_minesweeper", "x": 1368, "y": 1544}, {"key": "f_table_small", "x": 1160, "y": 1640}, {"key": "f_table_small", "x": 1160, "y": 1688}], "collision": {"cols": 120, "rows": 118, "blocked": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}} \ No newline at end of file diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1b9e1ab..8e536ba 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -161,12 +161,14 @@ failure teardown, from PR #148 review) is still tracked as an open item — see ## 9. Arcade cabinets -**What it is:** Single-player mini-games playable at map-placed arcade cabinets: **Snake** and **Flappy** -(a third game, **2048, has been retired** and is no longer offered). - -**How to use it:** Walk up to a cabinet and interact; a lazy-loaded overlay (`ArcadeOverlay`) opens over the -game world (which sleeps underneath, same pattern as meetings). Escape closes it instantly. Score is -submitted when the run ends, and a high-score list is shown. +**What it is:** Single-player mini-games playable at map-placed arcade cabinets: **Snake**, **Flappy**, +**Stellar Forge** (merge-drop), **Breakout**, and **Minesweeper** (2048 was retired and is no longer offered). + +**How to use it:** Walk south from spawn down the stone path into the Game Arcade. Walk up to a cabinet +and press **E**. A lazy-loaded overlay opens (the world sleeps underneath). Escape pauses (Resume / +Restart / Quit); F toggles fullscreen; M mutes arcade sound. Score is submitted when the run ends, and +a high-score list is shown per cabinet. Breakout: A/D or mouse to aim, click or Space to serve. +Minesweeper: click or Space to open, right-click or X to flag, Shift-click to chord. **Status & caveats:** Live. Game rules are pure, deterministic modules in `frontend/src/game/arcade/` (`snake.ts`, `flappy.ts`, plus a shared seeded PRNG) — no `Math.random`, everything replayable from a seed. diff --git a/frontend/ATTRIBUTIONS.md b/frontend/ATTRIBUTIONS.md index 0ade357..2fc468c 100644 --- a/frontend/ATTRIBUTIONS.md +++ b/frontend/ATTRIBUTIONS.md @@ -67,6 +67,7 @@ release. | `furniture/table_round.png` | Project-original | Redrawn in-repo (PRD 12) | Project asset — replaces the prior flat placeholder, matched to the furniture palette. | | `furniture/arcade_snake.png`, `furniture/arcade_flappy.png` | Project-original | Generated in-repo (PRD 11, `scripts/gen_arcade_sprites.py`) | Project asset — 32×32 upright arcade cabinets in the cool-office palette, marquee/screen tinted per game (teal/amber). Not derived from any third-party pack. | | `furniture/arcade_merge-drop.png` | Project-original | Generated in-repo (Arcade 2.0, `scripts/gen_arcade_sprites.py`) | Project asset — the Stellar Forge cabinet: same 32×32 body as its neighbours, marquee/screen tinted violet (`#b082ff`). Not derived from any third-party pack. | +| `furniture/arcade_breakout.png`, `furniture/arcade_minesweeper.png` | Project-original | Generated in-repo (`scripts/gen_arcade_sprites.py`) | Project asset — same 32×32 cabinet body, marquee/screen tinted coral (`#e2564d`) for Breakout and blue (`#5b8def`) for Minesweeper. Not derived from any third-party pack. | | `tilesets/room_builder.png`, `furniture/lz_*.png` | [Modern Interiors — RPG Tileset](https://limezu.itch.io/moderninteriors), **free** tier (`Modern_Interiors_Free_v2.2`, `Interiors_free/16x16`) | **LimeZu** | ⚠️ **NON-COMMERCIAL ONLY.** The free tier's `Modern tiles_Free/LICENSE.txt` reads verbatim: *"YOU CAN USE THE ASSET IN NON COMMERCIAL PROJECTS · YOU CAN EDIT THE SPRITES AND USE THEM IN NON COMMERCIAL PROJECTS / YOU CAN'T USE THE ASSET IN COMMERCIAL PROJECTS · YOU CAN'T EDIT THE SPRITES AND USE THEM IN COMMERCIAL PROJECTS · YOU CAN'T EDIT AND RESELL THE SPRITES."* That is fine today because hyprverse is Raja's personal, non-commercial project. **If it is ever monetised, sponsored or shipped as a product, buy the paid pack (~$1.20, carries the commercial licence) before shipping — or strip these assets.** Shipped: `room_builder.png` verbatim (interior floor tiles: brick / cross-tile / mint / screed / herringbone parquet), and 102 prop cutouts from `Interiors_free_16x16.png` cropped by `scripts/gen_limezu_sprites.py` (source tile coordinates named in that script). The raw sheets are **not** committed. Used campus-wide by the district art passes in `scripts/campus_decor/` (see `docs/ux-art-audit-2026-08-01.md` and `docs/art/limezu-catalog.md`). | | `landing/campus-hero.png` | Composed in-repo (`scripts/gen_landing_backdrop.py`) from `maps/campus.json` + the `exterior.png` / `floors_walls.png` tilesets and `furniture/*.png` (all attributed above) | (respective pack authors) | Landing diorama backdrop (PRD 19) — a pure composition of already-attributed pack assets: a fixed crop of the real campus map re-rendered to one palette-quantised PNG. No new art authored; inherits the licenses of its source tiles/sprites. | diff --git a/frontend/README.md b/frontend/README.md index 604e4ad..ac4a259 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -608,7 +608,7 @@ the feedback card. **Architecture — pure rules, thin renderers, audio-agnostic:** - **Rules** live in pure modules under `src/game/arcade/` — `snake.ts`, - `flappy.ts`, `mergedrop.ts`, plus a seeded PRNG (`prng.ts`). Plain values in, + `flappy.ts`, `mergedrop.ts`, `breakout.ts`, `minesweeper.ts`, plus a seeded PRNG (`prng.ts`). Plain values in, plain values out; no Phaser/net/DOM imports. Randomness flows through the serializable `rngSeed` in each state, so *a given seed + input script always reproduces a run* (asserted by determinism tests). Each module lands with its @@ -661,6 +661,16 @@ the map — never hand-edit `campus.json`. Add a cabinet sprite via #### Stellar Forge — the merge-drop cabinet (Arcade 2.0) +**Breakout** (`breakout`, west wall of the Arcade Hall at tile 68,96). Aim the +paddle with the mouse or A/D (or arrows); click or Space to serve. Clear the +brick field to advance a level; three missed balls end the run. Combo multiplies +brick points. + +**Minesweeper** (`minesweeper`, east of Stellar Forge at tile 85,96). 10×10 +board, 12 mines. Click or Space opens a cell (the first click is always safe); +right-click or X flags; Shift-click / middle-click chords a numbered cell. +Score is 10 per safe reveal plus a win bonus and leftover time. + The flagship cabinet (`merge-drop`, east of the Arcade Hall doorway at tile 82,96). You release celestial bodies into a gravity well; two touching bodies of the same tier fuse into the next rung of a ten-step evolution ladder (Pebble → diff --git a/frontend/public/assets/furniture/arcade_breakout.png b/frontend/public/assets/furniture/arcade_breakout.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba85a1fdb06e6115106d24f16291b9ad7ec3591 GIT binary patch literal 363 zcmV-x0hIoUP)9%JP_4f}=#k289 z;+aoDHXd=dc=-10{Nz|J_!DvqJP9<~f;^wsay++TV~fa(;0DZhK{@7|>;izD{(;*- z@C4*j;Ms)ADF~iGa0AK^#sK%XsYgEsuBr&i48)$wB~%&+j!%)MXVA*?ih}?E002ov JPDHLkV1naaqlf?i literal 0 HcmV?d00001 diff --git a/frontend/public/assets/furniture/arcade_minesweeper.png b/frontend/public/assets/furniture/arcade_minesweeper.png new file mode 100644 index 0000000000000000000000000000000000000000..46725b30d577e87c49784a2ad39b0e98216b1e06 GIT binary patch literal 366 zcmV-!0g?WRP)Q9NAR_;t9K%1nc}UQF7Ge$auDn2!7<%K`h*DR)@E6f$ zlH>qV45qpR?(eD~sOHK2t0X&M=p;f01_p*-zkU*>j*X3-XfsK200RR9Lty>&llbJ< zH)@)bVjf9RKxzn3Jqj2YFhc<5Bw2oJ?@FT7k(>*T?On+rXjX|LHg9S@(H5cuqN9ME z5TI%leE9H@pc-;&L#h=-=2 tiles): · cols 78-81 stay free of solid bodies — door spine from the threshold south - · rows 98-99 under both cabinet interactables stay clear (play stance) + · rows 98-99 under the cabinet interactables stay clear (play stance) · the board-table block (x=70-74, y=102-105) carries no props at all; it is approached from the east (x>=76) and from the north (y=100-101 cross-aisle) · the room centre (x=76-83, y=100-106) is open floor — spectator space @@ -52,7 +53,8 @@ def _paint_floor(ctx) -> None: # tile of the snack corner. The centre stays plain cream. ctx.fill_pattern(AH_X0, AH_Y0, AH_X1, AH_Y1, ctx.FLOOR_CREAM) ctx.fill_pattern(70, 101, 74, 106, ctx.FLOOR_PARQUET) # board-table mat - ctx.fill_pattern(84, 95, 86, 99, ctx.FLOOR_SLATE) # snack corner (E wall) + ctx.fill_pattern(84, 95, 86, 99, ctx.FLOOR_SLATE) # minesweeper pad (E wall) + ctx.fill_pattern(84, 101, 86, 104, ctx.FLOOR_SLATE) # relocated snack corner # Door gap: shell paves stone; cream threshold so the entrance reads as room. ctx.fill_pattern(78, 94, 81, 94, ctx.FLOOR_CREAM) @@ -61,10 +63,10 @@ def _furnish(ctx) -> None: # Cols 78-81 never solid. Rows 98-99 under the cabinets stay free of solids. # ── Cabinet wall dressing (non-solid, flush to the north wall) ──────── - ctx.furn("f_lz_poster_grid", 69, 95, False) # poster left of the snake cab - # Chalk scoreboard, hung in the gap between the two cabinet sprites - # (snake ends at x=72.5, flappy starts at x=75.5). + # Poster in the gap between Breakout (68) and Snake (71). + ctx.furn("f_lz_poster_grid", 70, 95, False) + # Chalk scoreboard in the gap between Snake (71) and Flappy (76). ctx.furn("f_lz_chalkboard", 74, 95, False) - # ── Snack point (east wall) ─────────────────────────────────────────── - ctx.furn("f_lz_vending_shelf", 85, 96, True) + # ── Snack point (east wall, south of the Minesweeper cabinet) ───────── + ctx.furn("f_lz_vending_shelf", 85, 102, True) diff --git a/frontend/scripts/gen_arcade_sprites.py b/frontend/scripts/gen_arcade_sprites.py index a60f7a6..af0b551 100644 --- a/frontend/scripts/gen_arcade_sprites.py +++ b/frontend/scripts/gen_arcade_sprites.py @@ -6,7 +6,7 @@ marquee + screen glow is tinted per cabinet). Cohesive with the existing 16/32px Pipoya-family furniture PNGs; NOT derived from any third-party pack. -Writes: public/assets/furniture/arcade_{snake,flappy,merge-drop}.png +Writes: public/assets/furniture/arcade_{snake,flappy,merge-drop,breakout,minesweeper}.png Run: python3 scripts/gen_arcade_sprites.py """ import os @@ -31,6 +31,8 @@ "snake": (127, 209, 185, 255), # teal #7fd1b9 "flappy": (242, 193, 78, 255), # amber #f2c14e "merge-drop": (176, 130, 255, 255), # violet #b082ff — Stellar Forge + "breakout": (226, 86, 77, 255), # coral #e2564d + "minesweeper": (91, 141, 239, 255), # blue #5b8def } diff --git a/frontend/scripts/gen_campus.py b/frontend/scripts/gen_campus.py index 3525ea7..ea064ac 100644 --- a/frontend/scripts/gen_campus.py +++ b/frontend/scripts/gen_campus.py @@ -535,7 +535,19 @@ def plant_tree(block, tx, ty): # tiles below it: the 32px solid cabinet body clears by row 98, so rows 98-99 # give a collision-free approach strip that is still inside the findNear rect. # `game` selects the module. Snake/Flappy sit west of the north doorway - # (x=79-80), Stellar Forge east of it. (2048 was retired — PRD 25.36.) + # (x=79-80), Stellar Forge east of it. Breakout is against the west wall; + # Minesweeper sits east of Stellar Forge (the north-wall run east of the door). + # (2048 was retired — PRD 25.36.) + { + "id": 40013, "name": "arcade_breakout", + "x": 68 * TS, "y": 96 * TS, "width": 2 * TS, "height": 4 * TS, + "rotation": 0, "type": "", "visible": True, + "properties": [ + {"name": "interactType", "type": "string", "value": "arcade"}, + {"name": "label", "type": "string", "value": "Breakout"}, + {"name": "game", "type": "string", "value": "breakout"}, + ], + }, { "id": 40010, "name": "arcade_snake", "x": 71 * TS, "y": 96 * TS, "width": 2 * TS, "height": 4 * TS, @@ -546,6 +558,16 @@ def plant_tree(block, tx, ty): {"name": "game", "type": "string", "value": "snake"}, ], }, + { + "id": 40014, "name": "arcade_minesweeper", + "x": 85 * TS, "y": 96 * TS, "width": 2 * TS, "height": 4 * TS, + "rotation": 0, "type": "", "visible": True, + "properties": [ + {"name": "interactType", "type": "string", "value": "arcade"}, + {"name": "label", "type": "string", "value": "Minesweeper"}, + {"name": "game", "type": "string", "value": "minesweeper"}, + ], + }, { "id": 40011, "name": "arcade_flappy", "x": 76 * TS, "y": 96 * TS, "width": 2 * TS, "height": 4 * TS, @@ -678,11 +700,14 @@ def furn(key, tx, ty, solid): # Arcade Room (south) — solid cabinets lining the north wall; each pairs with an # arcade interactable zone at the same tile (see interactables_objs). Plus a # little themed dressing so the hall doesn't read empty. +furn("f_arcade_breakout", 68, 96, True) furn("f_arcade_snake", 71, 96, True) furn("f_arcade_flappy", 76, 96, True) -# Stellar Forge — east of the doorway (cols 78-81 free), west of the snack -# corner dressing in campus_decor/arcade_hall.py. Same wall run as its neighbours. +# Stellar Forge — east of the doorway (cols 78-81 free). Minesweeper continues +# the same north-wall run east of it; the snack vending moves south along the +# east wall in campus_decor/arcade_hall.py so the two 32px bodies do not overlap. furn("f_arcade_merge-drop", 82, 96, True) +furn("f_arcade_minesweeper", 85, 96, True) # Arcade dressing: re-authored in scripts/campus_decor/ (LimeZu art pass). diff --git a/frontend/src/game/arcade/breakout.test.ts b/frontend/src/game/arcade/breakout.test.ts new file mode 100644 index 0000000..067a4dd --- /dev/null +++ b/frontend/src/game/arcade/breakout.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; +import { + BASE_BALL_SPEED, + BREAKOUT_COLS, + BREAKOUT_HEIGHT, + BREAKOUT_LIVES, + BREAKOUT_STEP, + BREAKOUT_WIDTH, + breakoutTick, + initBreakout, + type BreakoutInput, + type BreakoutState, +} from "./breakout"; + +const HOLD: BreakoutInput = { aim: null, move: 0, launch: false }; +const LAUNCH: BreakoutInput = { aim: null, move: 0, launch: true }; + +function play(state: BreakoutState, input: BreakoutInput = HOLD): BreakoutState { + return breakoutTick(state, input).state; +} + +function launch(seed = 1): BreakoutState { + return play(initBreakout(seed), LAUNCH); +} + +describe("initBreakout", () => { + it("starts ready with a full brick field, three lives, and a parked ball", () => { + const s = initBreakout(1); + expect(s.phase).toBe("ready"); + expect(s.lives).toBe(BREAKOUT_LIVES); + expect(s.score).toBe(0); + expect(s.level).toBe(1); + expect(s.bricks.length).toBeGreaterThan(BREAKOUT_COLS * 3); + expect(s.bricks.every((b) => b.hp > 0)).toBe(true); + expect(s.ballVx).toBe(0); + expect(s.ballVy).toBe(0); + expect(s.paddleX).toBe(BREAKOUT_WIDTH / 2); + expect(s.ballX).toBe(s.paddleX); + expect(s.height).toBe(BREAKOUT_HEIGHT); + expect(s.paddleY).toBe(BREAKOUT_HEIGHT - 28); + }); + + it("accepts a taller court without changing the brick field width", () => { + const s = initBreakout(1, 720); + expect(s.width).toBe(BREAKOUT_WIDTH); + expect(s.height).toBe(720); + expect(s.paddleY).toBe(720 - 28); + const desktop = initBreakout(1); + expect(s.bricks.length).toBe(desktop.bricks.length); + expect(s.bricks[0]?.x).toBe(desktop.bricks[0]?.x); + expect(s.bricks[0]?.w).toBe(desktop.bricks[0]?.w); + }); +}); + +describe("breakoutTick — ready / serve", () => { + it("keeps the ball glued to a steered paddle until launch", () => { + const left = play(initBreakout(1), { aim: null, move: -1, launch: false }); + expect(left.phase).toBe("ready"); + expect(left.paddleX).toBeLessThan(BREAKOUT_WIDTH / 2); + expect(left.ballX).toBe(left.paddleX); + expect(left.ballY).toBeLessThan(left.paddleY); + }); + + it("serves on launch and starts the run", () => { + const s = launch(7); + expect(s.phase).toBe("play"); + expect(s.ballVy).toBeLessThan(0); + expect(Math.abs(s.ballVx) + Math.abs(s.ballVy)).toBeGreaterThan(0); + }); + + it("is a no-op once the run is over", () => { + const over: BreakoutState = { ...initBreakout(1), phase: "over", lives: 0 }; + expect(breakoutTick(over, LAUNCH).state).toBe(over); + expect(breakoutTick(over, LAUNCH).events).toEqual([]); + }); +}); + +describe("breakoutTick — paddle and walls", () => { + it("clamps pointer aim to the inner walls", () => { + const s = play(initBreakout(1), { aim: -400, move: 0, launch: false }); + expect(s.paddleX).toBeGreaterThan(s.paddleW / 2); + const right = play(initBreakout(1), { aim: 4000, move: 0, launch: false }); + expect(right.paddleX).toBeLessThan(BREAKOUT_WIDTH - s.paddleW / 2); + }); + + it("bounces off the side walls instead of leaving the court", () => { + let s = launch(3); + s = { + ...s, + ballX: 12, + ballY: 220, + ballVx: -400, + ballVy: 0, + }; + const next = play(s); + expect(next.ballVx).toBeGreaterThan(0); + expect(next.ballX).toBeGreaterThanOrEqual(s.ballR); + }); +}); + +describe("breakoutTick — bricks, lives, levels", () => { + it("breaks a brick, awards its points, and emits brick", () => { + const base = launch(1); + const brick = base.bricks[0]; + if (!brick) throw new Error("expected a brick"); + const aimed: BreakoutState = { + ...base, + ballX: brick.x + brick.w / 2, + ballY: brick.y + brick.h + base.ballR - 0.5, + ballVx: 0, + ballVy: -BASE_BALL_SPEED, + }; + const step = breakoutTick(aimed, HOLD); + expect(step.events).toContain("brick"); + expect(step.state.score).toBeGreaterThan(0); + expect(step.state.bricks.length).toBeLessThan(aimed.bricks.length); + }); + + it("drops a life and returns to ready when the ball falls off the bottom", () => { + const s: BreakoutState = { + ...launch(1), + ballX: BREAKOUT_WIDTH / 2, + ballY: BREAKOUT_HEIGHT + 20, + ballVx: 0, + ballVy: 200, + }; + const step = breakoutTick(s, HOLD); + expect(step.events).toContain("life"); + expect(step.state.lives).toBe(BREAKOUT_LIVES - 1); + expect(step.state.phase).toBe("ready"); + expect(step.state.combo).toBe(0); + }); + + it("ends the run on the last dropped life", () => { + const s: BreakoutState = { + ...launch(1), + lives: 1, + ballX: BREAKOUT_WIDTH / 2, + ballY: BREAKOUT_HEIGHT + 20, + ballVx: 0, + ballVy: 200, + }; + const step = breakoutTick(s, HOLD); + expect(step.events).toContain("over"); + expect(step.state.phase).toBe("over"); + expect(step.state.lives).toBe(0); + }); + + it("refills a harder field when the last brick is cleared", () => { + const base = launch(1); + const last = base.bricks[0]; + if (!last) throw new Error("expected a brick"); + const s: BreakoutState = { + ...base, + bricks: [{ ...last, hp: 1 }], + ballX: last.x + last.w / 2, + ballY: last.y + last.h + base.ballR - 0.5, + ballVx: 0, + ballVy: -BASE_BALL_SPEED, + }; + const step = breakoutTick(s, HOLD); + expect(step.events).toContain("level"); + expect(step.state.level).toBe(2); + expect(step.state.bricks.length).toBeGreaterThan(1); + expect(step.state.phase).toBe("play"); + }); +}); + +describe("determinism", () => { + function run(seed: number): BreakoutState { + let s = initBreakout(seed); + for (let i = 0; i < 4000 && s.phase !== "over"; i += 1) { + const launchNow = s.phase === "ready" && i % 30 === 0; + const move = (i % 80 < 40 ? -1 : 1) as -1 | 1; + s = breakoutTick(s, { aim: null, move, launch: launchNow }, BREAKOUT_STEP).state; + } + return s; + } + + it("same seed + input script ⇒ identical outcome", () => { + expect(run(555)).toEqual(run(555)); + }); + + it("different seeds diverge", () => { + expect(run(555).rngSeed).not.toBe(run(777).rngSeed); + }); +}); diff --git a/frontend/src/game/arcade/breakout.ts b/frontend/src/game/arcade/breakout.ts new file mode 100644 index 0000000..f3be34f --- /dev/null +++ b/frontend/src/game/arcade/breakout.ts @@ -0,0 +1,455 @@ +/** + * Breakout — pure game rules. + * + * Plain values in, plain values out; no Phaser/net/DOM imports. Brick layout + * and the serve angle are drawn from `rngSeed`, so a seed + input script + * reproduces a run (see breakout.test.ts). The renderer feeds `breakoutTick` + * on a fixed timestep (`BREAKOUT_STEP`) with the latest paddle intent. + * + * World: origin top-left, `y` down. The ball is a circle; bricks and the + * paddle are axis-aligned rects. Collision uses only `+ - * /`, `Math.sqrt`, + * `Math.abs`, `Math.min`/`Math.max` — no wall-clock time. + * + * Phases: `ready` (ball sits on the paddle) → `play` → `over`. Clearing a + * board advances the level in-place (still `play`); losing the last life + * ends the run. + */ +import { nextFloat } from "./prng"; + +export type BreakoutPhase = "ready" | "play" | "over"; + +export type BreakoutEvent = + | "launch" + | "brick" + | "paddle" + | "wall" + | "life" + | "level" + | "over" + | "combo"; + +export interface Brick { + readonly x: number; + readonly y: number; + readonly w: number; + readonly h: number; + readonly hp: number; + readonly points: number; + readonly row: number; +} + +export interface BreakoutState { + readonly width: number; + readonly height: number; + readonly paddleX: number; + readonly paddleY: number; + readonly paddleW: number; + readonly paddleH: number; + readonly paddleSpeed: number; + readonly ballX: number; + readonly ballY: number; + readonly ballVx: number; + readonly ballVy: number; + readonly ballR: number; + readonly ballSpeed: number; + readonly bricks: readonly Brick[]; + readonly lives: number; + readonly level: number; + readonly score: number; + readonly combo: number; + readonly phase: BreakoutPhase; + readonly rngSeed: number; +} + +export interface BreakoutInput { + /** Pointer aim for the paddle centre, in world x; null when keyboard-only. */ + readonly aim: number | null; + /** Held keyboard steer: -1 left, 0 none, 1 right. */ + readonly move: -1 | 0 | 1; + /** True on the tick that should serve a waiting ball. */ + readonly launch: boolean; +} + +export interface BreakoutStep { + readonly state: BreakoutState; + readonly events: readonly BreakoutEvent[]; +} + +export const BREAKOUT_STEP = 1 / 120; + +export const BREAKOUT_WIDTH = 640; +export const BREAKOUT_HEIGHT = 400; +export const BREAKOUT_LIVES = 3; +export const BREAKOUT_COLS = 10; +export const BREAKOUT_ROWS = 6; +export const BRICK_GAP = 4; +export const BRICK_TOP = 36; +export const BRICK_H = 14; +export const PADDLE_W = 78; +export const PADDLE_H = 10; +export const PADDLE_SPEED = 420; +export const BALL_R = 5.5; +export const BASE_BALL_SPEED = 280; +export const LEVEL_SPEED_STEP = 22; +export const MAX_BALL_SPEED = 420; +export const MIN_VY_FRAC = 0.42; +export const WALL_PAD = 10; + +const POINTS_BY_ROW = [60, 50, 40, 30, 20, 10] as const; + +function clamp(v: number, lo: number, hi: number): number { + return v < lo ? lo : v > hi ? hi : v; +} + +function brickField( + width: number, + rows: number, + cols: number, + hp: number +): Brick[] { + const inset = WALL_PAD + 6; + const usable = width - inset * 2; + const w = (usable - BRICK_GAP * (cols - 1)) / cols; + const bricks: Brick[] = []; + for (let row = 0; row < rows; row += 1) { + const points = POINTS_BY_ROW[row] ?? 10; + for (let col = 0; col < cols; col += 1) { + bricks.push({ + x: inset + col * (w + BRICK_GAP), + y: BRICK_TOP + row * (BRICK_H + BRICK_GAP), + w, + h: BRICK_H, + hp, + points, + row, + }); + } + } + return bricks; +} + +function paddleY(height: number): number { + return height - 28; +} + +function restBallX(paddleX: number): number { + return paddleX; +} + +function restBallY(height: number, radius: number): number { + return paddleY(height) - radius - 0.5; +} + +function withSpeed( + vx: number, + vy: number, + speed: number +): { vx: number; vy: number } { + const mag = Math.sqrt(vx * vx + vy * vy); + if (mag <= 0) return { vx: 0, vy: -speed }; + let nx = (vx / mag) * speed; + let ny = (vy / mag) * speed; + const minVy = speed * MIN_VY_FRAC; + if (Math.abs(ny) < minVy) { + ny = ny < 0 ? -minVy : minVy; + const rest = Math.sqrt(Math.max(0, speed * speed - ny * ny)); + nx = nx < 0 ? -rest : rest; + } + return { vx: nx, vy: ny }; +} + +function serveVelocity(seed: number, speed: number): { vx: number; vy: number; seed: number } { + const draw = nextFloat(seed); + const side = draw.value < 0.5 ? -1 : 1; + const tilt = 0.35 + draw.value * 0.45; + const { vx, vy } = withSpeed(side * tilt, -1, speed); + return { vx, vy, seed: draw.seed }; +} + +function levelHp(level: number): number { + return level >= 3 ? 2 : 1; +} + +function levelSpeed(level: number): number { + return Math.min(MAX_BALL_SPEED, BASE_BALL_SPEED + (level - 1) * LEVEL_SPEED_STEP); +} + +function levelRows(level: number): number { + return Math.min(BREAKOUT_ROWS, 4 + level); +} + +/** + * Minimum court height that still fits the brick field, ball rest, and paddle. + * Portrait cabinets pass a larger height; the default desktop court is 400. + */ +const MIN_COURT_HEIGHT = + BRICK_TOP + BREAKOUT_ROWS * (BRICK_H + BRICK_GAP) + 80; + +export function initBreakout(seed: number, height = BREAKOUT_HEIGHT): BreakoutState { + const width = BREAKOUT_WIDTH; + const courtHeight = + Number.isFinite(height) && height >= MIN_COURT_HEIGHT ? height : BREAKOUT_HEIGHT; + const paddleX = width / 2; + return { + width, + height: courtHeight, + paddleX, + paddleY: paddleY(courtHeight), + paddleW: PADDLE_W, + paddleH: PADDLE_H, + paddleSpeed: PADDLE_SPEED, + ballX: restBallX(paddleX), + ballY: restBallY(courtHeight, BALL_R), + ballVx: 0, + ballVy: 0, + ballR: BALL_R, + ballSpeed: BASE_BALL_SPEED, + bricks: brickField(width, levelRows(1), BREAKOUT_COLS, levelHp(1)), + lives: BREAKOUT_LIVES, + level: 1, + score: 0, + combo: 0, + phase: "ready", + rngSeed: seed >>> 0 || 1, + }; +} + +function steerPaddle(state: BreakoutState, input: BreakoutInput, dt: number): number { + const half = state.paddleW / 2; + const minX = WALL_PAD + half; + const maxX = state.width - WALL_PAD - half; + if (input.aim !== null) return clamp(input.aim, minX, maxX); + return clamp(state.paddleX + input.move * state.paddleSpeed * dt, minX, maxX); +} + +function circleHitsRect( + cx: number, + cy: number, + r: number, + x: number, + y: number, + w: number, + h: number +): boolean { + const closestX = clamp(cx, x, x + w); + const closestY = clamp(cy, y, y + h); + const dx = cx - closestX; + const dy = cy - closestY; + return dx * dx + dy * dy <= r * r; +} + +function bounceOffRect( + cx: number, + cy: number, + vx: number, + vy: number, + r: number, + x: number, + y: number, + w: number, + h: number +): { vx: number; vy: number; x: number; y: number } { + const closestX = clamp(cx, x, x + w); + const closestY = clamp(cy, y, y + h); + const dx = cx - closestX; + const dy = cy - closestY; + if (Math.abs(dx) > Math.abs(dy)) { + return { + vx: -vx, + vy, + x: dx < 0 ? x - r : x + w + r, + y: cy, + }; + } + return { + vx, + vy: -vy, + x: cx, + y: dy < 0 ? y - r : y + h + r, + }; +} + +function bouncePaddle( + state: BreakoutState, + ballX: number, + speed: number +): { vx: number; vy: number } { + const half = state.paddleW / 2; + const hit = clamp((ballX - state.paddleX) / half, -1, 1); + return withSpeed(hit * 0.95, -1, speed); +} + +function refillBricks(state: BreakoutState, level: number): readonly Brick[] { + return brickField(state.width, levelRows(level), BREAKOUT_COLS, levelHp(level)); +} + +function parkBall(state: BreakoutState, paddleX: number, lives: number): BreakoutState { + return { + ...state, + paddleX, + ballX: restBallX(paddleX), + ballY: restBallY(state.height, state.ballR), + ballVx: 0, + ballVy: 0, + combo: 0, + lives, + phase: lives <= 0 ? "over" : "ready", + }; +} + +/** + * Advance one physics step. `dt` is seconds; the renderer uses `BREAKOUT_STEP`. + * Terminal states (`over`) are identity so a finishing overlay can keep ticking. + */ +export function breakoutTick( + state: BreakoutState, + input: BreakoutInput, + dt: number = BREAKOUT_STEP +): BreakoutStep { + if (state.phase === "over") return { state, events: [] }; + + const paddleX = steerPaddle(state, input, dt); + if (state.phase === "ready") { + let next: BreakoutState = { + ...state, + paddleX, + ballX: restBallX(paddleX), + ballY: restBallY(state.height, state.ballR), + }; + if (!input.launch) return { state: next, events: [] }; + const served = serveVelocity(next.rngSeed, next.ballSpeed); + next = { + ...next, + phase: "play", + ballVx: served.vx, + ballVy: served.vy, + rngSeed: served.seed, + }; + return { state: next, events: ["launch"] }; + } + + const events: BreakoutEvent[] = []; + let ballX = state.ballX + state.ballVx * dt; + let ballY = state.ballY + state.ballVy * dt; + let vx = state.ballVx; + let vy = state.ballVy; + let speed = state.ballSpeed; + const r = state.ballR; + const left = WALL_PAD + r; + const right = state.width - WALL_PAD - r; + const top = WALL_PAD + r; + + if (ballX <= left) { + ballX = left; + vx = Math.abs(vx); + events.push("wall"); + } else if (ballX >= right) { + ballX = right; + vx = -Math.abs(vx); + events.push("wall"); + } + if (ballY <= top) { + ballY = top; + vy = Math.abs(vy); + events.push("wall"); + } + + if (ballY - r > state.height) { + const parked = parkBall({ ...state, paddleX }, paddleX, state.lives - 1); + events.push("life"); + if (parked.phase === "over") events.push("over"); + return { state: parked, events }; + } + + const paddleRect = { + x: paddleX - state.paddleW / 2, + y: state.paddleY, + w: state.paddleW, + h: state.paddleH, + }; + if (vy > 0 && circleHitsRect(ballX, ballY, r, paddleRect.x, paddleRect.y, paddleRect.w, paddleRect.h)) { + const bounced = bouncePaddle({ ...state, paddleX }, ballX, speed); + vx = bounced.vx; + vy = bounced.vy; + ballY = state.paddleY - r - 0.5; + events.push("paddle"); + } + + let bricks = state.bricks; + let score = state.score; + let combo = events.includes("paddle") ? 0 : state.combo; + let hitIndex = -1; + for (let i = 0; i < bricks.length; i += 1) { + const brick = bricks[i]; + if (!brick || brick.hp <= 0) continue; + if (circleHitsRect(ballX, ballY, r, brick.x, brick.y, brick.w, brick.h)) { + hitIndex = i; + break; + } + } + if (hitIndex >= 0) { + const brick = bricks[hitIndex]; + if (brick) { + const bounced = bounceOffRect(ballX, ballY, vx, vy, r, brick.x, brick.y, brick.w, brick.h); + vx = bounced.vx; + vy = bounced.vy; + ballX = bounced.x; + ballY = bounced.y; + const hp = brick.hp - 1; + const nextBricks = bricks.slice(); + nextBricks[hitIndex] = { ...brick, hp }; + bricks = nextBricks; + events.push("brick"); + if (hp <= 0) { + combo += 1; + const gain = brick.points * combo; + score += gain; + if (combo >= 3) events.push("combo"); + } + } + } + + const remaining = bricks.filter((b) => b.hp > 0); + if (remaining.length === 0) { + const level = state.level + 1; + speed = levelSpeed(level); + const served = serveVelocity(state.rngSeed, speed); + events.push("level"); + return { + state: { + ...state, + paddleX, + ballX: restBallX(paddleX), + ballY: restBallY(state.height, r), + ballVx: served.vx, + ballVy: served.vy, + ballSpeed: speed, + bricks: refillBricks(state, level), + level, + score, + combo: 0, + phase: "play", + rngSeed: served.seed, + }, + events, + }; + } + + const scaled = withSpeed(vx, vy, speed); + return { + state: { + ...state, + paddleX, + ballX, + ballY, + ballVx: scaled.vx, + ballVy: scaled.vy, + ballSpeed: speed, + bricks: remaining, + score, + combo, + phase: "play", + }, + events, + }; +} diff --git a/frontend/src/game/arcade/minesweeper.test.ts b/frontend/src/game/arcade/minesweeper.test.ts new file mode 100644 index 0000000..b6ea35a --- /dev/null +++ b/frontend/src/game/arcade/minesweeper.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from "vitest"; +import { + MINES_CELL_COUNT, + MINES_COLS, + MINES_COUNT, + REVEAL_POINTS, + WIN_BONUS, + initMinesweeper, + minesChord, + minesFlag, + minesMoveCursor, + minesNeighbors, + minesRemaining, + minesReveal, + minesRevealedSafe, + minesTick, + type MinesState, +} from "./minesweeper"; + +function revealAllSafe(seed: number, start: number): MinesState { + let s = minesReveal(initMinesweeper(seed), start).state; + for (let guard = 0; guard < MINES_CELL_COUNT && s.phase === "play"; guard += 1) { + const hidden = s.cells.findIndex((c) => !c.mine && c.mark === "hidden"); + if (hidden < 0) break; + s = minesReveal(s, hidden).state; + } + return s; +} + +describe("initMinesweeper", () => { + it("starts ready with a covered board and no mines placed", () => { + const s = initMinesweeper(1); + expect(s.phase).toBe("ready"); + expect(s.placed).toBe(false); + expect(s.cells).toHaveLength(MINES_CELL_COUNT); + expect(s.cells.every((c) => c.mark === "hidden" && !c.mine)).toBe(true); + expect(s.score).toBe(0); + expect(minesRemaining(s)).toBe(MINES_COUNT); + }); +}); + +describe("minesReveal", () => { + it("places mines after the first click and never mines the opening", () => { + const step = minesReveal(initMinesweeper(9), 0); + expect(step.state.placed).toBe(true); + expect(step.state.phase).toBe("play"); + expect(step.events).toContain("reveal"); + const opening = new Set([0, ...minesNeighbors(0, MINES_COLS, step.state.rows)]); + for (const i of opening) { + const cell = step.state.cells[i]; + expect(cell?.mine, `opening cell ${i} is safe`).toBe(false); + } + expect(step.state.cells.filter((c) => c.mine)).toHaveLength(MINES_COUNT); + }); + + it("floods zeroes so a corner opening reveals more than one cell", () => { + const s = minesReveal(initMinesweeper(1), 0).state; + expect(minesRevealedSafe(s)).toBeGreaterThan(1); + const origin = s.cells[0]; + expect(origin?.mark).toBe("revealed"); + expect(origin?.mine).toBe(false); + }); + + it("ignores a flagged cell", () => { + const opened = minesReveal(initMinesweeper(2), 0).state; + const hidden = opened.cells.findIndex((c) => c.mark === "hidden"); + expect(hidden).toBeGreaterThanOrEqual(0); + const flagged = minesFlag(opened, hidden).state; + const again = minesReveal(flagged, hidden); + expect(again.state.cells[hidden]?.mark).toBe("flag"); + expect(again.events).toEqual([]); + }); + + it("does not place mines or start the clock when the first click is a flag", () => { + const flagged = minesFlag(initMinesweeper(1), 0).state; + const step = minesReveal(flagged, 0); + expect(step.events).toEqual([]); + expect(step.state.placed).toBe(false); + expect(step.state.phase).toBe("ready"); + expect(step.state.elapsedMs).toBe(0); + expect(step.state.cells[0]?.mark).toBe("flag"); + expect(step.state.rngSeed).toBe(flagged.rngSeed); + const opened = minesReveal(step.state, 1); + expect(opened.state.placed).toBe(true); + expect(opened.events).toContain("reveal"); + }); + + it("ignores a second click on an already-open cell", () => { + const opened = minesReveal(initMinesweeper(2), 0).state; + const noop = minesReveal(opened, 0); + expect(noop.events).toEqual([]); + expect(noop.state.cells[0]?.mark).toBe("revealed"); + }); + + it("loses when a mine is revealed after placement", () => { + const opened = minesReveal(initMinesweeper(4), 0).state; + const mine = opened.cells.findIndex((c) => c.mine && c.mark === "hidden"); + expect(mine).toBeGreaterThanOrEqual(0); + const step = minesReveal(opened, mine); + expect(step.events).toContain("boom"); + expect(step.state.phase).toBe("lost"); + expect(step.state.boomIndex).toBe(mine); + expect(step.state.cells[mine]?.mark).toBe("revealed"); + }); + + it("wins once every safe cell is open and auto-flags remaining mines", () => { + const s = revealAllSafe(11, 0); + expect(s.phase).toBe("won"); + expect(s.cells.filter((c) => c.mine && c.mark === "flag")).toHaveLength(MINES_COUNT); + expect(s.score).toBeGreaterThanOrEqual(WIN_BONUS + minesRevealedSafe(s) * REVEAL_POINTS); + }); +}); + +describe("minesFlag / cursor / tick", () => { + it("toggles a flag and updates remaining mines", () => { + const s = initMinesweeper(1); + const flagged = minesFlag(s, 5); + expect(flagged.events).toEqual(["flag"]); + expect(flagged.state.cells[5]?.mark).toBe("flag"); + expect(minesRemaining(flagged.state)).toBe(MINES_COUNT - 1); + const cleared = minesFlag(flagged.state, 5); + expect(cleared.events).toEqual(["unflag"]); + expect(cleared.state.cells[5]?.mark).toBe("hidden"); + }); + + it("moves the keyboard cursor and stops at the edges", () => { + const s = initMinesweeper(1); + expect(minesMoveCursor(s, "left")).toBe(s); + expect(minesMoveCursor(s, "up")).toBe(s); + const right = minesMoveCursor(s, "right"); + expect(right.cursor).toBe(1); + const down = minesMoveCursor(s, "down"); + expect(down.cursor).toBe(MINES_COLS); + }); + + it("advances elapsed time only while playing", () => { + const ready = minesTick(initMinesweeper(1), 1000); + expect(ready.elapsedMs).toBe(0); + const playing = minesReveal(initMinesweeper(1), 0).state; + const ticked = minesTick(playing, 1500); + expect(ticked.elapsedMs).toBe(1500); + const lost = minesReveal(playing, playing.cells.findIndex((c) => c.mine)).state; + expect(minesTick(lost, 4000).elapsedMs).toBe(lost.elapsedMs); + }); +}); + +describe("minesChord", () => { + it("opens remaining neighbours when the flags match the number", () => { + const blank = initMinesweeper(1); + const cells = blank.cells.map((cell, i) => { + if (i === 12) return { mine: true, adjacent: 0, mark: "flag" as const }; + if (i === 11) return { mine: false, adjacent: 1, mark: "revealed" as const }; + if (i === 0 || i === 1 || i === 2 || i === 10 || i === 20 || i === 21 || i === 22) { + return { mine: false, adjacent: 1, mark: "hidden" as const }; + } + return cell; + }); + const s: MinesState = { + ...blank, + cells, + placed: true, + phase: "play", + cursor: 11, + }; + const step = minesChord(s, 11); + expect(step.events).toContain("chord"); + expect(step.state.cells[12]?.mark).toBe("flag"); + expect(step.state.cells[10]?.mark).toBe("revealed"); + expect(step.state.cells[21]?.mark).toBe("revealed"); + }); + + it("is ignored on hidden cells and when flags do not match", () => { + const opened = minesReveal(initMinesweeper(8), 0).state; + const hidden = opened.cells.findIndex((c) => c.mark === "hidden"); + expect(minesChord(opened, hidden).events).toEqual([]); + const numbered = opened.cells.findIndex((c) => c.mark === "revealed" && c.adjacent > 0); + expect(minesChord(opened, numbered).events).toEqual([]); + }); +}); + +describe("determinism", () => { + function run(seed: number): MinesState { + let s = initMinesweeper(seed); + const script = [0, 3, 12, 25, 40, 55, 70, 88, 99]; + for (const i of script) { + if (s.phase !== "ready" && s.phase !== "play") break; + if (i % 2 === 0) s = minesFlag(s, (i + 7) % MINES_CELL_COUNT).state; + s = minesReveal(s, i).state; + s = minesTick(s, 16); + } + return s; + } + + it("same seed + click script ⇒ identical outcome", () => { + expect(run(555)).toEqual(run(555)); + }); + + it("different seeds diverge", () => { + expect(run(555).cells.map((c) => (c.mine ? 1 : 0)).join("")).not.toBe( + run(777).cells.map((c) => (c.mine ? 1 : 0)).join("") + ); + }); +}); diff --git a/frontend/src/game/arcade/minesweeper.ts b/frontend/src/game/arcade/minesweeper.ts new file mode 100644 index 0000000..5d1d04a --- /dev/null +++ b/frontend/src/game/arcade/minesweeper.ts @@ -0,0 +1,344 @@ +/** + * Minesweeper — pure game rules. + * + * Plain values in, plain values out; no Phaser/net/DOM imports. Mine placement + * is drawn from `rngSeed` on the first reveal (the clicked cell and its + * neighbours are kept clear so the opening is always safe), so a seed + click + * script reproduces a run (see minesweeper.test.ts). + * + * The board is a flat row-major array. A move is a cell index. The timer is + * advanced by `minesTick` using an explicit `dt` so pausing the overlay freezes + * elapsed time. Scoring is higher-is-better: 10 points per safe reveal, a win + * bonus, and leftover time. + */ +import { nextInt } from "./prng"; + +export type MinesMark = "hidden" | "flag" | "revealed"; +export type MinesPhase = "ready" | "play" | "won" | "lost"; + +export type MinesEvent = "reveal" | "flag" | "unflag" | "chord" | "boom" | "win"; + +export interface MinesCell { + readonly mine: boolean; + readonly adjacent: number; + readonly mark: MinesMark; +} + +export interface MinesState { + readonly cols: number; + readonly rows: number; + readonly mineCount: number; + readonly cells: readonly MinesCell[]; + readonly placed: boolean; + readonly phase: MinesPhase; + readonly cursor: number; + readonly elapsedMs: number; + readonly score: number; + readonly boomIndex: number | null; + readonly rngSeed: number; +} + +export interface MinesStep { + readonly state: MinesState; + readonly events: readonly MinesEvent[]; +} + +export const MINES_COLS = 10; +export const MINES_ROWS = 10; +export const MINES_COUNT = 12; +export const MINES_CELL_COUNT = MINES_COLS * MINES_ROWS; +export const REVEAL_POINTS = 10; +export const WIN_BONUS = 1500; +export const TIME_BUDGET_SEC = 240; +export const TIME_POINTS_PER_SEC = 5; + +const EMPTY: MinesCell = { mine: false, adjacent: 0, mark: "hidden" }; + +function clampIndex(index: number, count: number): number { + if (!Number.isInteger(index) || count <= 0) return 0; + if (index < 0) return 0; + if (index >= count) return count - 1; + return index; +} + +export function minesIndex(col: number, row: number, cols: number = MINES_COLS): number { + return row * cols + col; +} + +export function minesCoord( + index: number, + cols: number = MINES_COLS +): { col: number; row: number } { + return { col: index % cols, row: Math.floor(index / cols) }; +} + +/** Neighbours in a stable scan order (top-left → bottom-right). */ +export function minesNeighbors( + index: number, + cols: number = MINES_COLS, + rows: number = MINES_ROWS +): number[] { + const { col, row } = minesCoord(index, cols); + const out: number[] = []; + for (let dr = -1; dr <= 1; dr += 1) { + for (let dc = -1; dc <= 1; dc += 1) { + if (dr === 0 && dc === 0) continue; + const r = row + dr; + const c = col + dc; + if (r < 0 || r >= rows || c < 0 || c >= cols) continue; + out.push(minesIndex(c, r, cols)); + } + } + return out; +} + +export function minesRemaining(state: MinesState): number { + let flags = 0; + for (const cell of state.cells) { + if (cell.mark === "flag") flags += 1; + } + return state.mineCount - flags; +} + +export function minesRevealedSafe(state: MinesState): number { + let n = 0; + for (const cell of state.cells) { + if (cell.mark === "revealed" && !cell.mine) n += 1; + } + return n; +} + +function scoreFor(state: Pick): number { + let revealed = 0; + for (const cell of state.cells) { + if (cell.mark === "revealed" && !cell.mine) revealed += 1; + } + const base = revealed * REVEAL_POINTS; + if (state.phase !== "won") return base; + const elapsedSec = Math.floor(state.elapsedMs / 1000); + const timePts = Math.max(0, TIME_BUDGET_SEC - elapsedSec) * TIME_POINTS_PER_SEC; + return base + WIN_BONUS + timePts; +} + +function blankBoard(count: number): MinesCell[] { + return Array.from({ length: count }, () => EMPTY); +} + +export function initMinesweeper(seed: number): MinesState { + const cells = blankBoard(MINES_CELL_COUNT); + return { + cols: MINES_COLS, + rows: MINES_ROWS, + mineCount: MINES_COUNT, + cells, + placed: false, + phase: "ready", + cursor: 0, + elapsedMs: 0, + score: 0, + boomIndex: null, + rngSeed: seed >>> 0 || 1, + }; +} + +function forbiddenSet(start: number, cols: number, rows: number): Set { + const set = new Set([start]); + for (const n of minesNeighbors(start, cols, rows)) set.add(n); + return set; +} + +function placeMines(state: MinesState, start: number): MinesState { + const forbidden = forbiddenSet(start, state.cols, state.rows); + const pool: number[] = []; + for (let i = 0; i < state.cells.length; i += 1) { + if (!forbidden.has(i)) pool.push(i); + } + let seed = state.rngSeed; + for (let i = pool.length - 1; i > 0; i -= 1) { + const draw = nextInt(seed, i + 1); + seed = draw.seed; + const j = draw.value; + const a = pool[i]; + const b = pool[j]; + if (a === undefined || b === undefined) continue; + pool[i] = b; + pool[j] = a; + } + const mineAt = new Set(pool.slice(0, state.mineCount)); + const cells: MinesCell[] = state.cells.map((cell, i) => ({ + mine: mineAt.has(i), + adjacent: 0, + mark: cell.mark, + })); + for (let i = 0; i < cells.length; i += 1) { + const cell = cells[i]; + if (!cell || cell.mine) continue; + let adj = 0; + for (const n of minesNeighbors(i, state.cols, state.rows)) { + if (mineAt.has(n)) adj += 1; + } + cells[i] = { ...cell, adjacent: adj }; + } + return { ...state, cells, placed: true, rngSeed: seed, phase: "play" }; +} + +function floodReveal(cells: readonly MinesCell[], start: number, cols: number, rows: number): MinesCell[] { + const next = cells.slice(); + const stack = [start]; + const seen = new Set(); + while (stack.length > 0) { + const i = stack.pop(); + if (i === undefined || seen.has(i)) continue; + seen.add(i); + const cell = next[i]; + if (!cell || cell.mark === "flag" || cell.mine) continue; + next[i] = { ...cell, mark: "revealed" }; + if (cell.adjacent !== 0) continue; + const neigh = minesNeighbors(i, cols, rows); + for (let k = neigh.length - 1; k >= 0; k -= 1) { + const n = neigh[k]; + if (n === undefined) continue; + const nc = next[n]; + if (nc && nc.mark === "hidden") stack.push(n); + } + } + return next; +} + +function allSafeRevealed(cells: readonly MinesCell[]): boolean { + for (const cell of cells) { + if (!cell.mine && cell.mark !== "revealed") return false; + } + return true; +} + +function finishWin(state: MinesState, cells: readonly MinesCell[]): MinesState { + const flagged = cells.map((cell) => + cell.mine && cell.mark !== "revealed" ? { ...cell, mark: "flag" as const } : cell + ); + const next: MinesState = { + ...state, + cells: flagged, + phase: "won", + score: 0, + boomIndex: null, + }; + return { ...next, score: scoreFor(next) }; +} + +function finishLoss(state: MinesState, cells: readonly MinesCell[], boomIndex: number): MinesState { + const exposed = cells.map((cell, i) => { + if (cell.mine) return { ...cell, mark: "revealed" as const }; + if (cell.mark === "flag") return cell; + return i === boomIndex ? { ...cell, mark: "revealed" as const } : cell; + }); + const next: MinesState = { + ...state, + cells: exposed, + phase: "lost", + boomIndex, + score: 0, + }; + return { ...next, score: scoreFor(next) }; +} + +function live(state: MinesState): boolean { + return state.phase === "ready" || state.phase === "play"; +} + +export function minesMoveCursor(state: MinesState, dir: "up" | "down" | "left" | "right"): MinesState { + if (!live(state)) return state; + const { col, row } = minesCoord(state.cursor, state.cols); + let c = col; + let r = row; + if (dir === "left") c -= 1; + else if (dir === "right") c += 1; + else if (dir === "up") r -= 1; + else r += 1; + if (c < 0 || c >= state.cols || r < 0 || r >= state.rows) return state; + return { ...state, cursor: minesIndex(c, r, state.cols) }; +} + +export function minesFlag(state: MinesState, index: number): MinesStep { + if (!live(state)) return { state, events: [] }; + const i = clampIndex(index, state.cells.length); + const cell = state.cells[i]; + if (!cell || cell.mark === "revealed") return { state, events: [] }; + const cells = state.cells.slice(); + const flagged = cell.mark !== "flag"; + cells[i] = { ...cell, mark: flagged ? "flag" : "hidden" }; + const next: MinesState = { ...state, cells, cursor: i, score: scoreFor({ ...state, cells }) }; + return { state: next, events: [flagged ? "flag" : "unflag"] }; +} + +export function minesReveal(state: MinesState, index: number): MinesStep { + if (!live(state)) return { state, events: [] }; + const i = clampIndex(index, state.cells.length); + const cell = state.cells[i]; + // Reject flagged/revealed cells BEFORE placing mines. A first-click on a + // flag must not consume the safe opening or start the clock. + if (!cell || cell.mark === "flag" || cell.mark === "revealed") { + return { state, events: [] }; + } + let current = { ...state, cursor: i }; + if (!current.placed) current = placeMines(current, i); + const placed = current.cells[i]; + if (!placed) return { state: current, events: [] }; + if (placed.mine) { + const lost = finishLoss(current, current.cells, i); + return { state: lost, events: ["boom"] }; + } + const cells = floodReveal(current.cells, i, current.cols, current.rows); + if (allSafeRevealed(cells)) { + const won = finishWin(current, cells); + return { state: won, events: ["reveal", "win"] }; + } + const next: MinesState = { + ...current, + cells, + phase: "play", + score: scoreFor({ ...current, cells, phase: "play" }), + }; + return { state: next, events: ["reveal"] }; +} + +/** + * Chord: on a revealed numbered cell whose neighbouring flags equal its + * adjacent count, reveal every remaining hidden neighbour. A misplaced flag + * detonates. + */ +export function minesChord(state: MinesState, index: number): MinesStep { + if (state.phase !== "play") return { state, events: [] }; + const i = clampIndex(index, state.cells.length); + const cell = state.cells[i]; + if (!cell || cell.mark !== "revealed" || cell.adjacent <= 0) { + return { state, events: [] }; + } + const neigh = minesNeighbors(i, state.cols, state.rows); + let flags = 0; + for (const n of neigh) { + const nc = state.cells[n]; + if (nc?.mark === "flag") flags += 1; + } + if (flags !== cell.adjacent) return { state, events: [] }; + + let current = { ...state, cursor: i }; + const events: MinesEvent[] = ["chord"]; + for (const n of neigh) { + const nc = current.cells[n]; + if (!nc || nc.mark !== "hidden") continue; + const step = minesReveal(current, n); + current = step.state; + for (const ev of step.events) { + if (ev !== "reveal") events.push(ev); + } + if (current.phase === "lost" || current.phase === "won") break; + } + return { state: current, events }; +} + +/** Advance the play clock. Frozen once the board is won or lost. */ +export function minesTick(state: MinesState, dtMs: number): MinesState { + if (state.phase !== "play") return state; + return { ...state, elapsedMs: state.elapsedMs + Math.max(0, dtMs) }; +} diff --git a/frontend/src/game/scenes/BootScene.ts b/frontend/src/game/scenes/BootScene.ts index 7677a6e..3ce5ced 100644 --- a/frontend/src/game/scenes/BootScene.ts +++ b/frontend/src/game/scenes/BootScene.ts @@ -47,6 +47,7 @@ export default class BootScene extends Phaser.Scene { const furniture = [ "chair", "chair_side", "table_round", "table_small", "desk", "plant_big", "plant_small", "sofa_small", "water", "clock", "arcade_snake", "arcade_flappy", "arcade_merge-drop", + "arcade_breakout", "arcade_minesweeper", // LimeZu interior props (scripts/gen_limezu_sprites.py). Catalog: docs/art/limezu-catalog.md // Only keys the map or WorldScene actually uses are preloaded; maps.test.ts guards both directions. "lz_ac", "lz_armchair", "lz_armchair_side", "lz_bench_gold", "lz_bin", "lz_board", "lz_bookshelf", diff --git a/frontend/src/ui/arcade/ArcadeOverlay.test.tsx b/frontend/src/ui/arcade/ArcadeOverlay.test.tsx index a4d233f..4c72913 100644 --- a/frontend/src/ui/arcade/ArcadeOverlay.test.tsx +++ b/frontend/src/ui/arcade/ArcadeOverlay.test.tsx @@ -167,6 +167,14 @@ describe("ArcadeOverlay pause menu", () => { expect(onClose).not.toHaveBeenCalled(); }); + it("ignores auto-repeated Escape so a held key cannot flicker pause/resume", () => { + render( {}} />); + fireEvent.keyDown(window, { key: "Escape" }); + expect(panelOpen("pause")).toBe(true); + fireEvent.keyDown(window, { key: "Escape", repeat: true }); + expect(panelOpen("pause")).toBe(true); + }); + it("arrow to Quit + Enter closes the overlay", () => { vi.useFakeTimers(); const onClose = vi.fn(); @@ -588,6 +596,34 @@ describe("ArcadeOverlay chrome", () => { }); }); +describe("ArcadeOverlay new cabinet registry", () => { + it("mounts the breakout cabinet from the shared registry", async () => { + net.fetchLeaderboard.mockResolvedValue({ + game: "breakout", + top: [{ username: "ada", score: 42 }], + best: 17, + }); + render( {}} />); + expect(await screen.findByText("Your best")).toBeTruthy(); + expect(net.fetchLeaderboard).toHaveBeenCalledWith("breakout"); + expect(screen.getByRole("img", { name: /Breakout/i })).toBeTruthy(); + expect(screen.getByLabelText("Breakout arcade")).toBeTruthy(); + }); + + it("mounts the minesweeper cabinet from the shared registry", async () => { + net.fetchLeaderboard.mockResolvedValue({ + game: "minesweeper", + top: [], + best: null, + }); + render( {}} />); + expect(await screen.findByText("Your best")).toBeTruthy(); + expect(net.fetchLeaderboard).toHaveBeenCalledWith("minesweeper"); + expect(screen.getByRole("grid", { name: "Minesweeper board" })).toBeTruthy(); + expect(screen.getByLabelText("Minesweeper arcade")).toBeTruthy(); + }); +}); + describe("ArcadeOverlay merge-drop registry", () => { // Arcade 2.0: the merge-drop cabinet is registered like any other game — the // overlay resolves it from the shared registry and its renderer mounts on a diff --git a/frontend/src/ui/arcade/ArcadeOverlay.tsx b/frontend/src/ui/arcade/ArcadeOverlay.tsx index e59269b..3d041bf 100644 --- a/frontend/src/ui/arcade/ArcadeOverlay.tsx +++ b/frontend/src/ui/arcade/ArcadeOverlay.tsx @@ -16,6 +16,8 @@ import { useReducedMotion } from "../reducedMotionBridge"; import SnakeGame from "./SnakeGame"; import FlappyGame from "./FlappyGame"; import MergeDropGame from "./MergeDropGame"; +import BreakoutGame from "./BreakoutGame"; +import MinesweeperGame from "./MinesweeperGame"; import { terminalHoldMs } from "./terminalHold"; import { exitFullscreen, @@ -31,6 +33,8 @@ const GAMES: Record> = { snake: SnakeGame, flappy: FlappyGame, "merge-drop": MergeDropGame, + breakout: BreakoutGame, + minesweeper: MinesweeperGame, }; /** Fade-out budget before the parent unmounts us (matches the CSS transition). */ @@ -159,6 +163,7 @@ export default function ArcadeOverlay({ game, label, onClose }: ArcadeOverlayPro useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key !== "m" && e.key !== "M") return; + if (e.repeat) return; e.preventDefault(); setSettings({ muteArcade: !getSettings().muteArcade }); }; @@ -227,6 +232,7 @@ export default function ArcadeOverlay({ game, label, onClose }: ArcadeOverlayPro useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key !== "f" && e.key !== "F") return; + if (e.repeat) return; e.preventDefault(); toggleFullscreen(); }; @@ -398,6 +404,8 @@ export default function ArcadeOverlay({ game, label, onClose }: ArcadeOverlayPro e.stopPropagation(); }; if (phase.k === "closing") return; + // Auto-repeat would flicker pause/resume and retrigger Play again. + if (e.repeat && (e.key === "Escape" || e.key === "Enter" || e.key === " ")) return; if (e.key === "Escape") { swallow(); diff --git a/frontend/src/ui/arcade/BreakoutGame.test.tsx b/frontend/src/ui/arcade/BreakoutGame.test.tsx new file mode 100644 index 0000000..c78a7ff --- /dev/null +++ b/frontend/src/ui/arcade/BreakoutGame.test.tsx @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; + +vi.mock("../../game/arcade/breakout", async (importOriginal) => { + const mod = await importOriginal(); + return { ...mod, breakoutTick: vi.fn(mod.breakoutTick) }; +}); + +import BreakoutGame from "./BreakoutGame"; +import { breakoutTick } from "../../game/arcade/breakout"; + +const tickSpy = vi.mocked(breakoutTick); + +function launchesSince(start: number): boolean[] { + return tickSpy.mock.calls.slice(start).map((call) => { + const input = call[1]; + if (!input) throw new Error("breakoutTick called without input"); + return input.launch; + }); +} + +describe("BreakoutGame", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); + }); + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + vi.useRealTimers(); + }); + + it("exposes an accessible name covering mouse and keyboard serve", () => { + render( + {}} + onGameOver={() => {}} + />, + ); + expect( + screen.getByRole("img", { name: /Breakout.*Space to serve/i }), + ).toBeTruthy(); + }); + + it("does not throw when serving from the keyboard while the canvas has no context", () => { + render( + {}} + onGameOver={() => {}} + />, + ); + expect(() => { + fireEvent.keyDown(window, { key: " " }); + fireEvent.keyDown(window, { key: "ArrowLeft" }); + }).not.toThrow(); + expect(screen.getByRole("img", { name: /Breakout/i })).toBeTruthy(); + }); + + it("serves on Space keydown only, never on keyup", () => { + render( + {}} + onGameOver={() => {}} + />, + ); + fireEvent.keyDown(window, { key: " " }); + act(() => { + vi.advanceTimersByTime(200); + }); + const afterDown = launchesSince(0).filter(Boolean).length; + expect(afterDown).toBeGreaterThan(0); + const start = tickSpy.mock.calls.length; + fireEvent.keyUp(window, { key: " " }); + act(() => { + vi.advanceTimersByTime(200); + }); + expect(launchesSince(start).filter(Boolean)).toEqual([]); + }); +}); diff --git a/frontend/src/ui/arcade/BreakoutGame.tsx b/frontend/src/ui/arcade/BreakoutGame.tsx new file mode 100644 index 0000000..f342e4a --- /dev/null +++ b/frontend/src/ui/arcade/BreakoutGame.tsx @@ -0,0 +1,286 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { bus } from "../../game/eventBus"; +import { + BREAKOUT_HEIGHT, + BREAKOUT_STEP, + BREAKOUT_WIDTH, + breakoutTick, + initBreakout, + type BreakoutEvent, + type BreakoutInput, + type BreakoutState, +} from "../../game/arcade/breakout"; +import { + burst, + initJuice, + popup, + shake, + stepJuice, + type JuiceState, +} from "../../game/arcade/juice"; +import { useReducedMotion } from "../reducedMotionBridge"; +import { renderBreakout } from "./breakout/render"; +import type { ArcadeGameProps } from "./gameTypes"; + +const SCALE = 2; +const MAX_DPR = 2; +const MAX_FRAME_SECONDS = 0.25; +const MAX_STEPS_PER_FRAME = 60; + +/** Portrait cabinet: taller court so the 640-wide field fills the surface. */ +function portraitCourtHeight(cssWidth: number, cssHeight: number): number { + if (!(cssWidth > 0) || cssHeight <= cssWidth) return BREAKOUT_HEIGHT; + return Math.max(BREAKOUT_HEIGHT, Math.round(BREAKOUT_WIDTH * (cssHeight / cssWidth))); +} + +const BUS_FOR_EVENT: Readonly>> = + { + launch: "arcade-flap", + brick: "arcade-point", + paddle: "arcade-hit", + wall: "arcade-hit", + life: "arcade-hit", + combo: "arcade-bonus", + level: "arcade-bonus", + over: "arcade-over", + }; + +/** + * Thin canvas renderer for the pure Breakout module. Rules stay in + * game/arcade/breakout — this drives the fixed-step loop, paddle input, and + * domain events on the bus. + */ +export default function BreakoutGame({ + seed, + paused, + shake: shakePref, + onScore, + onGameOver, +}: ArcadeGameProps) { + const canvasRef = useRef(null); + const stateRef = useRef(initBreakout(seed)); + const juiceRef = useRef(initJuice(seed)); + const overRef = useRef(false); + const pausedRef = useRef(paused); + const keysRef = useRef({ left: false, right: false }); + const aimRef = useRef(null); + const launchRef = useRef(false); + const reducedMotion = useReducedMotion(); + const shakeEnabled = shakePref && !reducedMotion; + const [live, setLive] = useState("Ready. Click or press Space to serve. 3 lives left."); + + useEffect(() => { + pausedRef.current = paused; + }, [paused]); + + const draw = useCallback(() => { + const ctx = canvasRef.current?.getContext("2d"); + if (!ctx) return; + const state = stateRef.current; + const scaleX = ctx.canvas.width / state.width; + const scaleY = ctx.canvas.height / state.height; + if (!(scaleX > 0) || !(scaleY > 0)) return; + renderBreakout(ctx, state, juiceRef.current, scaleX, scaleY, shakeEnabled); + }, [shakeEnabled]); + + useEffect(() => { + draw(); + }, [draw]); + + const fit = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const boxW = canvas.clientWidth; + const boxH = canvas.clientHeight; + if (boxW < 1 || boxH < 1) return; + + const portrait = boxH > boxW; + if (!portrait) { + const nextW = BREAKOUT_WIDTH * SCALE; + const nextH = BREAKOUT_HEIGHT * SCALE; + if (canvas.width !== nextW || canvas.height !== nextH) { + canvas.width = nextW; + canvas.height = nextH; + } + if (stateRef.current.height !== BREAKOUT_HEIGHT && stateRef.current.phase === "ready") { + stateRef.current = initBreakout(seed); + } + draw(); + return; + } + + const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR); + const pxW = Math.max(1, Math.round(boxW * dpr)); + const pxH = Math.max(1, Math.round(boxH * dpr)); + if (canvas.width !== pxW || canvas.height !== pxH) { + canvas.width = pxW; + canvas.height = pxH; + } + const worldH = portraitCourtHeight(boxW, boxH); + if (stateRef.current.height !== worldH && stateRef.current.phase === "ready") { + stateRef.current = initBreakout(seed, worldH); + } + draw(); + }, [draw, seed]); + + useEffect(() => { + fit(); + if (typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver(fit); + const canvas = canvasRef.current; + if (canvas) observer.observe(canvas); + return () => observer.disconnect(); + }, [fit]); + + const sampleInput = useCallback((): BreakoutInput => { + const move: -1 | 0 | 1 = keysRef.current.left === keysRef.current.right + ? 0 + : keysRef.current.left + ? -1 + : 1; + const launch = launchRef.current; + launchRef.current = false; + return { aim: aimRef.current, move, launch }; + }, []); + + const applyEvents = useCallback( + (prev: BreakoutState, next: BreakoutState, events: readonly BreakoutEvent[]) => { + if (next.score !== prev.score) onScore(next.score); + let juice = juiceRef.current; + for (const ev of events) { + const busName = BUS_FOR_EVENT[ev]; + if (busName) bus.emit(busName); + if (ev === "brick") { + juice = burst(juice, { + x: next.ballX, + y: next.ballY, + count: reducedMotion ? 0 : 10, + color: "#f2c14e", + speed: 3.2, + life: 420, + size: 2.2, + }); + juice = popup(juice, { + x: next.ballX, + y: next.ballY - 8, + text: `+${next.score - prev.score}`, + color: "#7fd1b9", + }); + if (shakeEnabled) juice = shake(juice, 0.18); + } + if ((ev === "life" || ev === "over") && shakeEnabled) juice = shake(juice, 0.55); + } + juiceRef.current = juice; + if (next.phase !== prev.phase || next.lives !== prev.lives) { + if (next.phase === "ready") { + setLive(`Ready. Click or press Space to serve. ${next.lives} lives left.`); + } else if (next.phase === "over") { + setLive(`Game over. Final score ${next.score}.`); + } else if (next.phase === "play" && prev.phase === "ready") { + setLive(`Playing. ${next.lives} lives left.`); + } + } + if (next.phase === "over" && !overRef.current) { + overRef.current = true; + onGameOver(next.score); + } + }, + [onScore, onGameOver, reducedMotion, shakeEnabled] + ); + + const advance = useCallback(() => { + const prev = stateRef.current; + const step = breakoutTick(prev, sampleInput()); + stateRef.current = step.state; + juiceRef.current = stepJuice(juiceRef.current, BREAKOUT_STEP * 1000); + applyEvents(prev, step.state, step.events); + }, [sampleInput, applyEvents]); + + useEffect(() => { + if (paused) return; + let raf = 0; + let last = 0; + let acc = 0; + const frame = (now: number) => { + raf = requestAnimationFrame(frame); + if (!last) last = now; + acc += Math.min(MAX_FRAME_SECONDS, (now - last) / 1000); + last = now; + let steps = 0; + while (acc >= BREAKOUT_STEP && steps < MAX_STEPS_PER_FRAME) { + advance(); + acc -= BREAKOUT_STEP; + steps += 1; + } + if (steps >= MAX_STEPS_PER_FRAME) acc = 0; + draw(); + }; + raf = requestAnimationFrame(frame); + return () => cancelAnimationFrame(raf); + }, [paused, draw, advance]); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + if (!e.repeat && !pausedRef.current) launchRef.current = true; + return; + } + if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") { + e.preventDefault(); + keysRef.current.left = true; + } + if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") { + e.preventDefault(); + keysRef.current.right = true; + } + }; + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") { + keysRef.current.left = false; + } + if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") { + keysRef.current.right = false; + } + }; + window.addEventListener("keydown", onKeyDown); + window.addEventListener("keyup", onKeyUp); + return () => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("keyup", onKeyUp); + }; + }, []); + + const aimFromClient = useCallback((clientX: number): number | null => { + const canvas = canvasRef.current; + if (!canvas) return null; + const rect = canvas.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return null; + const worldW = stateRef.current.width; + const worldH = stateRef.current.height; + const scale = Math.min(rect.width / worldW, rect.height / worldH); + const left = rect.left + (rect.width - worldW * scale) / 2; + return (clientX - left) / scale; + }, []); + + return ( + { + aimRef.current = aimFromClient(e.clientX); + }} + onPointerLeave={() => { + aimRef.current = null; + }} + onPointerDown={(e) => { + aimRef.current = aimFromClient(e.clientX); + if (!pausedRef.current) launchRef.current = true; + }} + /> + ); +} diff --git a/frontend/src/ui/arcade/FlappyGame.test.tsx b/frontend/src/ui/arcade/FlappyGame.test.tsx new file mode 100644 index 0000000..4092908 --- /dev/null +++ b/frontend/src/ui/arcade/FlappyGame.test.tsx @@ -0,0 +1,28 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import FlappyGame from "./FlappyGame"; + +describe("FlappyGame accessibility", () => { + beforeEach(() => { + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); + }); + + afterEach(() => { + cleanup(); + }); + + it("names the canvas with click and keyboard flap controls", () => { + render( + {}} + onGameOver={() => {}} + />, + ); + expect( + screen.getByRole("img", { name: /Flappy.*Click or press Space/i }), + ).toBeTruthy(); + }); +}); diff --git a/frontend/src/ui/arcade/FlappyGame.tsx b/frontend/src/ui/arcade/FlappyGame.tsx index 329d062..a1812fc 100644 --- a/frontend/src/ui/arcade/FlappyGame.tsx +++ b/frontend/src/ui/arcade/FlappyGame.tsx @@ -198,6 +198,8 @@ export default function FlappyGame({ ); diff --git a/frontend/src/ui/arcade/MergeDropGame.tsx b/frontend/src/ui/arcade/MergeDropGame.tsx index 97a3d25..4290a15 100644 --- a/frontend/src/ui/arcade/MergeDropGame.tsx +++ b/frontend/src/ui/arcade/MergeDropGame.tsx @@ -840,6 +840,8 @@ export default function MergeDropGame({ seed, paused, shake, onScore, onGameOver className="arcade-canvas" width={W * S} height={H * S} + role="img" + aria-label="Stellar Forge. Move with the mouse or A and D. Click or press Space to drop." onPointerMove={onPointerMove} onPointerDown={onPointerDown} /> diff --git a/frontend/src/ui/arcade/MinesweeperGame.test.tsx b/frontend/src/ui/arcade/MinesweeperGame.test.tsx new file mode 100644 index 0000000..4a887b8 --- /dev/null +++ b/frontend/src/ui/arcade/MinesweeperGame.test.tsx @@ -0,0 +1,107 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; +import MinesweeperGame from "./MinesweeperGame"; +import { initMinesweeper, minesReveal } from "../../game/arcade/minesweeper"; + +function mount(props?: { paused?: boolean; onGameOver?: (n: number) => void }) { + return render( + {}} + onGameOver={props?.onGameOver ?? (() => {})} + />, + ); +} + +function cell(row: number, col: number, suffix = ","): HTMLElement { + return screen.getByRole("gridcell", { name: new RegExp(`^Row ${row} column ${col}${suffix}`) }); +} + +describe("MinesweeperGame", () => { + afterEach(() => { + cleanup(); + vi.useRealTimers(); + }); + + it("renders a labelled 10×10 grid with start instructions", () => { + mount(); + expect(screen.getByRole("grid", { name: "Minesweeper board" })).toBeTruthy(); + expect(screen.getAllByRole("gridcell")).toHaveLength(100); + expect(screen.getByText(/Arrows or WASD move/)).toBeTruthy(); + expect(screen.getByLabelText("12 mines remaining")).toBeTruthy(); + }); + + it("opens the first cell on click and keeps the opening safe", () => { + mount(); + fireEvent.click(cell(1, 1)); + const opened = screen.getAllByRole("gridcell").filter( + (el) => el.getAttribute("aria-pressed") === "true", + ); + expect(opened.length).toBeGreaterThan(1); + expect(screen.queryByRole("gridcell", { name: /exploded mine/ })).toBeNull(); + }); + + it("flags a hidden cell on right-click", () => { + mount(); + fireEvent.contextMenu(cell(1, 2, ", hidden$")); + expect(screen.getByRole("gridcell", { name: /^Row 1 column 2, flagged$/ })).toBeTruthy(); + expect(screen.getByLabelText("11 mines remaining")).toBeTruthy(); + }); + + it("does not let a timer tick overwrite a reveal", () => { + vi.useFakeTimers(); + mount(); + act(() => { + cell(1, 1).dispatchEvent(new MouseEvent("click", { bubbles: true })); + vi.advanceTimersByTime(400); + }); + const opened = screen.getAllByRole("gridcell").filter( + (el) => el.getAttribute("aria-pressed") === "true", + ); + expect(opened.length).toBeGreaterThan(1); + expect(screen.getByText("Sweep")).toBeTruthy(); + }); + + it("applies Arrow then Space to the new cursor cell in one turn", () => { + mount(); + act(() => { + window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })); + window.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true })); + }); + expect(screen.getByRole("gridcell", { name: /^Row 1 column 2,/ }).getAttribute("aria-pressed")).toBe( + "true", + ); + expect(screen.getByRole("gridcell", { name: /^Row 1 column 2,/ }).getAttribute("aria-current")).toBe( + "true", + ); + }); + + it("freezes input after game-over so a finishing overlay cannot keep playing", () => { + const onGameOver = vi.fn(); + render( + {}} + onGameOver={onGameOver} + />, + ); + fireEvent.click(cell(1, 1)); + const opened = minesReveal(initMinesweeper(4), 0).state; + const mine = opened.cells.findIndex((c) => c.mine && c.mark === "hidden"); + expect(mine).toBeGreaterThanOrEqual(0); + const row = Math.floor(mine / opened.cols) + 1; + const col = (mine % opened.cols) + 1; + fireEvent.click(screen.getByRole("gridcell", { name: new RegExp(`^Row ${row} column ${col},`) })); + expect(onGameOver).toHaveBeenCalledTimes(1); + expect(screen.getByText("Boom")).toBeTruthy(); + fireEvent.keyDown(window, { key: "ArrowRight" }); + fireEvent.keyDown(window, { key: "x" }); + fireEvent.keyDown(window, { key: " " }); + expect(onGameOver).toHaveBeenCalledTimes(1); + expect(screen.getAllByRole("gridcell").every((el) => (el as HTMLButtonElement).disabled)).toBe(true); + }); +}); diff --git a/frontend/src/ui/arcade/MinesweeperGame.tsx b/frontend/src/ui/arcade/MinesweeperGame.tsx new file mode 100644 index 0000000..24b2210 --- /dev/null +++ b/frontend/src/ui/arcade/MinesweeperGame.tsx @@ -0,0 +1,239 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { bus } from "../../game/eventBus"; +import { + initMinesweeper, + minesChord, + minesFlag, + minesMoveCursor, + minesRemaining, + minesReveal, + minesTick, + type MinesEvent, + type MinesState, +} from "../../game/arcade/minesweeper"; +import { useReducedMotion } from "../reducedMotionBridge"; +import type { ArcadeGameProps } from "./gameTypes"; + +const TICK_MS = 100; + +const BUS_FOR_EVENT: Readonly>> = + { + reveal: "arcade-point", + flag: "arcade-eat", + unflag: "arcade-eat", + chord: "arcade-point", + boom: "arcade-hit", + win: "arcade-bonus", + }; + +function formatClock(ms: number): string { + const sec = Math.min(999, Math.floor(ms / 1000)); + return String(sec).padStart(3, "0"); +} + +function cellLabel(state: MinesState, index: number): string { + const cell = state.cells[index]; + const col = (index % state.cols) + 1; + const row = Math.floor(index / state.cols) + 1; + const at = `Row ${row} column ${col}`; + if (!cell) return at; + if (state.phase === "lost" && index === state.boomIndex) return `${at}, exploded mine`; + if (cell.mark === "flag") return `${at}, flagged`; + if (cell.mark === "hidden") return `${at}, hidden`; + if (cell.mine) return `${at}, mine`; + if (cell.adjacent === 0) return `${at}, empty`; + return `${at}, ${cell.adjacent} adjacent ${cell.adjacent === 1 ? "mine" : "mines"}`; +} + +function asStep(state: MinesState): { state: MinesState; events: readonly MinesEvent[] } { + return { state, events: [] }; +} + +/** + * DOM renderer for the pure Minesweeper module. The grid is real buttons so + * keyboard, pointer, and screen-reader users share one control set. Rules stay + * in game/arcade/minesweeper. + * + * Every transition goes through {@link dispatch}: the ref is the live board and + * is written before `setState`, so a 100ms tick cannot replay a stale snapshot + * over a reveal/win, and Arrow-then-Space sees the new cursor. + */ +export default function MinesweeperGame({ + seed, + paused, + onScore, + onGameOver, +}: ArcadeGameProps) { + const [state, setState] = useState(() => initMinesweeper(seed)); + const stateRef = useRef(state); + const overRef = useRef(false); + const boardRef = useRef(null); + const reducedMotion = useReducedMotion(); + + const dispatch = useCallback((reduce: (prev: MinesState) => { state: MinesState; events: readonly MinesEvent[] }) => { + if (paused || overRef.current) return; + const prev = stateRef.current; + const step = reduce(prev); + if (step.state === prev && step.events.length === 0) return; + stateRef.current = step.state; + setState(step.state); + if (step.state.score !== prev.score) onScore(step.state.score); + for (const ev of step.events) { + const busName = BUS_FOR_EVENT[ev]; + if (busName) bus.emit(busName); + } + if (step.state.phase === "won" || step.state.phase === "lost") { + overRef.current = true; + if (step.state.phase === "lost") bus.emit("arcade-over"); + onGameOver(step.state.score); + } + }, [paused, onScore, onGameOver]); + + const focusCursor = useCallback((index: number) => { + const el = boardRef.current?.querySelector(`[data-idx="${index}"]`); + if (el instanceof HTMLButtonElement) el.focus(); + }, []); + + useEffect(() => { + if (paused) return; + const id = window.setInterval(() => { + dispatch((s) => { + if (s.phase !== "play") return asStep(s); + return asStep(minesTick(s, TICK_MS)); + }); + }, TICK_MS); + return () => window.clearInterval(id); + }, [paused, dispatch]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (paused || overRef.current) return; + const move = + e.key === "ArrowUp" || e.key === "w" || e.key === "W" + ? "up" + : e.key === "ArrowDown" || e.key === "s" || e.key === "S" + ? "down" + : e.key === "ArrowLeft" || e.key === "a" || e.key === "A" + ? "left" + : e.key === "ArrowRight" || e.key === "d" || e.key === "D" + ? "right" + : null; + if (move) { + e.preventDefault(); + dispatch((s) => asStep(minesMoveCursor(s, move))); + focusCursor(stateRef.current.cursor); + return; + } + if (e.key === "x" || e.key === "X") { + e.preventDefault(); + dispatch((s) => minesFlag(s, s.cursor)); + return; + } + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + if (e.repeat) return; + dispatch((s) => + e.shiftKey ? minesChord(s, s.cursor) : minesReveal(s, s.cursor) + ); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [paused, dispatch, focusCursor]); + + const remaining = minesRemaining(state); + const status = + state.phase === "won" + ? "Cleared" + : state.phase === "lost" + ? "Boom" + : state.placed + ? "Sweep" + : "Pick an opening"; + const over = state.phase === "won" || state.phase === "lost"; + + return ( +
+
+
+ Mines + {String(remaining).padStart(2, "0")} +
+

+ {status} +

+
+ Time + {formatClock(state.elapsedMs)} +
+
+ +
+ {state.cells.map((cell, i) => { + const pressed = cell.mark !== "hidden"; + const classes = [ + "arcade-mines-cell", + `is-${cell.mark}`, + cell.mark === "revealed" && cell.mine ? "is-mine" : "", + i === state.boomIndex ? "is-boom" : "", + i === state.cursor ? "is-cursor" : "", + cell.mark === "revealed" && !cell.mine && cell.adjacent > 0 ? `is-n${cell.adjacent}` : "", + ] + .filter(Boolean) + .join(" "); + return ( + + ); + })} +
+ +

+ Arrows or WASD move · Click or Space opens · Right-click or X flags · + Shift+click chords +

+
+ ); +} diff --git a/frontend/src/ui/arcade/SnakeGame.tsx b/frontend/src/ui/arcade/SnakeGame.tsx index 74ec20e..3c81e1a 100644 --- a/frontend/src/ui/arcade/SnakeGame.tsx +++ b/frontend/src/ui/arcade/SnakeGame.tsx @@ -478,6 +478,8 @@ export default function SnakeGame({ { + it("keeps the 11px design size on a desktop-width canvas", () => { + expect(breakoutHudFontWorld(BREAKOUT_WIDTH, 900)).toBe(11); + expect(breakoutHudFontWorld(BREAKOUT_WIDTH, 0)).toBe(11); + }); + + it("grows so HUD paints at least 12 CSS px on a 390-wide cabinet", () => { + const world = breakoutHudFontWorld(BREAKOUT_WIDTH, 390); + expect(world * (390 / BREAKOUT_WIDTH)).toBeGreaterThanOrEqual(12); + expect(world).toBeGreaterThan(11); + }); +}); diff --git a/frontend/src/ui/arcade/breakout/render.ts b/frontend/src/ui/arcade/breakout/render.ts new file mode 100644 index 0000000..3a44f1c --- /dev/null +++ b/frontend/src/ui/arcade/breakout/render.ts @@ -0,0 +1,149 @@ +/** + * Breakout drawing — presentation only. The reducer owns every rule. + */ +import type { BreakoutState } from "../../../game/arcade/breakout"; +import { fadeAlpha, shakeOffset, type JuiceState } from "../../../game/arcade/juice"; + +const ROW_COLORS = ["#e2564d", "#f2c14e", "#7fd1b9", "#2196f3", "#b082ff", "#9aa5c0"] as const; +const HUD_FONT_WORLD = 11; +const HUD_MIN_CSS_PX = 12; + +/** + * World-space HUD font that paints at least `minCssPx` on the displayed canvas. + * Desktop courts (client width ≳ 700) keep the 11px design size. + */ +export function breakoutHudFontWorld( + worldWidth: number, + clientWidth: number, + baseWorld = HUD_FONT_WORLD, + minCssPx = HUD_MIN_CSS_PX +): number { + if (!(clientWidth > 0) || !(worldWidth > 0)) return baseWorld; + return Math.max(baseWorld, minCssPx / (clientWidth / worldWidth)); +} + +export function renderBreakout( + ctx: CanvasRenderingContext2D, + state: BreakoutState, + juice: JuiceState, + scaleX: number, + scaleY: number, + shakeEnabled: boolean +): void { + const w = state.width; + const h = state.height; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0); + + ctx.fillStyle = "#0b1020"; + ctx.fillRect(0, 0, w, h); + const glow = ctx.createRadialGradient(w * 0.5, 0, 20, w * 0.5, 0, h * 0.8); + glow.addColorStop(0, "rgba(127, 209, 185, 0.10)"); + glow.addColorStop(1, "rgba(0, 0, 0, 0)"); + ctx.fillStyle = glow; + ctx.fillRect(0, 0, w, h); + + const shake = shakeEnabled ? shakeOffset(juice) : { x: 0, y: 0 }; + ctx.save(); + ctx.translate(shake.x, shake.y); + + ctx.strokeStyle = "rgba(58, 71, 103, 0.9)"; + ctx.lineWidth = 2; + ctx.strokeRect(8, 8, w - 16, h - 16); + + for (const brick of state.bricks) { + if (brick.hp <= 0) continue; + const color = ROW_COLORS[brick.row] ?? "#9aa5c0"; + ctx.fillStyle = color; + ctx.globalAlpha = brick.hp > 1 ? 1 : 0.82; + roundRect(ctx, brick.x, brick.y, brick.w, brick.h, 3); + ctx.fill(); + ctx.globalAlpha = 0.35; + ctx.fillStyle = "#ffffff"; + roundRect(ctx, brick.x + 2, brick.y + 1, brick.w - 4, 3, 1); + ctx.fill(); + ctx.globalAlpha = 1; + } + + const px = state.paddleX - state.paddleW / 2; + ctx.fillStyle = "#e8ecf5"; + roundRect(ctx, px, state.paddleY, state.paddleW, state.paddleH, 4); + ctx.fill(); + ctx.fillStyle = "#7fd1b9"; + ctx.fillRect(px + 8, state.paddleY + 2, state.paddleW - 16, 3); + + ctx.beginPath(); + ctx.arc(state.ballX, state.ballY, state.ballR, 0, Math.PI * 2); + ctx.fillStyle = "#f2c14e"; + ctx.fill(); + ctx.fillStyle = "#fff4c8"; + ctx.beginPath(); + ctx.arc(state.ballX - 1.4, state.ballY - 1.4, state.ballR * 0.35, 0, Math.PI * 2); + ctx.fill(); + + for (const p of juice.particles) { + ctx.globalAlpha = fadeAlpha(p.life, p.maxLife); + ctx.fillStyle = p.color; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.globalAlpha = 1; + const hudFont = breakoutHudFontWorld(w, ctx.canvas.clientWidth); + const hudK = hudFont / HUD_FONT_WORLD; + ctx.font = `700 ${12 * hudK}px system-ui, sans-serif`; + ctx.textAlign = "center"; + for (const u of juice.popups) { + ctx.globalAlpha = fadeAlpha(u.life, u.maxLife); + ctx.fillStyle = u.color; + ctx.fillText(u.text, u.x, u.y); + } + ctx.globalAlpha = 1; + + // HUD in uniform X-scale so a portrait Y-stretch does not squash glyphs. + const yAdj = scaleX === 0 ? 1 : scaleY / scaleX; + ctx.setTransform(scaleX, 0, 0, scaleX, 0, 0); + ctx.fillStyle = "#9aa5c0"; + ctx.font = `700 ${hudFont}px system-ui, sans-serif`; + ctx.textAlign = "left"; + ctx.fillText(`Lv ${state.level}`, 16, 24 * yAdj); + for (let i = 0; i < state.lives; i += 1) { + ctx.beginPath(); + ctx.arc(w - 20 - i * 14 * hudK, 20 * yAdj, 4.5 * hudK, 0, Math.PI * 2); + ctx.fillStyle = "#e2564d"; + ctx.fill(); + } + + if (state.phase === "ready") { + ctx.textAlign = "center"; + ctx.fillStyle = "rgba(11, 16, 32, 0.45)"; + ctx.fillRect(0, h * 0.52 * yAdj, w, 64 * hudK); + ctx.fillStyle = "#e8ecf5"; + ctx.font = `800 ${28 * hudK}px system-ui, sans-serif`; + ctx.fillText("BREAKOUT", w / 2, h * 0.58 * yAdj); + ctx.fillStyle = "#7fd1b9"; + ctx.font = `600 ${13 * hudK}px system-ui, sans-serif`; + ctx.fillText("Click or Space to serve · A/D or mouse to aim", w / 2, h * 0.58 * yAdj + 22 * hudK); + } + + ctx.restore(); +} + +function roundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + r: number +): void { + const radius = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + w, y, x + w, y + h, radius); + ctx.arcTo(x + w, y + h, x, y + h, radius); + ctx.arcTo(x, y + h, x, y, radius); + ctx.arcTo(x, y, x + w, y, radius); + ctx.closePath(); +} diff --git a/frontend/src/ui/arcade/flappy/hud.ts b/frontend/src/ui/arcade/flappy/hud.ts index b381d39..fe548af 100644 --- a/frontend/src/ui/arcade/flappy/hud.ts +++ b/frontend/src/ui/arcade/flappy/hud.ts @@ -27,15 +27,14 @@ export function drawReadyScreen(ctx: CanvasRenderingContext2D, state: FlappyStat ctx.shadowBlur = 14; ctx.shadowOffsetY = 6; outlinedText(ctx, "FLAPPY", cx, titleY, titleSize, "#ffe14d"); - outlinedText(ctx, "BIRD", cx, titleY + titleSize * 0.92, titleSize); ctx.restore(); - // Pulsing call to action. + // Pulsing call to action. Click and Space both serve (canvas pointer + keys). const promptSize = Math.min(40, state.width * 0.062); const pulse = 0.72 + Math.sin(state.time * 3.4) * 0.28; ctx.save(); ctx.globalAlpha = pulse; - outlinedText(ctx, "TAP TO START", cx, state.groundY * 0.74, promptSize); + outlinedText(ctx, "CLICK OR SPACE", cx, state.groundY * 0.74, promptSize); ctx.restore(); // The controls line and the pointing hand are deliberately absent: the diff --git a/frontend/src/ui/arcade/terminalHold.ts b/frontend/src/ui/arcade/terminalHold.ts index 5cac518..98f83b0 100644 --- a/frontend/src/ui/arcade/terminalHold.ts +++ b/frontend/src/ui/arcade/terminalHold.ts @@ -5,6 +5,8 @@ export const TERMINAL_HOLD_MS: Readonly> = { snake: 830, flappy: 450, "merge-drop": 450, + breakout: 450, + minesweeper: 280, }; /** Reduced motion removes decorative terminal effects and their delay. */ diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 3bc0833..2514e26 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -27,5 +27,5 @@ "noFallthroughCasesInSwitch": true }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/vitest.setup.ts"] } diff --git a/shared/src/constants.test.ts b/shared/src/constants.test.ts index aab5195..71988e1 100644 --- a/shared/src/constants.test.ts +++ b/shared/src/constants.test.ts @@ -71,5 +71,7 @@ describe("arcade score namespaces", () => { expect(scoreGameForArcadeGame("snake")).toBe("snake-v2"); expect(scoreGameForArcadeGame("flappy")).toBe("flappy"); expect(scoreGameForArcadeGame("merge-drop")).toBe("merge-drop"); + expect(scoreGameForArcadeGame("breakout")).toBe("breakout"); + expect(scoreGameForArcadeGame("minesweeper")).toBe("minesweeper"); }); }); diff --git a/shared/src/constants.ts b/shared/src/constants.ts index da745dd..1ccfa9a 100644 --- a/shared/src/constants.ts +++ b/shared/src/constants.ts @@ -25,7 +25,7 @@ export const GEOMETRY_MANIFEST_VERSION = 1; * submitted `game` against this tuple, and the frontend keys its cabinets and * leaderboards off the same list — single source of truth for both sides. */ -export const ARCADE_GAMES = ["snake", "flappy", "merge-drop"] as const; +export const ARCADE_GAMES = ["snake", "flappy", "merge-drop", "breakout", "minesweeper"] as const; export type ArcadeGame = (typeof ARCADE_GAMES)[number]; /** @@ -33,13 +33,22 @@ export type ArcadeGame = (typeof ARCADE_GAMES)[number]; * `snake` rows so independent frontend/backend deployments cannot mix the * restored cabinet's 10-point scale with historical 1-point scores. */ -export const ARCADE_SCORE_GAMES = ["snake", "snake-v2", "flappy", "merge-drop"] as const; +export const ARCADE_SCORE_GAMES = [ + "snake", + "snake-v2", + "flappy", + "merge-drop", + "breakout", + "minesweeper", +] as const; export type ArcadeScoreGame = (typeof ARCADE_SCORE_GAMES)[number]; export const SCORE_GAME_BY_ARCADE_GAME: Readonly> = { snake: "snake-v2", flappy: "flappy", "merge-drop": "merge-drop", + breakout: "breakout", + minesweeper: "minesweeper", }; /** Resolve the current score namespace for a cabinet without changing its map id. */