Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
516 changes: 453 additions & 63 deletions actions.go

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion db/queries/infractions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ SELECT * FROM infractions
WHERE id = $1;

-- name: InfractionsByGame :many
SELECT id, game_id, game_card_id, accused, accuser, created, active, affirmed
SELECT id, game_id, game_card_id, accused, accuser, created, active, affirmed, transfer_pending
FROM infractions
WHERE game_id = $1
ORDER BY created DESC;
Expand All @@ -25,3 +25,22 @@ ORDER BY created DESC;
SELECT COUNT(*) FROM infractions
WHERE game_id = $1
AND active = TRUE;

-- name: InfractionTransferQueue :exec
UPDATE infractions
SET transfer_pending = TRUE
WHERE id = $1;

-- name: InfractionTransferResolve :exec
UPDATE infractions
SET transfer_pending = FALSE
WHERE id = $1;

-- name: InfractionTransferPending :one
SELECT id, game_card_id, accused, accuser
FROM infractions
WHERE game_id = $1
AND affirmed = TRUE
AND transfer_pending = TRUE
ORDER BY created DESC
LIMIT 1;
Comment thread
turkosaurus marked this conversation as resolved.
11 changes: 10 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ VALUES
(5, 'challenge', 'a points challenge is pending'),
(6, 'prompt', 'a prompt challenge is pending'),
(7, 'ending', 'deck exhausted, waiting on host to end the game'),
(8, 'end', 'game over')
(8, 'end', 'game over'),
(9, 'prompt-shred', 'spinner may shred a rule card after a succeeded prompt'),
(10, 'accusation-transfer', 'accuser may give a rule card to the accused after an affirmed accusation')
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name, description = EXCLUDED.description;

Expand Down Expand Up @@ -294,13 +296,20 @@ CREATE TABLE IF NOT EXISTS infractions (
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT TRUE, -- active until decided
affirmed BOOLEAN DEFAULT FALSE,
-- set when an affirmed accusation still owes a card transfer from the
-- accuser to the accused; cleared once they give a card or skip.
transfer_pending BOOLEAN DEFAULT FALSE,
-- points changes are recorded in point_changes, not here
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
FOREIGN KEY (game_card_id) REFERENCES game_cards(id) ON DELETE CASCADE,
FOREIGN KEY (accused) REFERENCES players(id) ON DELETE CASCADE,
FOREIGN KEY (accuser) REFERENCES players(id) ON DELETE CASCADE
);

-- add transfer_pending on any live database, since CREATE TABLE above is a
-- no-op once infractions exists. idempotent: a no-op once the column is there.
ALTER TABLE infractions ADD COLUMN IF NOT EXISTS transfer_pending BOOLEAN DEFAULT FALSE;

CREATE UNLOGGED TABLE IF NOT EXISTS game_cache (
game_id VARCHAR(6) PRIMARY KEY,
value JSONB,
Expand Down
57 changes: 55 additions & 2 deletions db/sqlc/infractions.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions db/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 55 additions & 2 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,13 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
// still serve the log so the final events and the history
// modal work, but with 286 so the feed stops polling
renderEvents(w, r, gameID, stopPolling)
case "status", "table", "infraction", "prompt":
case "status", "table", "infraction", "prompt", "promptshred", "transfer":
w.WriteHeader(stopPolling)
default:
http.Error(w, "game over", http.StatusGone)
}
return
case stateEnding, stateChallenge, statePrompt, statePending, stateTurn, stateReady, stateInviting, stateCreated: // in progress (7 = deck spent, host to end)
case stateEnding, stateChallenge, statePrompt, statePending, stateTurn, stateReady, stateInviting, stateCreated, statePromptShred, stateAccusationTransfer: // in progress
switch topic {
case "players":
filepath := path.Join("static", "html", "tmpl.players.html")
Expand Down Expand Up @@ -260,6 +260,15 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
return
case "promptshred":
// the spinner's chooser after a succeeded prompt; the fragment
// renders only for the turn player, so it's empty for everyone else.
filepath := path.Join("static", "html", "tmpl.prompt_shred.html")
if err := renderTemplate(r.Context(), w, filepath, state); err != nil {
log.Error("render template", "error", err, "template", filepath)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
return
case "prompt":
// host-only poll: while a prompt challenge is live, hand the host
// the spinner's name, the prompt text, and how many seconds have
Expand Down Expand Up @@ -343,6 +352,50 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusNoContent)
return
case "transfer":
// accuser-only poll: while an affirmed accusation owes a card,
// hand the accuser the accused's name and the accuser's own rule
// cards so their "give a card" popup can open.
if state.Game.StateID != stateAccusationTransfer {
w.WriteHeader(http.StatusNoContent)
return
}
inf, err := queries.InfractionTransferPending(r.Context(), gameID)
if err != nil {
log.Debug("no transfer-pending infraction for poll",
"game_id", gameID, "error", err)
w.WriteHeader(http.StatusNoContent)
return
}
if int32(state.CallerID) != inf.Accuser {
w.WriteHeader(http.StatusNoContent)
return
}
accusedName := ""
for _, p := range state.Players {
if p.PlayerID == inf.Accused {
accusedName = p.Name
break
}
}
type transferCard struct {
ID int32 `json:"id"`
Content string `json:"content"`
}
cards := []transferCard{}
for _, c := range state.CardsPlayers {
if c.PlayerID.Int32 == int32(state.CallerID) && c.Type == "rule" {
content, _ := c.Content.(string)
cards = append(cards, transferCard{ID: c.ID, Content: content})
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"infraction_id": inf.ID,
"accused": accusedName,
"cards": cards,
})
return
default:
log.Warn(ErrTopicInvalid.Error())
http.Error(w, ErrTopicInvalid.Error(), http.StatusBadRequest)
Expand Down
20 changes: 11 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ const (

// game.state_id values, mirroring the game_states rows in db/schema.sql.
const (
stateCreated = 0 // game created, no members joined
stateInviting = 1 // at least one player has joined
stateReady = 2 // joining closed, ready to start (or paused)
stateTurn = 3 // a player is mid-turn
statePending = 4 // a rule modifier choice is pending
stateChallenge = 5 // a points challenge is pending
statePrompt = 6 // a prompt challenge is pending
stateEnding = 7 // deck spent, waiting on host to end
stateOver = 8 // game over
stateCreated = 0 // game created, no members joined
stateInviting = 1 // at least one player has joined
stateReady = 2 // joining closed, ready to start (or paused)
stateTurn = 3 // a player is mid-turn
statePending = 4 // a rule modifier choice is pending
stateChallenge = 5 // a points challenge is pending
statePrompt = 6 // a prompt challenge is pending
stateEnding = 7 // deck spent, waiting on host to end
stateOver = 8 // game over
statePromptShred = 9 // successful prompts pause to allow shredding a card
stateAccusationTransfer = 10 // affirmed accusations pause to allow transferring a card to the accused
)

//go:embed db/schema.sql
Expand Down
Loading
Loading