From 40b007e6d9646206f79162b6ad38c69b260c8e4c Mon Sep 17 00:00:00 2001 From: Aashutosh Rathi Date: Thu, 1 Aug 2024 13:20:54 +0530 Subject: [PATCH] rm initialmode, fetch highscores from mru, fix sending ticks (#12) * rm: initialmode, fetch highscores from server and more * fix some race conditions * cleanup * fix: prevent actions from from popping up post sign cancel * add workflow for deploying --- .github/workflows/pages.yml | 36 ++++++++++ .gitignore | 1 + client/game/attractMode.ts | 50 +++++-------- client/game/comets.ts | 45 ++++++------ client/game/demoMode.ts | 3 +- client/game/draw.ts | 9 +++ client/game/gameMode.ts | 2 +- client/game/highScoreMode.ts | 135 +++++++++++++++++++---------------- client/game/highscores.ts | 56 +++++---------- client/game/initialsMode.ts | 80 --------------------- client/game/keys.ts | 5 ++ client/game/loop.ts | 2 +- client/game/screen.ts | 1 + client/game/world.ts | 9 +-- client/index.html | 34 ++++----- client/rpc/api.ts | 35 +++++---- client/rpc/storage.ts | 3 +- client/rpc/wallet.ts | 1 + client/style.css | 15 ++-- rollup/index.ts | 20 ++++-- 20 files changed, 248 insertions(+), 294 deletions(-) create mode 100644 .github/workflows/pages.yml delete mode 100644 client/game/initialsMode.ts diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ded26ec --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,36 @@ +name: Update Deployment + +on: + push: + branches: + - "main" + +jobs: + publish: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Install dependencies + run: cd client && npm install + + - name: Remove .gitignore + run: rm -rf client/.gitignore + + - name: Generate your content + run: npm run build:client + + - name: Publish current workdir (which contains generated content) to GitHub Pages + uses: rayluo/github-pages-overwriter@v1.3 + + with: + source-directory: client + target-branch: gh-pages diff --git a/.gitignore b/.gitignore index ace2805..a159644 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/*.sqlite **/*.env +node_modules/ diff --git a/client/game/attractMode.ts b/client/game/attractMode.ts index 76b04b7..7306114 100644 --- a/client/game/attractMode.ts +++ b/client/game/attractMode.ts @@ -1,10 +1,9 @@ import { IGameState } from "../comets"; -import { startGame } from "../rpc/api"; +import { fetchMruInfo, startGame } from "../rpc/api"; import { addToStore, getFromStore, StorageKey } from "../rpc/storage"; import { DemoMode } from "./demoMode"; import { EventSource } from "./events"; import { HighScoreMode } from "./highScoreMode"; -import { Highscores } from "./highscores"; import { Key } from "./keys"; import { Screen } from "./screen"; import { Sound } from "./sounds"; @@ -14,10 +13,8 @@ const ATTRACT_TIME = 15; // combines DemoMode and HighscoreMode to attract people to part with their quarters export class AttractMode extends EventSource implements IGameState { - private demoTimer = 0; private currentMode: IGameState; private modes: IGameState[]; - private index: number = 0; private isStarting = false; constructor(world: World, lastScore: number) { @@ -25,7 +22,7 @@ export class AttractMode extends EventSource implements IGameState { this.modes = [ new HighScoreMode(lastScore), - new DemoMode(world || new World(Highscores.top.score)), + new DemoMode(world || new World()), ]; this.currentMode = this.modes[0]; @@ -36,43 +33,30 @@ export class AttractMode extends EventSource implements IGameState { update(step: number) { this.currentMode.update(step); - // since page is reloaded on update for now, we need to check if game is already started - if (getFromStore(StorageKey.GAME_ID)) { - this.trigger("done"); - } if (Key.isAnyPressed()) { - if (getFromStore(StorageKey.GAME_ID)) { - this.trigger("done"); - } else if (!this.isStarting) { + if (!this.isStarting) { this.isStarting = true; - startGame().then((res) => { - if (res?.error || !res.isOk) { - console.error(res.error); + startGame() + .then((res) => { + console.log("Game started", res.logs[0].value); + addToStore(StorageKey.GAME_ID, res.logs[0].value); + this.isStarting = false; + this.trigger("done"); + }) + .catch((e) => { + console.error("Error starting game", e.message); + }) + .finally(() => { this.isStarting = false; - return; - } - console.log("Game started", res.logs[0].value); - addToStore(StorageKey.GAME_ID, res.logs[0].value); - this.isStarting = false; - }); + // clears the keys to prevent the game from starting again + Key.clear(); + }); } - } else { - this.updateAttractTimer(step); } } render(screen: Screen, dt?: number) { this.currentMode.render(screen, dt); } - - updateAttractTimer(step: number) { - this.demoTimer += step; - - if (this.demoTimer >= ATTRACT_TIME) { - this.demoTimer = 0; - this.index = 1 - this.index; - this.currentMode = this.modes[this.index]; - } - } } diff --git a/client/game/comets.ts b/client/game/comets.ts index c471b5c..dddd3bd 100644 --- a/client/game/comets.ts +++ b/client/game/comets.ts @@ -1,12 +1,10 @@ import { IGameState } from "../comets"; -import { fetchMruInfo } from "../rpc/api"; +import { fetchLeaderboard, fetchMruInfo } from "../rpc/api"; import { removeFromStore, StorageKey } from "../rpc/storage"; import { getWalletClient } from "../rpc/wallet"; import { AttractMode } from "./attractMode"; import { GameMode } from "./gameMode"; import Global from "./global"; -import { Highscores } from "./highscores"; -import { InitialsMode } from "./initialsMode"; import { Key, Keys } from "./keys"; import { loop } from "./loop"; import { Screen } from "./screen"; @@ -18,26 +16,23 @@ export class Comets { private lastScore = 0; private attractMode: AttractMode; private gameMode: GameMode; - private initialsMode: InitialsMode; private currentMode: IGameState; private tickRecorder: TickRecorder; private screen: Screen; + private isSendingTicks = false; constructor() { this.init(); } init() { - this.attractMode = new AttractMode( - new World(Highscores.top.score), - this.lastScore - ); + this.attractMode = new AttractMode(new World(), this.lastScore); this.screen = new Screen(); this.currentMode = this.attractMode; this.tickRecorder = new TickRecorder(); const setGameMode = () => { - this.gameMode = new GameMode(new World(Highscores.top.score)); + this.gameMode = new GameMode(new World()); this.currentMode = this.gameMode; this.tickRecorder.reset(); @@ -45,19 +40,25 @@ export class Comets { // Send ticks in the form of an action to MRU // And wait for C1 to confirm score this.lastScore = world.score; - await this.tickRecorder.sendTicks(this.lastScore); - - if (Highscores.qualifies(world.score)) { - this.initialsMode = new InitialsMode(world.score); - this.currentMode = this.initialsMode; - - this.initialsMode.on("done", () => { - this.init(); - }); - } else { - // restart in attract mode - this.init(); + if (!this.isSendingTicks) { + this.isSendingTicks = true; + await this.tickRecorder + .sendTicks(this.lastScore) + .then(() => { + console.log("Sent ticks"); + }) + .catch((e) => { + console.error("Error sending ticks", e.message); + }) + .finally(() => { + removeFromStore(StorageKey.GAME_ID); + this.isSendingTicks = false; + this.init(); + // Reload page + window.location.reload(); + }); } + // restart in attract mode console.log("Game over"); }); }; @@ -115,7 +116,7 @@ const game = new Comets(); // setup things (async () => { - await fetchMruInfo(); + await Promise.all([fetchMruInfo(), fetchLeaderboard()]); await getWalletClient(); setTimeout(() => { loop(game); diff --git a/client/game/demoMode.ts b/client/game/demoMode.ts index e9dfd03..7e7d619 100644 --- a/client/game/demoMode.ts +++ b/client/game/demoMode.ts @@ -76,7 +76,8 @@ export class DemoMode implements IGameState { screen.draw.background(); screen.draw.scorePlayer1(this.world.score); screen.draw.oneCoinOnePlay(); - screen.draw.highscore(this.world.highscore); + // screen.draw.highscore(this.world.highscore); + screen.draw.stackr(); screen.draw.copyright(); } diff --git a/client/game/draw.ts b/client/game/draw.ts index ee8a536..2930b97 100644 --- a/client/game/draw.ts +++ b/client/game/draw.ts @@ -328,6 +328,15 @@ export class Draw { }); } + stackr() { + this.text2("Stackr", this.screen.font.small, (width) => { + return { + x: this.screen.width2 - width / 2, + y: Y_START, + }; + }); + } + oneCoinOnePlay() { this.text2("1 coin 1 play", this.screen.font.medium, (width) => { return { diff --git a/client/game/gameMode.ts b/client/game/gameMode.ts index c4417db..0be526b 100644 --- a/client/game/gameMode.ts +++ b/client/game/gameMode.ts @@ -107,8 +107,8 @@ export class GameMode extends EventSource implements IGameState { private renderStatic(screen: Screen) { screen.draw.background(); screen.draw.copyright(); + screen.draw.stackr(); screen.draw.scorePlayer1(this.world.score); - screen.draw.highscore(this.world.highscore); screen.draw.drawExtraLives(this.world.lives); // remaining shields diff --git a/client/game/highScoreMode.ts b/client/game/highScoreMode.ts index 40b2d54..047d6e8 100644 --- a/client/game/highScoreMode.ts +++ b/client/game/highScoreMode.ts @@ -1,77 +1,90 @@ -import { Screen } from './screen'; -import { Highscores } from './highscores'; -import { IGameState } from '../comets'; +import { IGameState } from "../comets"; +import { Highscores } from "./highscores"; +import { Screen } from "./screen"; +const formatAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +}; export class HighScoreMode implements IGameState { + blink: number = 0; + showPushStart: boolean = true; - blink: number = 0; - showPushStart: boolean = true; - highscore: number; + constructor(private score) {} - constructor(private score) { - this.highscore = Highscores.top.score; - } + update(dt) { + this.blink += dt; - update(dt) { - this.blink += dt; - - if (this.blink >= .4) { - this.blink = 0; - this.showPushStart = !this.showPushStart; - } + if (this.blink >= 0.4) { + this.blink = 0; + this.showPushStart = !this.showPushStart; } + } - render(screen: Screen, delta: number) { - this.drawBackground(screen); - this.drawPushStart(screen); - this.drawHighScores(screen); - } + render(screen: Screen, delta: number) { + this.drawBackground(screen); + this.drawPushStart(screen); + this.drawHighScores(screen); + } - private drawBackground(screen: Screen) { - screen.draw.background(); - screen.draw.scorePlayer1(this.score); - screen.draw.oneCoinOnePlay(); - screen.draw.highscore(this.highscore); - screen.draw.copyright(); - } + private drawBackground(screen: Screen) { + screen.draw.background(); + screen.draw.stackr(); + screen.draw.scorePlayer1(this.score); + screen.draw.oneCoinOnePlay(); + screen.draw.copyright(); + } - private drawHighScores(screen: Screen) { - const screenX = screen.width / 2; - const startY = Math.ceil(screen.height / 4.5) + (screen.font.xlarge + screen.font.small); - const spacing = screen.font.medium + screen.font.small; - - screen.draw.text2('high scores', screen.font.large, (width) => { - return { - x: screenX - (width / 2), - y: screen.height / 4.5 - } - }); + private drawHighScores(screen: Screen) { + const screenX = screen.width / 2; + const startY = + Math.ceil(screen.height / 4.5) + (screen.font.xlarge + screen.font.small); + const spacing = screen.font.medium + screen.font.small; - for (let i = 0; i < Highscores.scores.length; i++) { - const y = startY + (i * spacing); - const text = `${this.pad(i + 1, ' ', 2)}.${this.pad(Highscores.scores[i].score, ' ', 6)} ${Highscores.scores[i].initials}`; + screen.draw.text2("Leaderboard", screen.font.large, (width) => { + return { + x: screenX - width / 2, + y: screen.height / 4.5, + }; + }); - screen.draw.text2(text, screen.font.large, (width) => { - return { - x: screenX - (width / 2), - y: y - } - }); - } + if (Highscores.scores.length === 0) { + screen.draw.text2("not enough data", screen.font.medium, (width) => { + return { + x: screenX - width / 2, + y: startY, + }; + }); + return; } - private drawPushStart(screen: Screen) { - if (this.showPushStart) { - screen.draw.pushStart(); - } + for (let i = 0; i < Highscores.scores.length; i++) { + const y = startY + i * spacing; + const text = `${this.pad(i + 1, " ", 2)}.${this.pad( + formatAddress(Highscores.scores[i].address), + " ", + 6 + )} ${this.pad(Highscores.scores[i].score, " ", 8)}`; + + screen.draw.text2(text, screen.font.large, (width) => { + return { + x: screenX - width / 2, + y: y, + }; + }); } - - private pad(text: any, char: string, count: number) { - text = text.toString(); - while (text.length < count) { - text = char + text; - } - return text; + } + + private drawPushStart(screen: Screen) { + if (this.showPushStart) { + screen.draw.pushStart(); } + } -} \ No newline at end of file + private pad(text: any, char: string, count: number) { + text = text.toString(); + while (text.length < count) { + text = char + text; + } + return text; + } +} diff --git a/client/game/highscores.ts b/client/game/highscores.ts index 01b2540..23c5503 100644 --- a/client/game/highscores.ts +++ b/client/game/highscores.ts @@ -1,48 +1,24 @@ -// move to local storage at some point - -const defaults = [ - { score: 20140, initials: 'J H'}, - { score: 20050, initials: 'P A'}, - { score: 19930, initials: ' M'}, - { score: 19870, initials: 'G I'}, - { score: 19840, initials: 'A L'}, - { score: 19790, initials: 'M T'}, - { score: 19700, initials: 'E O'}, - { score: 19660, initials: 'S N'}, - { score: 190, initials: ' '}, - { score: 70, initials: ' '}, -]; - -const SCORE_KEY = 'jph_asteroids_hs'; +import { fetchLeaderboard } from "../rpc/api"; +import { getFromStore, StorageKey } from "../rpc/storage"; class Highscores { + constructor() {} - scores: { score: number, initials: string}[] = []; - - constructor() { - const str = window.localStorage.getItem(SCORE_KEY); - this.scores = str ? JSON.parse(str) || [] : defaults; - } - - get top() { - return this.scores[0]; - } - - qualifies(score: number) { - const less = highscores.scores.filter(x => x.score < score); - return !!less.length; - } + get scores(): { score: number; address: string }[] { + const data = getFromStore(StorageKey.LEADERBOARD); + return data ? data : []; + } - save(score: number, initials: string) { - if (this.qualifies(score)) { - this.scores.push({score: score, initials: initials}); - this.scores = this.scores.sort((a, b) => a.score > b.score ? -1 : 1).slice(0, 10); - window.localStorage.setItem(SCORE_KEY, JSON.stringify(this.scores)); - } - } + get top() { + return this.scores[0]; + } + qualifies(score: number) { + const less = highscores.scores.filter((x) => x.score < score); + return !!less.length; + } } -const highscores = new Highscores() +const highscores = new Highscores(); -export { highscores as Highscores } +export { highscores as Highscores }; diff --git a/client/game/initialsMode.ts b/client/game/initialsMode.ts deleted file mode 100644 index 767a686..0000000 --- a/client/game/initialsMode.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Screen } from './screen'; -import { Sound } from './sounds'; -import { Key } from './keys'; -import { Highscores } from './highscores'; -import { EventSource } from './events'; -import { IGameState } from '../comets'; - -const letters = '_abcdefghijklmnopqrstuvwxyz'; - -export class InitialsMode extends EventSource implements IGameState { - index: number = 1; - position: number; - score: number; - initials: string[]; - - constructor(score: number) { - super(); - this.score = score; - this.init(); - } - - init() { - this.position = 0; - this.index = 1; - this.initials = ['a', '_', '_']; - - Sound.stop(); - Sound.off(); - } - - update(dt) { - if (Key.wasRotateLeft()) { - this.index--; - if (this.index < 0) { - this.index = letters.length - 1; - } - this.initials[this.position] = letters[this.index]; - } - - if (Key.wasRotateRight()) { - this.index++; - if (this.index > letters.length - 1) { - this.index = 0; - } - this.initials[this.position] = letters[this.index]; - } - - if (Key.wasHyperspace()) { - this.position++; - - if (this.position >= 3) { - Highscores.save(this.score, this.initials.join('').replace('_',' ')); - this.init(); - this.trigger('done'); - } - - this.index = 1; - this.initials[this.position] = letters[this.index]; - } - } - - render(screen: Screen, delta: number) { - let offset: number = screen.height / 4.5; - const text = (t => screen.draw.text(t, 50, offset += screen.font.large + 5, screen.font.large)); - - screen.draw.background(); - screen.draw.highscore(Highscores.top.score); - screen.draw.scorePlayer1(this.score); - screen.draw.copyright(); - - text('your score is one of the ten best'); - text('please enter your initials'); - text('push rotate to select letter'); - text('push hyperspace when letter is correct'); - - screen.draw.text3(this.initials.join(''), screen.font.xlarge, (width) => { - return { x: screen.width2 - (width / 2), y: screen.height / 2 + screen.font.xlarge }; - }); - } -} \ No newline at end of file diff --git a/client/game/keys.ts b/client/game/keys.ts index bc2facf..cb1fd76 100644 --- a/client/game/keys.ts +++ b/client/game/keys.ts @@ -91,6 +91,11 @@ export class _Key { this.touched = !this.touched; } + clear() { + this.keys = {}; + this.prev = {}; + } + isPressed(key: number) { return this.prev[key] === false && this.keys[key] === true; } diff --git a/client/game/loop.ts b/client/game/loop.ts index e542f88..4da58c4 100644 --- a/client/game/loop.ts +++ b/client/game/loop.ts @@ -1,7 +1,7 @@ import { Comets } from "./comets"; const timestamp = () => { - return window.performance && window.performance.now + return window.performance?.now ? window.performance.now() : new Date().getTime(); }; diff --git a/client/game/screen.ts b/client/game/screen.ts index ab29aea..5003225 100644 --- a/client/game/screen.ts +++ b/client/game/screen.ts @@ -29,6 +29,7 @@ export class Screen implements Rect { this.init(); window.addEventListener('resize', () => { + console.log("resizing"); this.init(); }); } diff --git a/client/game/world.ts b/client/game/world.ts index 7b1b3ce..a3bdf89 100644 --- a/client/game/world.ts +++ b/client/game/world.ts @@ -27,7 +27,6 @@ const NUM_OF_LIVES = 1; export class World { level = 1; extraLifeScore = 0; - highscore; score = 0; lives = NUM_OF_LIVES; @@ -53,9 +52,7 @@ export class World { gameOver: boolean = false; started: boolean = false; - constructor(highscore: number) { - this.highscore = highscore; - } + constructor() {} get objects(): any { return [ @@ -328,10 +325,6 @@ export class World { this.score += obj.score; this.extraLifeScore += obj.score; - if (this.score > this.highscore) { - this.highscore = this.score; - } - if (this.extraLifeScore >= EXTRA_LIFE) { this.lives++; this.extraLifeScore -= EXTRA_LIFE; diff --git a/client/index.html b/client/index.html index 40b1867..b1d7c87 100644 --- a/client/index.html +++ b/client/index.html @@ -1,20 +1,20 @@ + + + Comets + + + + - - - Comets - - - - - - -
- -
- - - - \ No newline at end of file + +
+ +
+ + + diff --git a/client/rpc/api.ts b/client/rpc/api.ts index 753f1ba..0d32485 100644 --- a/client/rpc/api.ts +++ b/client/rpc/api.ts @@ -1,10 +1,5 @@ import { getAddress } from "viem"; -import { - addToStore, - getFromStore, - removeFromStore, - StorageKey, -} from "./storage"; +import { addToStore, getFromStore, StorageKey } from "./storage"; import { getWalletClient } from "./wallet"; const API_URL = "http://localhost:3210"; @@ -15,19 +10,30 @@ const fetchMruInfo = async () => { addToStore(StorageKey.MRU_INFO, res); }; +const fetchLeaderboard = async () => { + const response = await fetch(`${API_URL}/leaderboard`); + const res = await response.json(); + addToStore(StorageKey.LEADERBOARD, res); +}; + const submitAction = async (transition: string, inputs: any) => { const walletClient = await getWalletClient(); const mruInfo = getFromStore(StorageKey.MRU_INFO); const { domain, schemas } = mruInfo; const msgSender = getAddress(walletClient.account.address); - const signature = await walletClient.signTypedData({ - domain, - primaryType: schemas[transition].primaryType, - types: schemas[transition].types, - message: inputs, - account: msgSender, - }); + let signature; + try { + signature = await walletClient.signTypedData({ + domain, + primaryType: schemas[transition].primaryType, + types: schemas[transition].types, + message: inputs, + account: msgSender, + }); + } catch (e) { + console.error("Error signing message", e); + } const response = await fetch(`${API_URL}/${transition}`, { method: "POST", @@ -46,7 +52,6 @@ const submitAction = async (transition: string, inputs: any) => { const endGame = async (inputs: any) => { await submitAction("endGame", inputs); - removeFromStore(StorageKey.GAME_ID); }; const startGame = async () => { @@ -55,5 +60,5 @@ const startGame = async () => { return res; }; -export { endGame, fetchMruInfo, startGame }; +export { endGame, fetchLeaderboard, fetchMruInfo, startGame }; diff --git a/client/rpc/storage.ts b/client/rpc/storage.ts index f8dbf4f..572dd52 100644 --- a/client/rpc/storage.ts +++ b/client/rpc/storage.ts @@ -1,6 +1,7 @@ export enum StorageKey { MRU_INFO = "mru_info", GAME_ID = "game_id", + LEADERBOARD = "leaderboard", } export const addToStore = (key: StorageKey, value: any) => { @@ -12,5 +13,5 @@ export const removeFromStore = (key: StorageKey) => { }; export const getFromStore = (key: StorageKey) => { - return JSON.parse(localStorage.getItem(key)); + return JSON.parse(localStorage.getItem(key) || "null"); }; diff --git a/client/rpc/wallet.ts b/client/rpc/wallet.ts index a6977f7..3bcdf24 100644 --- a/client/rpc/wallet.ts +++ b/client/rpc/wallet.ts @@ -1,6 +1,7 @@ import { createWalletClient, custom, WalletClient } from "viem"; import { sepolia } from "viem/chains"; import { getFromStore, StorageKey } from "./storage"; + let walletClient: any; const addChainIfMissing = async (walletClient: WalletClient) => { diff --git a/client/style.css b/client/style.css index ac93966..87e54d0 100644 --- a/client/style.css +++ b/client/style.css @@ -1,17 +1,16 @@ @font-face { font-family: "hyperspace"; - src: url('assets/Hyperspace.otf'); + src: url("assets/Hyperspace.otf"); } body { - font-family: hyperspace; - width: 100vw; - height: 100vh; - margin: 0; - background-color: #000000; + font-family: hyperspace; + width: 100vw; + height: 100vh; + margin: 0; + background-color: #000000; } canvas { - vertical-align: middle; + vertical-align: middle; } - diff --git a/rollup/index.ts b/rollup/index.ts index 0c3047b..7f5e5d0 100644 --- a/rollup/index.ts +++ b/rollup/index.ts @@ -111,6 +111,20 @@ const main = async () => { }); }); + app.get("/leaderboard", async (_req, res) => { + const { state } = stateMachine; + const topTen = [...state.games] + .sort((a, b) => b.score - a.score) + .slice(0, 10); + + const leaderboard = topTen.map((game) => ({ + address: game.player, + score: game.score, + })); + + return res.json(leaderboard); + }); + const handleAction = async ( transition: string, schema: ActionSchema, @@ -129,12 +143,6 @@ const main = async () => { const { transition } = req.params; const schema = stfSchemaMap[transition]; - // TEMPORARY - // const { inputs } = req.body; - // const signature = await signMessage(wallet, schema, inputs); - // const msgSender = wallet.address; - - // FINAL const { inputs, signature, msgSender } = req.body; try {