999047f8cbdd0f58401b87d61ee62a7d63123294
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/main.go b/main.go
2index 46174d318846d1f677504639d5c05e10efe6448d..25f421d50aaf902ed31ec52b1262e7c702f6dc9c 100644
3--- a/main.go
4+++ b/main.go
5@@ -3,6 +3,7 @@ package main
6 import (
7 "flag"
8 "fmt"
9+ "net/http"
10 "os"
11
12 tea "github.com/charmbracelet/bubbletea"
13@@ -12,6 +13,8 @@ func main() {
14 dir := flag.String("dir", "/mnt/media/music", "directory to scan for music files")
15 blacklist := flag.String("blacklist", "blacklist", "path to blacklist file (one directory per line, fully excluded)")
16 rounds := flag.Int("rounds", 10, "number of rounds")
17+ web := flag.Bool("web", false, "run as a web server instead of the TUI")
18+ port := flag.Int("port", 8080, "port for the web server")
19 flag.Parse()
20
21 catalog, err := buildCatalog(*dir, *blacklist)
22@@ -28,6 +31,21 @@ func main() {
23 *rounds = len(catalog)
24 }
25
26+ if *web {
27+ srv, err := newServer(catalog, *rounds)
28+ if err != nil {
29+ fmt.Fprintf(os.Stderr, "error starting web server: %v\n", err)
30+ os.Exit(1)
31+ }
32+ addr := fmt.Sprintf(":%d", *port)
33+ fmt.Printf("Muse Guessr web server listening on http://localhost:%d\n", *port)
34+ if err := http.ListenAndServe(addr, srv.handler()); err != nil {
35+ fmt.Fprintf(os.Stderr, "server error: %v\n", err)
36+ os.Exit(1)
37+ }
38+ return
39+ }
40+
41 m := newModel(catalog, *rounds)
42 p := tea.NewProgram(m, tea.WithAltScreen())
43 if _, err := p.Run(); err != nil {
44diff --git a/server.go b/server.go
45new file mode 100644
46index 0000000000000000000000000000000000000000..6ae9a8fa90b243d4d22cbce699ec1973b85699c1
47--- /dev/null
48+++ b/server.go
49@@ -0,0 +1,146 @@
50+package main
51+
52+import (
53+ "embed"
54+ "html/template"
55+ "io/fs"
56+ "net/http"
57+ "strings"
58+ "sync"
59+)
60+
61+//go:embed web/templates/*.html
62+var templateFS embed.FS
63+
64+//go:embed web/static/*
65+var staticFS embed.FS
66+
67+// Game holds the mutable state of a single-player game session. All access is
68+// guarded by mu because HTTP handlers run concurrently.
69+type Game struct {
70+ mu sync.Mutex
71+ catalog []Song
72+ order []int
73+ pos int
74+ rounds int
75+ played int
76+ score int
77+ answer Song
78+ startSec int
79+ segment []byte // WAV bytes for the current round's clip
80+ hinted bool
81+ done bool
82+ result *RoundResult
83+}
84+
85+// RoundResult captures the outcome of a submitted guess, ready for rendering.
86+type RoundResult struct {
87+ Points int
88+ Parts []string
89+ Guesses []string
90+ FieldCorrect []bool
91+ FieldPartial []bool
92+ ResultMsg string
93+ Hinted bool
94+}
95+
96+func newGame(catalog []Song, rounds int) *Game {
97+ g := &Game{
98+ catalog: catalog,
99+ rounds: rounds,
100+ order: shuffled(len(catalog)),
101+ }
102+ g.beginRound()
103+ return g
104+}
105+
106+// beginRound selects the next song and resets the per-round state. Audio
107+// generation is wired in by a later stage.
108+func (g *Game) beginRound() {
109+ g.answer = g.nextSong()
110+ g.hinted = false
111+ g.result = nil
112+ g.startSec = -1
113+ g.segment = nil
114+}
115+
116+func (g *Game) nextSong() Song {
117+ if g.pos >= len(g.order) {
118+ g.pos = 0
119+ g.order = shuffled(len(g.catalog))
120+ }
121+ idx := g.order[g.pos]
122+ g.pos++
123+ return g.catalog[idx]
124+}
125+
126+// viewData is the render model passed to the templates.
127+type viewData struct {
128+ Score int
129+ MaxScore int
130+ Round int
131+ TotalRounds int
132+ Answer Song
133+ Hinted bool
134+ Done bool
135+ Result *RoundResult
136+}
137+
138+func (g *Game) viewData() viewData {
139+ return viewData{
140+ Score: g.score,
141+ MaxScore: g.rounds * pointsPerRound,
142+ Round: g.played + 1,
143+ TotalRounds: g.rounds,
144+ Answer: g.answer,
145+ Hinted: g.hinted,
146+ Done: g.done,
147+ Result: g.result,
148+ }
149diff --git a/web/static/htmx.min.js b/web/static/htmx.min.js
150new file mode 100644
151index 0000000000000000000000000000000000000000..de5f0f1a6173a84e0eacda6067c22d763d4dfebd
152--- /dev/null
153+++ b/web/static/htmx.min.js
154@@ -0,0 +1 @@
155diff --git a/web/static/style.css b/web/static/style.css
156new file mode 100644
157index 0000000000000000000000000000000000000000..bb7fa35426dc8a3540f9b9fedbd2498a879bf8fb
158--- /dev/null
159+++ b/web/static/style.css
160@@ -0,0 +1,56 @@
161+:root {
162+ --bg: #2e3440;
163+ --bg-2: #3b4252;
164+ --fg: #d8dee9;
165+ --dim: #7b889a;
166+ --accent: #a89984;
167+ --green: #a3be5c;
168+ --red: #bf616a;
169+ --yellow: #e5c07b;
170+ --orange: #d08a52;
171+}
172+
173+* { box-sizing: border-box; }
174+
175+html, body { height: 100%; }
176+
177+body {
178+ margin: 0;
179+ min-height: 100vh;
180+ display: flex;
181+ align-items: center;
182+ justify-content: center;
183+ padding: 24px;
184+ background: radial-gradient(120% 120% at 50% 0%, var(--bg-2), var(--bg) 60%);
185+ color: var(--fg);
186+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
187+}
188+
189+.game {
190+ width: min(92vw, 560px);
191+}
192+
193+.hud {
194+ display: flex;
195+ align-items: baseline;
196+ justify-content: space-between;
197+ gap: 12px;
198+}
199+
200+h1 {
201+ margin: 0;
202+ font-size: 1.9rem;
203+ letter-spacing: 0.5px;
204+ color: var(--accent);
205+}
206+
207+.score {
208+ font-size: 1.05rem;
209+ color: var(--dim);
210+ white-space: nowrap;
211+}
212+
213+.round {
214+ color: var(--dim);
215+ margin-top: 4px;
216+}
217diff --git a/web/templates/home.html b/web/templates/home.html
218new file mode 100644
219index 0000000000000000000000000000000000000000..9fede01a1bb59be3a6fbb94f9751094ab11e5ee1
220--- /dev/null
221+++ b/web/templates/home.html
222@@ -0,0 +1,7 @@
223+{{define "content"}}
224+<header class="hud">
225+ <h1>Muse Guessr</h1>
226+ <div class="score">Score: {{.Score}}/{{.MaxScore}}</div>
227+</header>
228+<p class="round">Round {{.Round}} of {{.TotalRounds}}</p>
229+{{end}}
230diff --git a/web/templates/layout.html b/web/templates/layout.html
231new file mode 100644
232index 0000000000000000000000000000000000000000..abe4071c6ee5e76a9082c1589459ba0258c6fb88
233--- /dev/null
234+++ b/web/templates/layout.html
235@@ -0,0 +1,17 @@
236+{{define "page"}}
237+<!DOCTYPE html>
238+<html lang="en">
239+<head>
240+ <meta charset="utf-8">
241+ <meta name="viewport" content="width=device-width, initial-scale=1">
242+ <title>Muse Guessr</title>
243+ <link rel="stylesheet" href="/static/style.css">
244+ <script src="/static/htmx.min.js"></script>
245+</head>
246+<body>
247+ <main class="game">
248+ {{template "content" .}}
249+ </main>
250+</body>
251+</html>
252+{{end}}