DEV-765 pipeline refactor (#64)

Refactor rulesets into smaller composable operations

In order to mix up the functionality from different rulesets like Solo, Royale, etc. the code in these classes needs to be broken up into small functions that can be composed in a pipeline to make a custom game mode.
This commit is contained in:
Torben 2022-03-16 16:58:05 -07:00 committed by GitHub
parent 5e629e9e93
commit 397d925110
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1475 additions and 222 deletions

View file

@ -1,5 +1,9 @@
package rules
import (
"strconv"
)
type RulesetError string
func (err RulesetError) Error() string { return string(err) }
@ -24,6 +28,7 @@ const (
EliminatedByOutOfHealth = "out-of-health"
EliminatedByHeadToHeadCollision = "head-collision"
EliminatedByOutOfBounds = "wall-collision"
EliminatedBySquad = "squad-eliminated"
// TODO - Error consts
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
@ -31,8 +36,147 @@ const (
ErrorNoRoomForFood = RulesetError("not enough space to place food")
ErrorNoMoveFound = RulesetError("move not provided for snake")
ErrorZeroLengthSnake = RulesetError("snake is length zero")
// Ruleset / game type names
GameTypeConstrictor = "constrictor"
GameTypeRoyale = "royale"
GameTypeSolo = "solo"
GameTypeSquad = "squad"
GameTypeStandard = "standard"
GameTypeWrapped = "wrapped"
// Game creation parameter names
ParamGameType = "name"
ParamFoodSpawnChance = "foodSpawnChance"
ParamMinimumFood = "minimumFood"
ParamHazardDamagePerTurn = "damagePerTurn"
ParamHazardMap = "hazardMap"
ParamHazardMapAuthor = "hazardMapAuthor"
ParamShrinkEveryNTurns = "shrinkEveryNTurns"
ParamAllowBodyCollisions = "allowBodyCollisions"
ParamSharedElimination = "sharedElimination"
ParamSharedHealth = "sharedHealth"
ParamSharedLength = "sharedLength"
)
type rulesetBuilder struct {
params map[string]string // game customisation parameters
seed int64 // used for random events in games
squads map[string]string // Snake ID -> Squad Name
}
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
func NewRulesetBuilder() *rulesetBuilder {
return &rulesetBuilder{
params: map[string]string{},
squads: map[string]string{},
}
}
// WithParams accepts a map of game 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
// - existing keys not present in the new map will be retained
// - non-existing keys only in the new map will be added
//
// Unrecognised parameters will be ignored and default values will be used.
// Invalid parameters (i.e. a non-numerical value where one is expected), will be ignored
// and default values will be used.
func (rb *rulesetBuilder) WithParams(params map[string]string) *rulesetBuilder {
for k, v := range params {
rb.params[k] = v
}
return rb
}
// WithSeed sets the seed used for randomisation by certain game modes.
func (rb *rulesetBuilder) WithSeed(seed int64) *rulesetBuilder {
rb.seed = seed
return rb
}
// AddSnakeToSquad adds the specified snake (by ID) to a squad with the given name.
// This configuration may be ignored by game modes if they do not support squads.
func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBuilder {
rb.squads[snakeID] = squadName
return rb
}
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
func (rb rulesetBuilder) Ruleset() Ruleset {
standardRuleset := &StandardRuleset{
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
HazardDamagePerTurn: paramsInt32(rb.params, ParamHazardDamagePerTurn, 0),
HazardMap: rb.params[ParamHazardMap],
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
}
name, ok := rb.params[ParamGameType]
if !ok {
return standardRuleset
}
switch name {
case GameTypeConstrictor:
return &ConstrictorRuleset{
StandardRuleset: *standardRuleset,
}
case GameTypeRoyale:
return &RoyaleRuleset{
StandardRuleset: *standardRuleset,
Seed: rb.seed,
ShrinkEveryNTurns: paramsInt32(rb.params, ParamShrinkEveryNTurns, 0),
}
case GameTypeSolo:
return &SoloRuleset{
StandardRuleset: *standardRuleset,
}
case GameTypeWrapped:
return &WrappedRuleset{
StandardRuleset: *standardRuleset,
}
case GameTypeSquad:
squadMap := map[string]string{}
for id, squad := range rb.squads {
squadMap[id] = squad
}
return &SquadRuleset{
StandardRuleset: *standardRuleset,
SquadMap: squadMap,
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
}
}
return standardRuleset
}
// 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
}
// paramsInt32 returns the int32 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 paramsInt32(params map[string]string, paramName string, defaultValue int32) int32 {
if val, ok := params[paramName]; ok {
i, err := strconv.Atoi(val)
if err == nil {
return int32(i)
}
}
return defaultValue
}
type Point struct {
X int32
Y int32
@ -57,4 +201,41 @@ type Ruleset interface {
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.
Settings() Settings
}
// 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 int32 `json:"foodSpawnChance"`
MinimumFood int32 `json:"minimumFood"`
HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"`
HazardMap string `json:"hazardMap"`
HazardMapAuthor string `json:"hazardMapAuthor"`
RoyaleSettings RoyaleSettings `json:"royale"`
SquadSettings SquadSettings `json:"squad"`
}
// RoyaleSettings contains settings that are specific to the "royale" game mode
type RoyaleSettings struct {
seed int64
ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"`
}
// SquadSettings contains settings that are specific to the "squad" game mode
type SquadSettings struct {
squadMap map[string]string
AllowBodyCollisions bool `json:"allowBodyCollisions"`
SharedElimination bool `json:"sharedElimination"`
SharedHealth bool `json:"sharedHealth"`
SharedLength bool `json:"sharedLength"`
}
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
// It is expected to modify the boardState directly.
// The return values are a boolean (to indicate whether the game has ended as a result of the stage)
// and an error if any errors occurred during the stage.
//
// Errors should be treated as meaning the stage failed and the board state is now invalid.
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)