mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-14 00:16:56 +00:00
Collapse the three env parsers into one generic helper, the duplicated graceful-shutdown block into shutdownServer, and the score cap into pointsFor. Behaviour is unchanged.
205 lines
5.7 KiB
Go
205 lines
5.7 KiB
Go
package game
|
|
|
|
import (
|
|
"iter"
|
|
"time"
|
|
)
|
|
|
|
// PlayerID identifies a seat at the table. The engine never learns anything
|
|
// else about a player: no name, no connection, no session.
|
|
type PlayerID string
|
|
|
|
// Dictionary is the slice of the word store the engine needs.
|
|
//
|
|
// An interface rather than *dictionary.Store so the engine can be tested
|
|
// against a hand-built word graph with no SQLite involved, and so the bot's
|
|
// search can be exercised on boards small enough to reason about.
|
|
type Dictionary interface {
|
|
// Resolve maps a normalized word to its canonical form.
|
|
Resolve(word string) (string, bool)
|
|
// FirstSyllable and LastSyllable report the ends of a canonical word.
|
|
// The engine must use these rather than splitting the player's input:
|
|
// canonicalization can move either end.
|
|
FirstSyllable(word string) (string, bool)
|
|
LastSyllable(word string) (string, bool)
|
|
// WordsStartingWith iterates the words that may follow a syllable.
|
|
WordsStartingWith(syllable string) iter.Seq[string]
|
|
// OutDegree reports how many words start with a syllable.
|
|
OutDegree(syllable string) (int, error)
|
|
}
|
|
|
|
// RejectReason says why a submission was not accepted. Callers map these to
|
|
// player-facing messages, so each one has to be specific enough to act on.
|
|
type RejectReason int
|
|
|
|
const (
|
|
ReasonNone RejectReason = iota
|
|
ReasonNotYourTurn
|
|
ReasonTooFewSyllables
|
|
ReasonNotInDictionary
|
|
ReasonWrongLink
|
|
ReasonAlreadyUsed
|
|
ReasonTimeout
|
|
ReasonGameOver
|
|
|
|
// NumRejectReasons is one past the last defined reason.
|
|
//
|
|
// It exists so a transport layer can prove it maps every reason without
|
|
// guessing where the list ends. Deriving that from String returning
|
|
// "unknown" would be circular: a reason added without a String case would
|
|
// shrink the range being checked and the check would still pass.
|
|
NumRejectReasons
|
|
)
|
|
|
|
func (r RejectReason) String() string {
|
|
switch r {
|
|
case ReasonNone:
|
|
return "accepted"
|
|
case ReasonNotYourTurn:
|
|
return "not your turn"
|
|
case ReasonTooFewSyllables:
|
|
return "fewer than two syllables"
|
|
case ReasonNotInDictionary:
|
|
return "not in dictionary"
|
|
case ReasonWrongLink:
|
|
return "wrong first syllable"
|
|
case ReasonAlreadyUsed:
|
|
return "already used"
|
|
case ReasonTimeout:
|
|
return "turn expired"
|
|
case ReasonGameOver:
|
|
return "game already over"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// Move is one accepted word.
|
|
//
|
|
// Word is always the canonical spelling, which may differ from what the player
|
|
// typed. Typed records the raw input so the UI can show that a correction
|
|
// happened rather than silently replacing the player's text.
|
|
type Move struct {
|
|
Player PlayerID
|
|
Word string
|
|
Typed string
|
|
First string
|
|
Last string
|
|
Syllables int
|
|
Points int
|
|
// Parts is how Points was arrived at: one entry per non-zero term, summing
|
|
// exactly to Points even when the maxPointsPerWord cap trimmed them. See
|
|
// pointsFor.
|
|
Parts []PointPart
|
|
At time.Time
|
|
}
|
|
|
|
// PointKind names one term of a word's score. PointKindNone is the zero value
|
|
// and never appears in a PointPart — it exists only so the wire mapping in
|
|
// wsapi/convert.go has somewhere unreachable to send an unmapped value, the
|
|
// same shape RejectReason and EndReason already use.
|
|
type PointKind int
|
|
|
|
const (
|
|
PointKindNone PointKind = iota
|
|
PointKindBase
|
|
PointKindChain
|
|
PointKindSyllables
|
|
PointKindSpeed
|
|
PointKindRarity
|
|
|
|
// NumPointKinds is one past the last defined kind. See NumRejectReasons
|
|
// for why the count is a constant rather than a walk.
|
|
NumPointKinds
|
|
)
|
|
|
|
func (k PointKind) String() string {
|
|
switch k {
|
|
case PointKindNone:
|
|
return "none"
|
|
case PointKindBase:
|
|
return "base"
|
|
case PointKindChain:
|
|
return "chain"
|
|
case PointKindSyllables:
|
|
return "syllables"
|
|
case PointKindSpeed:
|
|
return "speed"
|
|
case PointKindRarity:
|
|
return "rarity"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// PointPart is one named term of a word's score: how many points it
|
|
// contributed, and which of pointsFor's terms it was.
|
|
type PointPart struct {
|
|
Kind PointKind
|
|
Value int
|
|
}
|
|
|
|
// EndReason says how a finished game ended.
|
|
type EndReason int
|
|
|
|
const (
|
|
EndNone EndReason = iota
|
|
EndTimeout
|
|
EndNoLegalMove
|
|
EndResigned
|
|
|
|
// NumEndReasons is one past the last defined end reason. See
|
|
// NumRejectReasons for why the count is a constant rather than a walk.
|
|
NumEndReasons
|
|
)
|
|
|
|
func (r EndReason) String() string {
|
|
switch r {
|
|
case EndNone:
|
|
return "in play"
|
|
case EndTimeout:
|
|
return "timeout"
|
|
case EndNoLegalMove:
|
|
return "no legal move"
|
|
case EndResigned:
|
|
return "resigned"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// Standing is one player's final placing.
|
|
//
|
|
// Rank 1 is whoever was still standing when the game ended; below them, a
|
|
// player eliminated later ranks above one eliminated earlier. Outlasting
|
|
// somebody is what beats them, so the ranking is finishing order and the score
|
|
// is reported beside it rather than deciding it.
|
|
type Standing struct {
|
|
Player PlayerID
|
|
Score int
|
|
Rank int
|
|
// Reason is how this player left the game, and EndNone for the winner,
|
|
// who did not.
|
|
Reason EndReason
|
|
}
|
|
|
|
// State is a snapshot for the transport layer to render. It copies everything
|
|
// it exposes, so a caller can hold it without touching engine state.
|
|
type State struct {
|
|
Current string
|
|
Turn PlayerID
|
|
Deadline time.Time
|
|
Scores map[PlayerID]int
|
|
// Alive says who is still in the game. A player who has been eliminated
|
|
// keeps their score and their place in the history; they simply no longer
|
|
// get a turn.
|
|
Alive map[PlayerID]bool
|
|
// Eliminated is the order players went out, first out first. A caller that
|
|
// remembers its length can tell exactly who went out on the last input.
|
|
Eliminated []PlayerID
|
|
ChainLength int
|
|
Over bool
|
|
Winner PlayerID
|
|
EndReason EndReason
|
|
// Standings is the final table, best first. Nil while the game is in
|
|
// play, when it would be meaningless for the same reason Winner is.
|
|
Standings []Standing
|
|
}
|