Skip to content

Commit

Permalink
Add stats/{id} page for single runs
Browse files Browse the repository at this point in the history
  • Loading branch information
oskarrough committed Feb 22, 2024
1 parent 20593a5 commit 8983ff0
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 64 deletions.
24 changes: 21 additions & 3 deletions src/game/backend.js
Expand Up @@ -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<object>} 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
*/

/**
Expand All @@ -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',
Expand All @@ -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: {
Expand All @@ -52,3 +62,11 @@ export async function getRuns() {
const {runs} = await res.json()
return runs
}

/**
* @returns {Promise<Run>} a single run
*/
export async function getRun(id) {
const res = await fetch(apiUrl + `/${id}`)
return res.json()
}
2 changes: 1 addition & 1 deletion src/ui/components/publish-run.js
Expand Up @@ -34,7 +34,7 @@ export function PublishRun({game}) {
<input type="text" name="playername" required placeholder="Know thyself" />
</label>
<button disabled=${loading} type="submit">Submit my run</button>
<p>${loading ? 'submitting' : ''}</p>
<p>${loading ? 'Submitting…' : ''}</p>
<p><a href="/stats">View highscores</a></p>
`
: html`<p>Thank you.</p>`}
Expand Down
130 changes: 70 additions & 60 deletions src/ui/pages/stats.astro
Expand Up @@ -19,71 +19,81 @@ const runs = (await getRuns()).reverse()
<div class="Box Box--text Box--full">
<p>
A chronological list of Slay the Web runs.<br />
There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. <a href="https://matrix.to/#/#slaytheweb:matrix.org" rel="nofollow">Chat on #slaytheweb:matrix.org</a></p>
There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. <a
href="https://matrix.to/#/#slaytheweb:matrix.org"
rel="nofollow">Chat on #slaytheweb:matrix.org</a>
</p>
<table>
<thead>
<tr>
<th>Player</th>
<th>Win?</th>
<th>Floor</th>
<th>Health</th>
<th>Cards</th>
<th align="right">Time</th>
<th align="right">Date</th>
</tr>
</thead>
<tbody>
{
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))
</div>
<table>
<thead>
<tr>
<th>Player</th>
<th>Win?</th>
<th>Floor</th>
<th>Health</th>
<th>Cards</th>
<th align="right">Time</th>
<th align="right">Date</th>
</tr>
</thead>
<tbody>
{
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 (
<tr>
<td>{run.player}</td>
<td>{state.won ? 'WIN' : 'LOSS'}</td>
<td>{state.dungeon.y}</td>
<td>{state.player.currentHealth}</td>
<td>{run.gameState.deck.length}</td>
<td align="right">{duration}</td>
<td align="right">{date}</td>
</tr>
)
})
: 'Loading...'
}
</tbody>
</table>
<p>
If you want your run removed, <a href="https://matrix.to/#/#slaytheweb:matrix.org">let me know</a>.
</p>
</div>
return (
<tr>
<td>
<a href={`/stats/` + run.id}>
{run.id}. {run.player}
</a>
</td>
<td>{state.won ? 'WIN' : 'LOSS'}</td>
<td>{state.dungeon.y}</td>
<td>
{state.player.currentHealth}/{state.player.maxHealth}
</td>
<td>{run.gameState.deck.length}</td>
<td align="right">{duration}</td>
<td align="right">{date}</td>
</tr>
)
})
: 'Loading...'
}
</tbody>
</table>
<p>
If you want your run removed, <a href="https://matrix.to/#/#slaytheweb:matrix.org">let me know</a>.
</p>
</article>
</Layout>


<style>
table {width: 100%; border-spacing: 0;}
tbody tr:nth-child(odd) {
background-color: #eee;
}
th,
td {
text-align: left;
}
table {
width: 100%;
border-spacing: 0;
}
tbody tr:nth-child(odd) {
background-color: #eee;
}
th,
td {
text-align: left;
}
</style>
55 changes: 55 additions & 0 deletions 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)
}
---

<Layout title="Statistics & Highscores">
<article class="Container">
<p><a class="Button" href="/stats">&larr; Back to all runs</a></p>
<h1>Slay the Web run no. {run.id}</h1>
<div class="Box Box--text Box--full">
<p><em>{run.player}</em> made it to floor {state.dungeon.y} and {state.won ? 'won' : 'lost'} in {duration} on {date} with {state.player.currentHealth}/{state.player.maxHealth} health.</p>
{extraStats && <p>You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.</p>}
<p>Final deck had {state.deck.length} cards:</p>
<ul>
{state.deck.map(card => <li>{card}</li>)}
</ul>

</div>
</article>
</Layout>

0 comments on commit 8983ff0

Please sign in to comment.