diff --git a/cli/commands/play.go b/cli/commands/play.go index 6923f67..6dcf24d 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -133,7 +133,11 @@ func (gameState *GameState) initialize() { } // Build ruleset from settings - ruleset := rules.NewRulesetBuilder().WithSeed(gameState.Seed).WithParams(gameState.settings).Ruleset() + ruleset := rules.NewRulesetBuilder(). + WithSeed(gameState.Seed). + WithParams(gameState.settings). + WithSolo(len(gameState.URLs) < 2). + Ruleset() gameState.ruleset = ruleset // Initialize snake states as empty until we can ping the snake URLs diff --git a/ruleset.go b/ruleset.go index 1b28ec0..e69bbe8 100644 --- a/ruleset.go +++ b/ruleset.go @@ -79,6 +79,7 @@ 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 } // NewRulesetBuilder returns an instance of a builder for the Ruleset types. @@ -118,6 +119,12 @@ func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder { return rb } +// WithSolo sets whether the ruleset is a solo game. +func (rb *rulesetBuilder) WithSolo(value bool) *rulesetBuilder { + rb.solo = value + return rb +} + // Ruleset constructs a customised ruleset using the parameters passed to the builder. func (rb rulesetBuilder) Ruleset() PipelineRuleset { name, ok := rb.params[ParamGameType] @@ -125,20 +132,28 @@ func (rb rulesetBuilder) Ruleset() PipelineRuleset { name = GameTypeStandard } + var stages []string + if rb.solo { + stages = append(stages, StageGameOverSoloSnake) + } else { + stages = append(stages, StageGameOverStandard) + } + switch name { case GameTypeStandard: - return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...)) + stages = append(stages, standardRulesetStages[1:]...) case GameTypeConstrictor: - return rb.PipelineRuleset(name, NewPipeline(constrictorRulesetStages...)) + stages = append(stages, constrictorRulesetStages[1:]...) case GameTypeRoyale: - return rb.PipelineRuleset(name, NewPipeline(royaleRulesetStages...)) + stages = append(stages, royaleRulesetStages[1:]...) case GameTypeSolo: - return rb.PipelineRuleset(name, NewPipeline(soloRulesetStages...)) + stages = soloRulesetStages case GameTypeWrapped: - return rb.PipelineRuleset(name, NewPipeline(wrappedRulesetStages...)) + stages = append(stages, wrappedRulesetStages[1:]...) default: - return rb.PipelineRuleset(name, NewPipeline(standardRulesetStages...)) + stages = append(stages, standardRulesetStages[1:]...) } + return rb.PipelineRuleset(name, NewPipeline(stages...)) } // PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name. diff --git a/ruleset_test.go b/ruleset_test.go index 5eb6500..423d15b 100644 --- a/ruleset_test.go +++ b/ruleset_test.go @@ -1,6 +1,7 @@ package rules_test import ( + "fmt" "testing" "github.com/BattlesnakeOfficial/rules" @@ -138,6 +139,94 @@ func TestRulesetBuilder(t *testing.T) { } } +func TestRulesetBuilderGameOver(t *testing.T) { + settings := rules.Settings{ + RoyaleSettings: rules.RoyaleSettings{ + ShrinkEveryNTurns: 12, + }, + } + moves := []rules.SnakeMove{ + {ID: "1", Move: "up"}, + } + boardState := rules.NewBoardState(7, 7) + boardState.Snakes = append(boardState.Snakes, rules.Snake{ + ID: "1", + Body: []rules.Point{ + {X: 3, Y: 3}, + {X: 3, Y: 3}, + {X: 3, Y: 3}, + }, + Health: 100, + }) + + tests := []struct { + gameType string + solo bool + gameOver bool + }{ + { + gameType: rules.GameTypeStandard, + solo: false, + gameOver: true, + }, + { + gameType: rules.GameTypeConstrictor, + solo: false, + gameOver: true, + }, + { + gameType: rules.GameTypeRoyale, + solo: false, + gameOver: true, + }, + { + gameType: rules.GameTypeWrapped, + solo: false, + gameOver: true, + }, + { + gameType: rules.GameTypeSolo, + solo: false, + gameOver: false, + }, + { + gameType: rules.GameTypeStandard, + solo: true, + gameOver: false, + }, + { + gameType: rules.GameTypeConstrictor, + solo: true, + gameOver: false, + }, + { + gameType: rules.GameTypeRoyale, + solo: true, + gameOver: false, + }, + { + gameType: rules.GameTypeWrapped, + solo: true, + gameOver: false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%v_%v", test.gameType, test.solo), func(t *testing.T) { + rsb := rules.NewRulesetBuilder().WithParams(map[string]string{ + rules.ParamGameType: test.gameType, + }).WithSolo(test.solo) + + ruleset := rsb.Ruleset() + + gameOver, _, err := ruleset.Execute(boardState, settings, moves) + + require.NoError(t, err) + require.Equal(t, test.gameOver, gameOver) + }) + } +} + func TestStageFuncContract(t *testing.T) { //nolint:gosimple var stage rules.StageFunc