game

package
v0.0.0-...-223d396 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 19, 2023 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package game implements the main game controller and contains representations for a current game state. Every server instance is an instance of a single game coordinator, which is responsible for multiplexing over multiple different game instances with associated game state. Each game contains at least two players, each of which has an associated websocket connection.

To achieve this efficiently, a tiered flow of data is used. The GameCoordinator is a singleton and owns all game instances for a particular game server. Each game coordinator will always be handling zero or more games at once. Each game further contains at least two player instances at a time. Players are owned by a Game and handled by that instance alone.

Index

Constants

View Source
const (
	CommandGameCount     = "gcount"
	CommandQuestionCount = "count"
	CommandNewQuestion   = "ques"
	CommandAnswerAck     = "ansack"
	CommandQuestionOver  = "qend"
	CommandSeeResults    = "res"
	CommandFinalResults  = "fres"

	CommandNewPlayer    = "plr"
	CommandRemovePlayer = "rmplr"
	CommandDisconPlayer = "dcplr"
	CommandStartAck     = "sack"
	CommandQuestionAck  = "quack"
	CommandNewAnswer    = "nans"
)

WebSocket server message commands. Each websocket message is formatted as "<verb> <body>", where body is any valid JSON value, including literals (raw numbers or string literals) or composites (arrays and objects). This allows arbitrary Go data to be passed to the client. The client expects each field in the passed data to be lower case.

View Source
const (
	MessageIdenfity    = "ident"
	MessageAcknowledge = "ack"
	MessageAnswer      = "ans"

	MessageKick         = "kick"
	MessageCountdown    = "count"
	MessageStartGame    = "start"
	MessageNextQuestion = "next"
	MessageAnswerNow    = "sans"
	MessageQuestionEnd  = "time"
)

WebSocket client message commands. Client equivalent of server message commands. See documentation for server message commands for more details on format.

View Source
const (
	// MaxMessagesize is the maximum size of a client message in bytes.
	MaxMessagesize = 4096
	// PongInterval is the time between keepalive pings.
	PongInterval = time.Second * 15
	// PongTimeout is the maximum time allowed waiting for a keepalive pong.
	PongTimeout = time.Second * 10
)

Client mechanism constants.

View Source
const (
	MinGamePin = 1111111111
	MaxGamePin = 4294967295
)

Gameplay constants with no need for configuration.

View Source
const (
	// Game is waiting for the host to connect.
	GameHostWaiting = iota
	// Game is waiting for sufficient players.
	GameWaiting
	// Game is currently live.
	GameRunning
	// Game is dead and waiting to be reaped.
	GameDead
)

Possible game states.

View Source
const (
	MaxGameTime    = time.Minute * 45
	MinPlayers     = 3
	BasePoints     = 1000
	StreakBonus    = 100
	MaxStreakBonus = 500
)

Gameplay constants.

Variables

View Source
var (
	ErrorConnectionClosed = fmt.Errorf("client: connection closed")
	ErrorBadMessageType   = fmt.Errorf("client: binary message received")
	ErrorMalformedMessage = fmt.Errorf("client: invalid message syntax")
)

Client errors.

Functions

func FormatMessage

func FormatMessage(command string, data interface{}) string

FormatMessage returns the client-readable form of the message consisting of the verb "command" and arguments from data in JSON form.

func ParseMessage

func ParseMessage(msg string, data interface{}) (string, error)

ParseMessage parses a formatted websocket message, exctracting the attached JSON data and the command verb, returning the verb and unmarshalling the data into "data". Any encontered errors are returned and are usually fatal to the client.

func Score

func Score(correct bool, base, streak int, taken, allowed time.Duration) int64

Score returns the number of points that should be awarded for an answer. The rules which govern this are as follows:

  1. No points are awarded for an incorrect answer
  2. As the time taken to answer tends toward zero, points tend toward 2*base.
  3. 100 additional points are awarded for each correct answer in a row (streak), up to a maximum of 500 bonus streak points
  4. At half time, no bonus points are awarded. After half time, bonus points are negative up to -base (no points awarded at all, not counting bonuses)

This function does all calculations as floating points, but truncates the result in the end. If taken > allowed or taken < 0, Score panics (these must never be allowed to happen).

func StringMessage

func StringMessage(msg string) (verb string, data string, err error)

StringMessage parses a given message into a verb and a data string, returning any errors encountered while parsing.

Types

type Action

type Action interface {
	Perform(game *Game)
}

A Action is some action which can be sent to the game runner goroutine to perform an action on live game data, synchronised with the rest of the game. As these run on the runner thread, there is no need for any kind of locking. The only condition is that these actions MUST NEVER set game.sf to nil. Use game.GameEnding instead.

type AddPlayer

type AddPlayer struct {
	Nick string
	ID   chan int
}

AddPlayer allocates a new slot on the server for one player to join and returns an ID for this player, which is the index into the players array. This ID will then be used by the websocket to request to join the game.

func (AddPlayer) Perform

func (p AddPlayer) Perform(game *Game)

type Answer

type Answer struct {
	PlayerID, Number int
}

func (Answer) Perform

func (a Answer) Perform(game *Game)

type Client

type Client struct {
	Connected bool
	Ctx       context.Context
	Cancel    context.CancelFunc
	// contains filtered or unexported fields
}

A Client represents a connection from a generic client which is capable of both receiving and sending messages, handling pings and graceful disconnections.

func (Client) Close

func (c Client) Close()

Close gracefully tears down the connection with a generic teardown message for the client.

func (Client) CloseReason

func (c Client) CloseReason(why string)

CloseReason gracefully tears down the connection with the specified teardown message for the client.

func (Client) Open

func (c Client) Open()

Open sets up the websocket connection for reading as a client. Among other things, it sets up the read deadline and PING subsystem handlers.

It is NOT necessary to call Open to use the client, and is often undesirable if you are not planning on taking ownership of this connection yet.

func (Client) Read

func (c Client) Read(buf []byte) (int, error)

Read reads the next client message into buf, handling any formatting errors and errors. Errors returned are fatal to the application and cannot be recovered. The application read loop must terminate after the first error.

func (Client) ReadMessage

func (c Client) ReadMessage(data interface{}) (string, error)

ReadMessage reads a single message from the client and parses according to the message passing scheme. Data arguments are unmarshalled into data. The command verb and any possible errors are returned.

func (Client) ReadString

func (c Client) ReadString() (string, string, error)

ReadString reads a single message from the client and returns the verb, data and any errors encountered. Errors returned are usually fatal to the client.

func (Client) Send

func (c Client) Send(msg string)

Send transmits a message to the client through the websocket connection, blocking either until the message has sent or the connection terminates. This call CANNOT block forever.

func (Client) SendMessage

func (c Client) SendMessage(verb string, body interface{})

SendMessage formats a message using FormatMessage and sends to the client.

func (Client) Write

func (c Client) Write(msg []byte) (int, error)

Write sends a message to the client, always formatted as a string. This method may ONLY be called after Open() and always returns successfully. An unsuccessful call will close the websocket connection.

type ConnectHost

type ConnectHost struct {
	Conn *websocket.Conn
}

func (ConnectHost) Perform

func (c ConnectHost) Perform(game *Game)

type ConnectPlayer

type ConnectPlayer struct {
	Conn *websocket.Conn
	// contains filtered or unexported fields
}

ConnectPlayer initializes a player's connection, performs the startup handshake asynchronously. When this is complete, submits a new action to the game runner to update the player's state.

func (ConnectPlayer) Perform

func (c ConnectPlayer) Perform(game *Game)

type ConnectionUpdate

type ConnectionUpdate struct {
	PlayerID  int
	Connected bool
}

ConnectionUpdate submits a new connection state to the game loop.

Used to inform the game loop of a disconnection or re-connection, if appropriate. This does not remove the player from the player roster, but does make it possible for the player to re-connect and resume.

func (ConnectionUpdate) Perform

func (c ConnectionUpdate) Perform(game *Game)

type Coordinator

type Coordinator struct {
	// contains filtered or unexported fields
}

Coordinator is responsible for managing all ongoing games in order to receive and delegate incoming events.

func NewCoordinator

func NewCoordinator(maxGameTime time.Duration) Coordinator

NewCoordinator allocates and returns a new game coordinator with a blank initial game map.

func (*Coordinator) CreateGame

func (c *Coordinator) CreateGame(q quiz.Quiz) Game

CreateGame creates a new game blank game with no players waiting for a host connection, generating a random PIN by continually regenerating a random PIN until a free one is found. If the maximum concurrent games are running, blocks until one is available (which hopefully should occur *very* rarely).

func (Coordinator) GameExists

func (c Coordinator) GameExists(pin Pin) bool

GameExists checks if a game with the specified PIN is present in the game map.

func (Coordinator) GetGame

func (c Coordinator) GetGame(pin Pin) (Game, bool)

GetGame does a thread safe lookup in the game map for the specified PIN. Arguments returned are in the Game, ok form as in default maps.

type EndAnswer

type EndAnswer struct{}

func (EndAnswer) Perform

func (s EndAnswer) Perform(game *Game)

type EndGame

type EndGame struct {
	Reason string
	Clean  bool
}

EndGame shuts down the game runner, thereby terminating the current game.

If the shutdown is clean, the state is merely shifted to the GameEnding state, which allows for the final leaderboard to be shown. If the shutdown was NOT clean, the state is immediately set to nil and the game runner shuts down on the spot. This is usually used when the host disconnects.

func (EndGame) Perform

func (e EndGame) Perform(game *Game)

type Game

type Game struct {
	PIN Pin
	quiz.Quiz

	Action  chan Action
	Request chan chan State
	// contains filtered or unexported fields
}

Game is a single instance of a running game.

func NewGame

func NewGame(pin Pin, quiz quiz.Quiz, reaper chan Pin, maxGameTime time.Duration) Game

func (*Game) AcceptAnswers

func (game *Game) AcceptAnswers() StateFunc

AcceptAnswers is active when the game is idle accepting answers until

  1. Every player has answered
  2. The time runs out (host will notify us)
  3. The host manually skips the question (host will notify us)

func (*Game) GameEnding

func (game *Game) GameEnding() StateFunc

GameEnding is the state while we are showing the game end screen and results summary, after which the game runner can shut down. It accepts one more message, which is the host communicating that it is finished.

func (*Game) GameTerminate

func (game *Game) GameTerminate() StateFunc

GameTerminate terminates the game loop on next iteration.

func (*Game) Question

func (game *Game) Question() StateFunc

Question is active when the game is showing a question but BEFORE we are accepting answers. At the point where the countdown is finished, a snapshot of the current player state is taken such that new players do not disrupt the existing players' game.

func (*Game) Run

func (game *Game) Run()

Run enters into the main game loop for this game instance, listening for events and mutating internal game state. Run blocks the calling goroutine until the game has terminated, at which point it will inform the GameCoordinator through the reaper channel it was initialised with.

func (*Game) Sustain

func (game *Game) Sustain() StateFunc

Sustain keeps the game in the current state.

func (*Game) WaitForHost

func (game *Game) WaitForHost() StateFunc

WaitForHost is the state while the host is still in the process of connecting.

type Host

type Host struct {
	Client
}

The Host of a game is the client which receives incoming question texts and which handles time synchronisation for the rest of the game.

func (Host) Run

func (h Host) Run(ev chan Action)

type KickPlayer

type KickPlayer struct {
	ID int
}

KickPlayer disconnects and bans a player ID from this game. This means the player will be disconnected and will be prevented from rejoining.

func (KickPlayer) Perform

func (k KickPlayer) Perform(game *Game)

type Leaderboard

type Leaderboard []PlayerInfo

Leaderboard represents a sorted leaderboard of players, sorted by score. It is designed for use with sort.Interface.

func NewLeaderboard

func NewLeaderboard(plrs []Player) (l Leaderboard)

NewLeaderboard copies all players from plrs into a new leaderboard.

func (Leaderboard) Len

func (l Leaderboard) Len() int

func (Leaderboard) Less

func (l Leaderboard) Less(i, j int) bool

func (Leaderboard) Swap

func (l Leaderboard) Swap(i, j int)

type NextQuestion

type NextQuestion struct{}

func (NextQuestion) Perform

func (n NextQuestion) Perform(game *Game)

type Pin

type Pin uint32

Pin is the 10-digit PIN which can be used to join a game. It is constrained at an int32, as that is the smallest int type which provides enough space without allowing any higher inputs.

func (Pin) String

func (p Pin) String() string

String properly formats the game PIN such that any leading digits are displayed properly as zeroes.

func (Pin) Validate

func (p Pin) Validate() bool

Validate returns true if a game pin is within the required limits.

type Player

type Player struct {
	Client

	ID      int
	Nick    string
	Score   int64
	Correct int
	Streak  int

	Banned bool
	// contains filtered or unexported fields
}

A Player is one registered player as part of a running game. Each player is a wrapper around a single websocket connection combined with a runner thread, which handles user-focused events, such as receiving answers or returning feedback.

All exported fields may be accessed concurrently, but all unexported - especially "conn" - are exclusively held by the player runner, which translates messages from the client into actions for the game runner and handles ping timeouts. These must never be used, except by the player runner thread.

func (Player) Info

func (p Player) Info() PlayerInfo

Info extracts a player's information into a PlayerInfo struct, stuitable for websocket transmission to a client.

func (Player) Run

func (p Player) Run(ev chan Action)

Run is the game runner thread. It continually receives from the "conn" websocket connection until notified to stop by the connection-scoped context. It parses each message and takes appropriate action on each based on the verb passed.

type PlayerInfo

type PlayerInfo struct {
	ID      int    `json:"id"`
	Nick    string `json:"name"`
	Score   int64  `json:"score"`
	Correct int    `json:"correct"`
	Streak  int    `json:"streak"`
}

PlayerInfo is a message object, mirroring the PlayerData interface on the client. It should only be used for formatted transmission over a websocket.

type SendResults

type SendResults struct{}

func (SendResults) Perform

func (s SendResults) Perform(_ *Game)

type StartAnswer

type StartAnswer struct{}

func (StartAnswer) Perform

func (s StartAnswer) Perform(game *Game)

type StartGame

type StartGame struct {
	Count int
}

StartGame either begins a game or game countdown. If Count is > 0, the countdown state is activated. Else the game is immediately started.

func (StartGame) Perform

func (s StartGame) Perform(game *Game)

type State

type State struct {
	Status          Status
	Host            *Host
	Players         []Player
	CurrentQuestion int
	// contains filtered or unexported fields
}

State is the current state of an ongoing, running game instance. This is separated into a separate struct for ease of passing around combined game state to other objects, as well as to separate methods which act on the game itself and its state.

type StateFunc

type StateFunc func() StateFunc

StateFunc is a current state in the finite state machine of the game state. It returns a new StateFunc that will replace it after it returns and could simply be itself.

type Status

type Status int

Status is either waiting, running or dead. See documentation on GameWaiting, GameRunning or GameDead for more details.

Directories

Path Synopsis
Package quiz implements Gahoot quiz archive serialisation and parsing, as well as hashing and integrity checking.
Package quiz implements Gahoot quiz archive serialisation and parsing, as well as hashing and integrity checking.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL