DEV-1761: New rules API (#118)

* DEV-1761: Clean up Ruleset interface (#115)

* remove legacy ruleset types and simplify ruleset interface

* remove unnecessary settings argument from Ruleset interface

* decouple rules.Settings from client API and store settings as strings

* DEV 1761: Add new BoardState and Point fields (#117)

* add Point.TTL, Point.Value, GameState and PointState to BoardState

* allow maps to access BoardState.GameState,PointState

* add PreUpdateBoard and refactor snail_mode with it

* fix bug where an extra turn was printed to the console

* fix formatting

* fix lint errors

Co-authored-by: JonathanArns <jonathan.arns@googlemail.com>
This commit is contained in:
Rob O'Dwyer 2022-10-28 16:49:49 -07:00 committed by GitHub
parent 639362ef46
commit 82e1999126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1349 additions and 1610 deletions

View file

@ -1,16 +1,15 @@
package rules
import (
"strconv"
)
type Ruleset interface {
// Returns the name of the ruleset, if applicable.
Name() string
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
IsGameOver(state *BoardState) (bool, error)
// Settings provides the game settings that are relevant to the ruleset.
// Returns the settings used by the ruleset.
Settings() Settings
// Processes the next turn of the ruleset, returning whether the game has ended, the next BoardState, or an error.
// For turn zero (initialization), moves will be left empty.
Execute(prevState *BoardState, moves []SnakeMove) (gameOver bool, nextState *BoardState, err error)
}
type SnakeMove struct {
@ -18,68 +17,12 @@ type SnakeMove struct {
Move string
}
// Settings contains all settings relevant to a game.
// It is used by game logic to take a previous game state and produce a next game state.
type Settings struct {
FoodSpawnChance int `json:"foodSpawnChance"`
MinimumFood int `json:"minimumFood"`
HazardDamagePerTurn int `json:"hazardDamagePerTurn"`
HazardMap string `json:"hazardMap"`
HazardMapAuthor string `json:"hazardMapAuthor"`
RoyaleSettings RoyaleSettings `json:"royale"`
SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility
rand Rand
seed int64
}
// Get a random number generator initialized based on the seed and current turn.
func (settings Settings) GetRand(turn int) Rand {
// Allow overriding the random generator for testing
if settings.rand != nil {
return settings.rand
}
if settings.seed != 0 {
return NewSeedRand(settings.seed + int64(turn))
}
// Default to global random number generator if neither seed or rand are set.
return GlobalRand
}
func (settings Settings) WithRand(rand Rand) Settings {
settings.rand = rand
return settings
}
func (settings Settings) Seed() int64 {
return settings.seed
}
func (settings Settings) WithSeed(seed int64) Settings {
settings.seed = seed
return settings
}
// RoyaleSettings contains settings that are specific to the "royale" game mode
type RoyaleSettings struct {
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
}
// SquadSettings contains settings that are specific to the "squad" game mode
type SquadSettings struct {
AllowBodyCollisions bool `json:"allowBodyCollisions"`
SharedElimination bool `json:"sharedElimination"`
SharedHealth bool `json:"sharedHealth"`
SharedLength bool `json:"sharedLength"`
}
type rulesetBuilder struct {
params map[string]string // game customisation parameters
seed int64 // used for random events in games
rand Rand // used for random number generation
solo bool // if true, only 1 alive snake is required to keep the game from ending
params map[string]string // game customisation parameters
seed int64 // used for random events in games
rand Rand // used for random number generation
solo bool // if true, only 1 alive snake is required to keep the game from ending
settings *Settings // used to set settings directly instead of via string params
}
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
@ -89,7 +32,7 @@ func NewRulesetBuilder() *rulesetBuilder {
}
}
// WithParams accepts a map of game parameters for customizing games.
// WithParams accepts a map of string parameters for customizing games.
//
// Parameters are copied. If called multiple times, parameters are merged such that:
// - existing keys in both maps get overwritten by the new ones
@ -125,13 +68,14 @@ func (rb *rulesetBuilder) WithSolo(value bool) *rulesetBuilder {
return rb
}
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
name, ok := rb.params[ParamGameType]
if !ok {
name = GameTypeStandard
}
// WithSettings sets the settings object for the ruleset directly.
func (rb *rulesetBuilder) WithSettings(settings Settings) *rulesetBuilder {
rb.settings = &settings
return rb
}
// NamedRuleset constructs a known ruleset by using name to look up a standard pipeline.
func (rb rulesetBuilder) NamedRuleset(name string) Ruleset {
var stages []string
if rb.solo {
stages = append(stages, StageGameOverSoloSnake)
@ -153,63 +97,28 @@ func (rb rulesetBuilder) Ruleset() PipelineRuleset {
case GameTypeWrapped:
stages = append(stages, wrappedRulesetStages[1:]...)
default:
name = GameTypeStandard
stages = append(stages, standardRulesetStages[1:]...)
}
return rb.PipelineRuleset(name, NewPipeline(stages...))
}
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
// It is intended to facilitate transitioning away from legacy Ruleset implementations to Pipeline
// implementations.
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRuleset {
// PipelineRuleset constructs a ruleset with the given name and pipeline using the parameters passed to the builder.
// This can be used to create custom rulesets.
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) Ruleset {
var settings Settings
if rb.settings != nil {
settings = *rb.settings
} else {
settings = NewSettings(rb.params).WithRand(rb.rand).WithSeed(rb.seed)
}
return &pipelineRuleset{
name: name,
pipeline: p,
settings: Settings{
FoodSpawnChance: paramsInt(rb.params, ParamFoodSpawnChance, 0),
MinimumFood: paramsInt(rb.params, ParamMinimumFood, 0),
HazardDamagePerTurn: paramsInt(rb.params, ParamHazardDamagePerTurn, 0),
HazardMap: rb.params[ParamHazardMap],
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
RoyaleSettings: RoyaleSettings{
ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0),
},
rand: rb.rand,
seed: rb.seed,
},
settings: settings,
}
}
// paramsBool returns the boolean value for the specified parameter.
// If the parameter doesn't exist, the default value will be returned.
// If the parameter does exist, but is not "true", false will be returned.
func paramsBool(params map[string]string, paramName string, defaultValue bool) bool {
if val, ok := params[paramName]; ok {
return val == "true"
}
return defaultValue
}
// paramsInt returns the int value for the specified parameter.
// If the parameter doesn't exist, the default value will be returned.
// If the parameter does exist, but is not a valid int, the default value will be returned.
func paramsInt(params map[string]string, paramName string, defaultValue int) int {
if val, ok := params[paramName]; ok {
i, err := strconv.Atoi(val)
if err == nil {
return i
}
}
return defaultValue
}
// PipelineRuleset groups the Pipeline and Ruleset methods.
// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code.
type PipelineRuleset interface {
Ruleset
Pipeline
}
type pipelineRuleset struct {
pipeline Pipeline
name string
@ -225,33 +134,10 @@ func (r pipelineRuleset) Settings() Settings {
func (r pipelineRuleset) Name() string { return r.name }
// impl Ruleset
// IMPORTANT: this implementation of IsGameOver deviates from the previous Ruleset implementations
// in that it checks if the *NEXT* state results in game over, not the previous state.
// This is due to the design of pipelines / stage functions not having a distinction between
// checking for game over and producing a next state.
func (r *pipelineRuleset) IsGameOver(b *BoardState) (bool, error) {
gameover, _, err := r.Execute(b, r.Settings(), nil) // checks if next state is game over
return gameover, err
func (r pipelineRuleset) Execute(bs *BoardState, sm []SnakeMove) (bool, *BoardState, error) {
return r.pipeline.Execute(bs, r.Settings(), sm)
}
// impl Ruleset
func (r pipelineRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
_, nextState, err := r.Execute(initialState, r.Settings(), nil)
return nextState, err
}
// impl Pipeline
func (r pipelineRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
return r.pipeline.Execute(bs, s, sm)
}
// impl Ruleset
func (r pipelineRuleset) CreateNextBoardState(bs *BoardState, sm []SnakeMove) (*BoardState, error) {
_, nextState, err := r.Execute(bs, r.Settings(), sm)
return nextState, err
}
// impl Pipeline
func (r pipelineRuleset) Err() error {
return r.pipeline.Err()
}