ea83374ddb1d7428cd8535f116d7f9bd0f29fa72
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/server.go b/server.go
2index 174a7d0810896fa2b46cf91ed261f46ee577d675..33023444c5d91f53b7284dcaaa5ee37645a97533 100644
3--- a/server.go
4+++ b/server.go
5@@ -2,6 +2,7 @@ package main
6
7 import (
8 "embed"
9+ "fmt"
10 "html/template"
11 "io/fs"
12 "net/http"
13@@ -85,6 +86,80 @@ func (g *Game) nextSong() Song {
14 return g.catalog[idx]
15 }
16
17+// submitGuess scores the player's guess against the current answer and stores
18+// the result for rendering.
19+func (g *Game) submitGuess(title, album, artist string) {
20+ guesses := []string{title, album, artist}
21+ points := 0
22+ var parts []string
23+ fieldCorrect := make([]bool, len(fields))
24+ fieldPartial := make([]bool, len(fields))
25+ for i, f := range fields {
26+ v := guesses[i]
27+ if v != "" {
28+ sim := fuzzyScore(v, f.value(g.answer))
29+ points += scoreFor(sim, f.weight)
30+ switch {
31+ case sim >= matchThreshold:
32+ fieldCorrect[i] = true
33+ parts = append(parts, f.label)
34+ case sim >= lowerThreshold:
35+ fieldPartial[i] = true
36+ }
37+ }
38+ }
39+ if g.hinted {
40+ points /= 2
41+ }
42+ g.score += points
43+ g.played++
44+ g.result = &RoundResult{
45+ Points: points,
46+ Parts: parts,
47+ Guesses: guesses,
48+ FieldCorrect: fieldCorrect,
49+ FieldPartial: fieldPartial,
50+ ResultMsg: resultMessage(points, parts),
51+ Hinted: g.hinted,
52+ }
53+ if g.played >= g.rounds {
54+ g.done = true
55+ }
56+}
57+
58+// resultMessage builds the human-readable summary line for a scored guess.
59+func resultMessage(points int, parts []string) string {
60+ switch {
61+ case len(parts) == 3:
62+ return fmt.Sprintf("Correct! +%d %s", points, pointWord(points))
63+ case len(parts) > 0:
64+ return fmt.Sprintf("%s correct! +%d %s", strings.Join(parts, ", "), points, pointWord(points))
65+ case points > 0:
66+ return fmt.Sprintf("+%d %s", points, pointWord(points))
67+ default:
68+ return "Wrong."
69+ }
70+}
71+
72+// nextRound advances to the next song (generating its audio clip), or is a
73+// no-op when the game is finished.
74+func (g *Game) nextRound() {
75+ if g.done {
76+ return
77+ }
78+ g.beginRound()
79+}
80+
81+// restart begins a fresh game with a new song order and zero score.
82+func (g *Game) restart() {
83+ g.order = shuffled(len(g.catalog))
84+ g.pos = 0
85+ g.played = 0
86+ g.score = 0
87+ g.done = false
88+ g.beginRound()
89+}
90+
91 // viewData is the render model passed to the templates.
92 type viewData struct {
93 Score int
94@@ -98,10 +173,14 @@ type viewData struct {
95 }
96
97 func (g *Game) viewData() viewData {
98+ round := g.played + 1
99+ if round > g.rounds {
100+ round = g.rounds
101+ }
102 return viewData{
103 Score: g.score,
104 MaxScore: g.rounds * pointsPerRound,
105diff --git a/web/static/style.css b/web/static/style.css
106index bb7fa35426dc8a3540f9b9fedbd2498a879bf8fb..c6b52ecb7fb2e9fae26767594f6d8162e0d6a626 100644
107--- a/web/static/style.css
108+++ b/web/static/style.css
109@@ -1,13 +1,14 @@
110 :root {
111- --bg: #2e3440;
112- --bg-2: #3b4252;
113- --fg: #d8dee9;
114- --dim: #7b889a;
115- --accent: #a89984;
116- --green: #a3be5c;
117- --red: #bf616a;
118- --yellow: #e5c07b;
119- --orange: #d08a52;
120+ --bg: #23262e;
121+ --bg-2: #2c3038;
122+ --card: #2a2e37;
123+ --fg: #e6e9ef;
124+ --dim: #8b93a1;
125+ --accent: #e0a458;
126+ --green: #7fbf6a;
127+ --red: #d97b74;
128+ --yellow: #e0c058;
129+ --line: #3a3f4a;
130 }
131
132 * { box-sizing: border-box; }
133@@ -21,13 +22,18 @@ body {
134 align-items: center;
135 justify-content: center;
136 padding: 24px;
137- background: radial-gradient(120% 120% at 50% 0%, var(--bg-2), var(--bg) 60%);
138+ background: radial-gradient(120% 130% at 50% -10%, var(--bg-2), var(--bg) 62%);
139 color: var(--fg);
140- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
141+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
142 }
143
144 .game {
145- width: min(92vw, 560px);
146+ width: min(94vw, 540px);
147+ background: linear-gradient(180deg, var(--card), #242832);
148+ border: 1px solid var(--line);
149+ border-radius: 16px;
150+ padding: 26px 28px 30px;
151+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
152 }
153
154 .hud {
155@@ -39,18 +45,126 @@ body {
156
157 h1 {
158 margin: 0;
159- font-size: 1.9rem;
160- letter-spacing: 0.5px;
161+ font-size: 1.7rem;
162+ letter-spacing: 0.4px;
163 color: var(--accent);
164 }
165
166 .score {
167- font-size: 1.05rem;
168+ font-size: 1rem;
169 color: var(--dim);
170 white-space: nowrap;
171+ font-variant-numeric: tabular-nums;
172 }
173
174 .round {
175 color: var(--dim);
176- margin-top: 4px;
177+ margin: 2px 0 18px;
178+ font-size: 0.9rem;
179 }
180+
181+/* Guess form */
182+.guess { display: flex; flex-direction: column; gap: 14px; }
183+
184+.field { display: flex; flex-direction: column; gap: 6px; }
185+
186+.field label {
187+ font-size: 0.78rem;
188+ text-transform: uppercase;
189+ letter-spacing: 0.8px;
190+ color: var(--dim);
191+}
192+
193+.field input {
194+ background: #20242c;
195+ border: 1px solid var(--line);
196+ border-radius: 10px;
197+ color: var(--fg);
198+ font-size: 1rem;
199+ padding: 11px 13px;
200+ outline: none;
201+ transition: border-color 0.15s, box-shadow 0.15s;
202+}
203+
204+.field input:focus {
205+ border-color: var(--accent);
206+ box-shadow: 0 0 0 3px rgba(224, 164, 88, 0.18);
207+}
208+
209diff --git a/web/templates/home.html b/web/templates/game.html
210rename from web/templates/home.html
211rename to web/templates/game.html
212index 9fede01a1bb59be3a6fbb94f9751094ab11e5ee1..642bb80a3f80a926a77787fc9d5350259319d107 100644
213--- a/web/templates/home.html
214+++ b/web/templates/game.html
215@@ -1,7 +1,9 @@
216-{{define "content"}}
217+{{define "game"}}
218 <header class="hud">
219 <h1>Muse Guessr</h1>
220 <div class="score">Score: {{.Score}}/{{.MaxScore}}</div>
221 </header>
222 <p class="round">Round {{.Round}} of {{.TotalRounds}}</p>
223+
224+{{if .Done}}{{template "gameover" .}}{{else if .Result}}{{template "result" .}}{{else}}{{template "playing" .}}{{end}}
225 {{end}}
226diff --git a/web/templates/gameover.html b/web/templates/gameover.html
227new file mode 100644
228index 0000000000000000000000000000000000000000..202814264b79f4591523c586df1cbb385b76fc2c
229--- /dev/null
230+++ b/web/templates/gameover.html
231@@ -0,0 +1,7 @@
232+{{define "gameover"}}
233+<div class="gameover">
234+ <div class="final-score">{{.Score}}</div>
235+ <p class="final-sub">out of {{.MaxScore}} points</p>
236+ <button hx-post="/restart" hx-target="#game" hx-swap="innerHTML">Play again</button>
237+</div>
238+{{end}}
239diff --git a/web/templates/layout.html b/web/templates/layout.html
240index abe4071c6ee5e76a9082c1589459ba0258c6fb88..55934db19db2ad51cb3e3baaed10bf99c40cc616 100644
241--- a/web/templates/layout.html
242+++ b/web/templates/layout.html
243@@ -9,8 +9,8 @@
244 <script src="/static/htmx.min.js"></script>
245 </head>
246 <body>
247- <main class="game">
248- {{template "content" .}}
249+ <main class="game" id="game">
250+ {{template "game" .}}
251 </main>
252 </body>
253 </html>
254diff --git a/web/templates/playing.html b/web/templates/playing.html
255new file mode 100644
256index 0000000000000000000000000000000000000000..9466a0807baeb2cd4e6e26280731902c4ce118e5
257--- /dev/null
258+++ b/web/templates/playing.html
259@@ -0,0 +1,26 @@
260+{{define "playing"}}
261+<audio id="clip" src="/audio" autoplay></audio>
262+<script>
263+ (function () {
264+ var a = document.getElementById('clip');
265+ if (a) { a.play().catch(function () {}); }
266+ var t = document.getElementById('g-title');
267+ if (t) { t.focus(); }
268+ })();
269+</script>
270+<form class="guess" hx-post="/guess" hx-target="#game" hx-swap="innerHTML">
271+ <div class="field">
272+ <label for="g-title">Title</label>
273+ <input id="g-title" name="title" type="text" autocomplete="off">
274+ </div>
275+ <div class="field">
276+ <label for="g-album">Album</label>
277+ <input id="g-album" name="album" type="text" autocomplete="off">
278+ </div>
279+ <div class="field">
280+ <label for="g-artist">Artist</label>
281+ <input id="g-artist" name="artist" type="text" autocomplete="off">
282+ </div>
283+ <button type="submit">Submit guess</button>
284+</form>
285+{{end}}
286diff --git a/web/templates/result.html b/web/templates/result.html
287new file mode 100644
288index 0000000000000000000000000000000000000000..4bad4e8fd8d1d9c87fded25bb81fa2a5a376af18
289--- /dev/null
290+++ b/web/templates/result.html
291@@ -0,0 +1,24 @@
292+{{define "result"}}
293+<div class="result">
294+ <div class="points {{if eq .Result.Points 0}}zero{{end}}">+{{.Result.Points}}</div>
295+ <ul class="parts">
296+ {{range .Result.Parts}}<li>{{.}}</li>
297+ {{end}}
298+ </ul>
299+ <div class="fields">
300+ <span class="f {{if index .Result.FieldCorrect 0}}ok{{else if index .Result.FieldPartial 0}}part{{else}}miss{{end}}">Title</span>
301+ <span class="f {{if index .Result.FieldCorrect 1}}ok{{else if index .Result.FieldPartial 1}}part{{else}}miss{{end}}">Album</span>
302+ <span class="f {{if index .Result.FieldCorrect 2}}ok{{else if index .Result.FieldPartial 2}}part{{else}}miss{{end}}">Artist</span>
303+ </div>
304+ <div class="answer">
305+ <strong>{{.Answer.Title}}</strong> — {{.Answer.Artist}}
306+ {{if .Answer.Album}}<span class="album">{{.Answer.Album}}</span>{{end}}
307+ </div>
308+ {{if .Result.Hinted}}<div class="hinted">Hint used</div>{{end}}
309+</div>
310+{{if .Done}}
311+<button hx-post="/restart" hx-target="#game" hx-swap="innerHTML">Play again</button>
312+{{else}}
313+<button hx-get="/next" hx-target="#game" hx-swap="innerHTML">Next song</button>
314+{{end}}
315+{{end}}