From 20593a5bacf0f398ba92c8e3465d6a55db299be6 Mon Sep 17 00:00:00 2001 From: Oskar Rough Date: Thu, 22 Feb 2024 11:47:54 +0100 Subject: [PATCH 1/3] Use simpler format for dungeon.pathTaken --- src/game/actions.js | 2 +- src/game/dungeon.js | 8 ++++---- src/ui/components/dungeon-stats.js | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/game/actions.js b/src/game/actions.js index d88b2d54..1e02dd51 100644 --- a/src/game/actions.js +++ b/src/game/actions.js @@ -567,7 +567,7 @@ function move(state, {move}) { draft.player.currentEnergy = 3 draft.player.block = 0 draft.dungeon.graph[move.y][move.x].didVisit = true - draft.dungeon.pathTaken.push({x: move.x, y: move.y}) + draft.dungeon.pathTaken.push([move.x, move.y]) draft.dungeon.x = move.x draft.dungeon.y = move.y // if (number === state.dungeon.rooms.length - 1) { diff --git a/src/game/dungeon.js b/src/game/dungeon.js index 6ca4ebfa..289e6e40 100644 --- a/src/game/dungeon.js +++ b/src/game/dungeon.js @@ -56,9 +56,9 @@ export const defaultOptions = { * @prop {string} id a unique id * @prop {Graph} graph * @prop {Array} paths - * @prop {number} x current x position - * @prop {number} y current y position - * @prop {Array} pathTaken a list of moves we've taken + * @prop {number} x current x position (which path) + * @prop {number} y current y position (where on the path) + * @prop {Array} pathTaken a list of moves we've taken */ /** @@ -87,7 +87,7 @@ export default function Dungeon(options) { paths, x: 0, y: 0, - pathTaken: [{x: 0, y: 0}], + pathTaken: [[0, 0]], } } diff --git a/src/ui/components/dungeon-stats.js b/src/ui/components/dungeon-stats.js index d99929c7..05ddb909 100644 --- a/src/ui/components/dungeon-stats.js +++ b/src/ui/components/dungeon-stats.js @@ -13,16 +13,17 @@ export default function DungeonStats({dungeon}) { ` } -const getEnemiesStats = (dungeon) => { +export const getEnemiesStats = (dungeon) => { const stats = { killed: 0, encountered: 0, maxHealth: 0, finalHealth: 0, } + if (!dungeon.graph) throw new Error('Missing dungeon graph') /* for each path taken (room) in the dungeon, get some stats */ - dungeon.pathTaken.forEach((usedNode) => { - const nodeData = dungeon.graph[usedNode.y][usedNode.x] + dungeon.pathTaken.forEach(([x, y]) => { + const nodeData = dungeon.graph[y][x] /* find some stats about the enemies encountered */ if (nodeData.room?.monsters) { /* how many encountered monsters */ From 8983ff0b5199ccd1d7e5177c2645fcd8d5f86ca4 Mon Sep 17 00:00:00 2001 From: Oskar Rough Date: Thu, 22 Feb 2024 11:48:32 +0100 Subject: [PATCH 2/3] Add stats/{id} page for single runs --- src/game/backend.js | 24 +++++- src/ui/components/publish-run.js | 2 +- src/ui/pages/stats.astro | 130 +++++++++++++++++-------------- src/ui/pages/stats/[id].astro | 55 +++++++++++++ 4 files changed, 147 insertions(+), 64 deletions(-) create mode 100644 src/ui/pages/stats/[id].astro diff --git a/src/game/backend.js b/src/game/backend.js index 83f80c81..ace0070f 100644 --- a/src/game/backend.js +++ b/src/game/backend.js @@ -7,7 +7,15 @@ const apiUrl = 'https://api.slaytheweb.cards/api/runs' * @typedef {object} Run * @prop {string} player - user inputted player name * @prop {object} gameState - the final state - * @prop {Array} gamePast - a list of past states + * @prop {PastEntry[]} gamePast - a list of past states + */ + +/** + * A simplified version of the game.past entries + * @typedef {object} PastEntry + * @prop {number} turn + * @prop {object} action + * @prop {object} player */ /** @@ -17,8 +25,6 @@ const apiUrl = 'https://api.slaytheweb.cards/api/runs' * @returns {Promise} */ export async function postRun(game, playerName) { - console.log('postRun', game.past.list) - /** @type {Run} */ const run = { player: playerName || 'Unknown entity', @@ -28,12 +34,16 @@ export async function postRun(game, playerName) { gamePast: game.past.list.map((item) => { return { action: item.action, + // we're not including the entire state, it's too much data + // but we do want to know which turn and the player's state at the time turn: item.state.turn, player: item.state.player, } }), } + console.log('Posting run', run) + return fetch(apiUrl, { method: 'POST', headers: { @@ -52,3 +62,11 @@ export async function getRuns() { const {runs} = await res.json() return runs } + +/** + * @returns {Promise} a single run + */ +export async function getRun(id) { + const res = await fetch(apiUrl + `/${id}`) + return res.json() +} diff --git a/src/ui/components/publish-run.js b/src/ui/components/publish-run.js index 3d5eb860..b9638cab 100644 --- a/src/ui/components/publish-run.js +++ b/src/ui/components/publish-run.js @@ -34,7 +34,7 @@ export function PublishRun({game}) { -

${loading ? 'submitting' : ''}

+

${loading ? 'Submitting…' : ''}

View highscores

` : html`

Thank you.

`} diff --git a/src/ui/pages/stats.astro b/src/ui/pages/stats.astro index cc722350..78771673 100644 --- a/src/ui/pages/stats.astro +++ b/src/ui/pages/stats.astro @@ -19,71 +19,81 @@ const runs = (await getRuns()).reverse()

A chronological list of Slay the Web runs.
- There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. Chat on #slaytheweb:matrix.org

+ There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. Chat on #slaytheweb:matrix.org

- - - - - - - - - - - - - - { - runs?.length - ? runs.map((run) => { - const state = run.gameState - const date = new Intl.DateTimeFormat('en', { - dateStyle: 'long', - // timeStyle: 'short', - hour12: false, - }).format(new Date(state.createdAt)) + +
PlayerWin?FloorHealthCardsTimeDate
+ + + + + + + + + + + + + { + runs?.length + ? runs.map((run) => { + const state = run.gameState + const date = new Intl.DateTimeFormat('en', { + dateStyle: 'long', + // timeStyle: 'short', + hour12: false, + }).format(new Date(state.createdAt)) - let duration = 0 - if (state.endedAt) { - const ms = state.endedAt - state.createdAt - const hours = Math.floor(ms / (1000 * 60 * 60)) - const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) - const seconds = Math.floor((ms / 1000) % 60) - duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s` - } + let duration = 0 + if (state.endedAt) { + const ms = state.endedAt - state.createdAt + const hours = Math.floor(ms / (1000 * 60 * 60)) + const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((ms / 1000) % 60) + duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s` + } - return ( - - - - - - - - - - ) - }) - : 'Loading...' - } - -
PlayerWin?FloorHealthCardsTimeDate
{run.player}{state.won ? 'WIN' : 'LOSS'}{state.dungeon.y}{state.player.currentHealth}{run.gameState.deck.length}{duration}{date}
-

- If you want your run removed, let me know. -

-
+ return ( + + + + {run.id}. {run.player} + + + {state.won ? 'WIN' : 'LOSS'} + {state.dungeon.y} + + {state.player.currentHealth}/{state.player.maxHealth} + + {run.gameState.deck.length} + {duration} + {date} + + ) + }) + : 'Loading...' + } + + +

+ If you want your run removed, let me know. +

- diff --git a/src/ui/pages/stats/[id].astro b/src/ui/pages/stats/[id].astro new file mode 100644 index 00000000..0e28eb08 --- /dev/null +++ b/src/ui/pages/stats/[id].astro @@ -0,0 +1,55 @@ +--- +import Layout from '../../layouts/Layout.astro' +import {getRuns, getRun} from '../../../game/backend.js' +import {getEnemiesStats} from '../../components/dungeon-stats.js' +import '../../styles/typography.css' + +export const getStaticPaths = (async () => { + const runs = await getRuns() + return runs.map(run => { + return { + params: {id: run.id} + } + }) +}) + +const {id} = Astro.params +const run = await getRun(id) +const state = run.gameState + +const date = new Intl.DateTimeFormat('en', { + dateStyle: 'long', + // timeStyle: 'short', + hour12: false, +}).format(new Date(state.createdAt)) + +const ms = state.endedAt - state.createdAt +const hours = Math.floor(ms / (1000 * 60 * 60)) +const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) +const seconds = Math.floor((ms / 1000) % 60) +const duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s` + +// Not all runs have this data in the backend. +let extraStats = false +if (state.dungeon.graph) { + extraStats = getEnemiesStats(state.dungeon) + console.log(extraStats) +} +--- + + +
+

← Back to all runs

+

Slay the Web run no. {run.id}

+
+

{run.player} made it to floor {state.dungeon.y} and {state.won ? 'won' : 'lost'} in {duration} on {date} with {state.player.currentHealth}/{state.player.maxHealth} health.

+ {extraStats &&

You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.

} +

Final deck had {state.deck.length} cards:

+
    + {state.deck.map(card =>
  • {card}
  • )} +
+ +
+
+
+ From 49e3d2482c4b73785d4afbd082f628e08debda1e Mon Sep 17 00:00:00 2001 From: Oskar Rough Date: Thu, 22 Feb 2024 12:11:14 +0100 Subject: [PATCH 3/3] Adjust stats pages to new API responses --- src/game/backend.js | 4 +++ src/ui/pages/stats.astro | 16 +++------- src/ui/pages/stats/[id].astro | 58 +++++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/game/backend.js b/src/game/backend.js index ace0070f..544ba88f 100644 --- a/src/game/backend.js +++ b/src/game/backend.js @@ -6,6 +6,10 @@ const apiUrl = 'https://api.slaytheweb.cards/api/runs' /** * @typedef {object} Run * @prop {string} player - user inputted player name + * @prop {win} number + * @prop {number} floor + * @prop {number} floor + * @prop {number} floor * @prop {object} gameState - the final state * @prop {PastEntry[]} gamePast - a list of past states */ diff --git a/src/ui/pages/stats.astro b/src/ui/pages/stats.astro index 78771673..81c1f88a 100644 --- a/src/ui/pages/stats.astro +++ b/src/ui/pages/stats.astro @@ -30,8 +30,6 @@ const runs = (await getRuns()).reverse() Player Win? Floor - Health - Cards Time Date @@ -45,11 +43,11 @@ const runs = (await getRuns()).reverse() dateStyle: 'long', // timeStyle: 'short', hour12: false, - }).format(new Date(state.createdAt)) + }).format(new Date(run.createdAt)) let duration = 0 - if (state.endedAt) { - const ms = state.endedAt - state.createdAt + if (run.endedAt) { + const ms = run.endedAt - run.createdAt const hours = Math.floor(ms / (1000 * 60 * 60)) const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) const seconds = Math.floor((ms / 1000) % 60) @@ -63,12 +61,8 @@ const runs = (await getRuns()).reverse() {run.id}. {run.player} - {state.won ? 'WIN' : 'LOSS'} - {state.dungeon.y} - - {state.player.currentHealth}/{state.player.maxHealth} - - {run.gameState.deck.length} + {run.won ? 'WIN' : 'LOSS'} + {run.floor} {duration} {date} diff --git a/src/ui/pages/stats/[id].astro b/src/ui/pages/stats/[id].astro index 0e28eb08..349b57d4 100644 --- a/src/ui/pages/stats/[id].astro +++ b/src/ui/pages/stats/[id].astro @@ -4,23 +4,22 @@ import {getRuns, getRun} from '../../../game/backend.js' import {getEnemiesStats} from '../../components/dungeon-stats.js' import '../../styles/typography.css' -export const getStaticPaths = (async () => { - const runs = await getRuns() - return runs.map(run => { - return { - params: {id: run.id} - } - }) -}) +export const getStaticPaths = async () => { + const runs = await getRuns() + return runs.map((run) => { + return { + params: {id: run.id}, + } + }) +} const {id} = Astro.params const run = await getRun(id) const state = run.gameState const date = new Intl.DateTimeFormat('en', { - dateStyle: 'long', - // timeStyle: 'short', - hour12: false, + dateStyle: 'long', + hour12: false, }).format(new Date(state.createdAt)) const ms = state.endedAt - state.createdAt @@ -32,24 +31,37 @@ const duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s` // Not all runs have this data in the backend. let extraStats = false if (state.dungeon.graph) { - extraStats = getEnemiesStats(state.dungeon) - console.log(extraStats) + extraStats = getEnemiesStats(state.dungeon) + console.log(extraStats) } ---
-

← Back to all runs

-

Slay the Web run no. {run.id}

+

← Back to all runs

+

Slay the Web run no. {run.id}

-

{run.player} made it to floor {state.dungeon.y} and {state.won ? 'won' : 'lost'} in {duration} on {date} with {state.player.currentHealth}/{state.player.maxHealth} health.

- {extraStats &&

You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.

} -

Final deck had {state.deck.length} cards:

-
    - {state.deck.map(card =>
  • {card}
  • )} -
- +

+ {run.player} made it to floor {state.dungeon.y} and {state.won ? 'won' : 'lost'} in { + duration + } on {date} with {state.player.currentHealth}/{state.player.maxHealth} health. +

+ { + extraStats && ( +

+ You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them. +

+ ) + } +

Final deck had {state.deck.length} cards:

+
    + {state.deck.map((card) =>
  • {card}
  • )} +
+

+ Feel free to inspect the data yourself: api.slaytheweb.cards/runs/{run.id}. +

-