Add solo mode where players survive as long as possible while tracking score (food eaten). Single player games start with a shorter 3-second countdown vs 10 seconds for multiplayer, maintain exactly 1 food item for classic snake feel, and end when the player dies rather than when one player remains. - Add GameMode type (ModeMultiplayer/ModeSinglePlayer) and Score field - Filter single player games from "Join a Game" lobby list - Show "Ready?" and "Score: X" UI for single player mode - Hide invite link for single player games - Preserve game mode on rematch
162 lines
3.9 KiB
Go
162 lines
3.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/ryanhamamura/c4/snake"
|
|
"github.com/ryanhamamura/via/h"
|
|
)
|
|
|
|
func SnakeStatusBanner(sg *snake.SnakeGame, mySlot int, rematchClick h.H) h.H {
|
|
switch sg.Status {
|
|
case snake.StatusWaitingForPlayers:
|
|
if sg.Mode == snake.ModeSinglePlayer {
|
|
return h.Div(h.Class("alert bg-base-200 text-xl font-bold"),
|
|
h.Text("Ready?"),
|
|
)
|
|
}
|
|
return h.Div(h.Class("alert bg-base-200 text-xl font-bold"),
|
|
h.Text("Waiting for players..."),
|
|
)
|
|
|
|
case snake.StatusCountdown:
|
|
remaining := time.Until(sg.CountdownEnd)
|
|
secs := int(math.Ceil(remaining.Seconds()))
|
|
if secs < 0 {
|
|
secs = 0
|
|
}
|
|
return h.Div(h.Class("alert alert-info text-xl font-bold"),
|
|
h.Text(fmt.Sprintf("Starting in %d...", secs)),
|
|
)
|
|
|
|
case snake.StatusInProgress:
|
|
if sg.State != nil && mySlot >= 0 && mySlot < len(sg.State.Snakes) {
|
|
s := sg.State.Snakes[mySlot]
|
|
if s != nil && !s.Alive {
|
|
return h.Div(h.Class("alert alert-error text-xl font-bold"),
|
|
h.Text("You're out!"),
|
|
)
|
|
}
|
|
}
|
|
// Show score during single player gameplay
|
|
if sg.Mode == snake.ModeSinglePlayer {
|
|
return h.Div(h.Class("alert alert-success text-xl font-bold"),
|
|
h.Text(fmt.Sprintf("Score: %d", sg.Score)),
|
|
)
|
|
}
|
|
return h.Div(h.Class("alert alert-success text-xl font-bold"),
|
|
h.Text("Go!"),
|
|
)
|
|
|
|
case snake.StatusFinished:
|
|
var msg string
|
|
var class string
|
|
|
|
if sg.Mode == snake.ModeSinglePlayer {
|
|
msg = fmt.Sprintf("Game Over! Score: %d", sg.Score)
|
|
class = "alert alert-info text-xl font-bold"
|
|
} else if sg.Winner != nil {
|
|
if sg.Winner.Slot == mySlot {
|
|
msg = "You win!"
|
|
class = "alert alert-success text-xl font-bold"
|
|
} else {
|
|
msg = sg.Winner.Nickname + " wins!"
|
|
class = "alert alert-error text-xl font-bold"
|
|
}
|
|
} else {
|
|
msg = "It's a draw!"
|
|
class = "alert alert-warning text-xl font-bold"
|
|
}
|
|
|
|
content := []h.H{h.Class(class), h.Text(msg)}
|
|
|
|
if sg.RematchGameID != nil {
|
|
content = append(content,
|
|
h.A(
|
|
h.Class("btn btn-sm bg-white text-gray-800 border-none ml-4"),
|
|
h.Href("/snake/"+*sg.RematchGameID),
|
|
h.Text("Join Rematch"),
|
|
),
|
|
)
|
|
} else if rematchClick != nil {
|
|
content = append(content,
|
|
h.Button(
|
|
h.Class("btn btn-sm bg-white text-gray-800 border-none ml-4"),
|
|
h.Type("button"),
|
|
h.Text("Play again"),
|
|
rematchClick,
|
|
),
|
|
)
|
|
}
|
|
|
|
return h.Div(content...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SnakePlayerList(sg *snake.SnakeGame, mySlot int) h.H {
|
|
var items []h.H
|
|
|
|
for i, p := range sg.Players {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
|
|
colorHex := "#666"
|
|
if i < len(snake.SnakeColors) {
|
|
colorHex = snake.SnakeColors[i]
|
|
}
|
|
|
|
name := p.Nickname
|
|
if i == mySlot {
|
|
name += " (You)"
|
|
}
|
|
|
|
var statusEl h.H
|
|
if sg.Status == snake.StatusInProgress || sg.Status == snake.StatusFinished {
|
|
if sg.State != nil && i < len(sg.State.Snakes) {
|
|
s := sg.State.Snakes[i]
|
|
if s != nil {
|
|
if s.Alive {
|
|
length := len(s.Body)
|
|
statusEl = h.Span(h.Class("text-sm opacity-60"), h.Text(fmt.Sprintf(" (%d)", length)))
|
|
} else {
|
|
statusEl = h.Span(h.Class("text-sm opacity-40"), h.Text(" (dead)"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
chipStyle := fmt.Sprintf("width:16px;height:16px;border-radius:50%%;background:%s;display:inline-block;", colorHex)
|
|
|
|
items = append(items, h.Div(h.Class("flex items-center gap-2"),
|
|
h.Span(h.Attr("style", chipStyle)),
|
|
h.Span(h.Text(name)),
|
|
statusEl,
|
|
))
|
|
}
|
|
|
|
listAttrs := []h.H{h.Class("flex flex-wrap gap-4 mb-2")}
|
|
listAttrs = append(listAttrs, items...)
|
|
return h.Div(listAttrs...)
|
|
}
|
|
|
|
func SnakeInviteLink(gameID string) h.H {
|
|
fullURL := getBaseURL() + "/snake/" + gameID
|
|
return h.Div(h.Class("mt-4 text-center"),
|
|
h.P(h.Text("Share this link to invite players:")),
|
|
h.Div(h.Class("bg-base-200 p-4 rounded-lg font-mono break-all my-2"),
|
|
h.Text(fullURL),
|
|
),
|
|
h.Button(
|
|
h.Class("btn btn-sm mt-2"),
|
|
h.Type("button"),
|
|
h.Text("Copy Link"),
|
|
h.Attr("onclick", "navigator.clipboard.writeText('"+fullURL+"')"),
|
|
),
|
|
)
|
|
}
|