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:
parent
5e629e9e93
commit
397d925110
25 changed files with 1475 additions and 222 deletions
|
|
@ -57,11 +57,22 @@ var MinimumFood int32
|
||||||
var HazardDamagePerTurn int32
|
var HazardDamagePerTurn int32
|
||||||
var ShrinkEveryNTurns int32
|
var ShrinkEveryNTurns int32
|
||||||
|
|
||||||
|
var defaultConfig = map[string]string{
|
||||||
|
// default to standard ruleset
|
||||||
|
rules.ParamGameType: "standard",
|
||||||
|
// squad settings default to true (not zero value)
|
||||||
|
rules.ParamSharedElimination: "true",
|
||||||
|
rules.ParamSharedHealth: "true",
|
||||||
|
rules.ParamSharedLength: "true",
|
||||||
|
rules.ParamAllowBodyCollisions: "true",
|
||||||
|
}
|
||||||
|
|
||||||
var playCmd = &cobra.Command{
|
var playCmd = &cobra.Command{
|
||||||
Use: "play",
|
Use: "play",
|
||||||
Short: "Play a game of Battlesnake locally.",
|
Short: "Play a game of Battlesnake locally.",
|
||||||
Long: "Play a game of Battlesnake locally.",
|
Long: "Play a game of Battlesnake locally.",
|
||||||
Run: run,
|
Run: run,
|
||||||
|
PreRun: playPreRun,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -90,6 +101,10 @@ func init() {
|
||||||
playCmd.Flags().SortFlags = false
|
playCmd.Flags().SortFlags = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func playPreRun(cmd *cobra.Command, args []string) {
|
||||||
|
initialiseGameConfig()
|
||||||
|
}
|
||||||
|
|
||||||
var run = func(cmd *cobra.Command, args []string) {
|
var run = func(cmd *cobra.Command, args []string) {
|
||||||
rand.Seed(Seed)
|
rand.Seed(Seed)
|
||||||
|
|
||||||
|
|
@ -174,54 +189,23 @@ var run = func(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initialiseGameConfig() {
|
||||||
|
defaultConfig[rules.ParamGameType] = GameType
|
||||||
|
defaultConfig[rules.ParamFoodSpawnChance] = fmt.Sprint(FoodSpawnChance)
|
||||||
|
defaultConfig[rules.ParamMinimumFood] = fmt.Sprint(MinimumFood)
|
||||||
|
defaultConfig[rules.ParamHazardDamagePerTurn] = fmt.Sprint(HazardDamagePerTurn)
|
||||||
|
defaultConfig[rules.ParamShrinkEveryNTurns] = fmt.Sprint(ShrinkEveryNTurns)
|
||||||
|
}
|
||||||
|
|
||||||
func getRuleset(seed int64, snakeStates map[string]SnakeState) rules.Ruleset {
|
func getRuleset(seed int64, snakeStates map[string]SnakeState) rules.Ruleset {
|
||||||
var ruleset rules.Ruleset
|
rb := rules.NewRulesetBuilder().WithSeed(seed).WithParams(defaultConfig)
|
||||||
var royale rules.RoyaleRuleset
|
|
||||||
|
|
||||||
standard := rules.StandardRuleset{
|
for _, s := range snakeStates {
|
||||||
FoodSpawnChance: FoodSpawnChance,
|
rb.AddSnakeToSquad(s.ID, s.Squad)
|
||||||
MinimumFood: MinimumFood,
|
|
||||||
HazardDamagePerTurn: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch GameType {
|
return rb.Ruleset()
|
||||||
case "royale":
|
|
||||||
standard.HazardDamagePerTurn = HazardDamagePerTurn
|
|
||||||
royale = rules.RoyaleRuleset{
|
|
||||||
StandardRuleset: standard,
|
|
||||||
Seed: seed,
|
|
||||||
ShrinkEveryNTurns: ShrinkEveryNTurns,
|
|
||||||
}
|
|
||||||
ruleset = &royale
|
|
||||||
case "squad":
|
|
||||||
squadMap := map[string]string{}
|
|
||||||
for _, snakeState := range snakeStates {
|
|
||||||
squadMap[snakeState.ID] = snakeState.Squad
|
|
||||||
}
|
|
||||||
ruleset = &rules.SquadRuleset{
|
|
||||||
StandardRuleset: standard,
|
|
||||||
SquadMap: squadMap,
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SharedElimination: true,
|
|
||||||
SharedHealth: true,
|
|
||||||
SharedLength: true,
|
|
||||||
}
|
|
||||||
case "solo":
|
|
||||||
ruleset = &rules.SoloRuleset{
|
|
||||||
StandardRuleset: standard,
|
|
||||||
}
|
|
||||||
case "wrapped":
|
|
||||||
ruleset = &rules.WrappedRuleset{
|
|
||||||
StandardRuleset: standard,
|
|
||||||
}
|
|
||||||
case "constrictor":
|
|
||||||
ruleset = &rules.ConstrictorRuleset{
|
|
||||||
StandardRuleset: standard,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ruleset = &standard
|
|
||||||
}
|
|
||||||
return ruleset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]SnakeState) *rules.BoardState {
|
func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]SnakeState) *rules.BoardState {
|
||||||
|
|
@ -384,20 +368,7 @@ func createClientGame(ruleset rules.Ruleset) client.Game {
|
||||||
return client.Game{ID: GameId, Timeout: Timeout, Ruleset: client.Ruleset{
|
return client.Game{ID: GameId, Timeout: Timeout, Ruleset: client.Ruleset{
|
||||||
Name: ruleset.Name(),
|
Name: ruleset.Name(),
|
||||||
Version: "cli", // TODO: Use GitHub Release Version
|
Version: "cli", // TODO: Use GitHub Release Version
|
||||||
Settings: client.RulesetSettings{
|
Settings: ruleset.Settings(),
|
||||||
HazardDamagePerTurn: HazardDamagePerTurn,
|
|
||||||
FoodSpawnChance: FoodSpawnChance,
|
|
||||||
MinimumFood: MinimumFood,
|
|
||||||
RoyaleSettings: client.RoyaleSettings{
|
|
||||||
ShrinkEveryNTurns: ShrinkEveryNTurns,
|
|
||||||
},
|
|
||||||
SquadSettings: client.SquadSettings{
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SharedElimination: true,
|
|
||||||
SharedHealth: true,
|
|
||||||
SharedLength: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
|
@ -35,8 +36,69 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||||
s1State.ID: s1State,
|
s1State.ID: s1State,
|
||||||
s2State.ID: s2State,
|
s2State.ID: s2State,
|
||||||
}
|
}
|
||||||
snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, &rules.StandardRuleset{})
|
initialiseGameConfig() // initialise default config
|
||||||
|
snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, getRuleset(0, snakeStates))
|
||||||
requestBody := serialiseSnakeRequest(snakeRequest)
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
|
|
||||||
test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
test.RequireJSONMatchesFixture(t, "testdata/snake_request_body.json", string(requestBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSettingsRequestSerialization(t *testing.T) {
|
||||||
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
|
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||||
|
state := &rules.BoardState{
|
||||||
|
Height: 11,
|
||||||
|
Width: 11,
|
||||||
|
Snakes: []rules.Snake{s1, s2},
|
||||||
|
}
|
||||||
|
s1State := SnakeState{
|
||||||
|
ID: "one",
|
||||||
|
Name: "ONE",
|
||||||
|
URL: "http://example1.com",
|
||||||
|
Head: "safe",
|
||||||
|
Tail: "curled",
|
||||||
|
Color: "#123456",
|
||||||
|
}
|
||||||
|
s2State := SnakeState{
|
||||||
|
ID: "two",
|
||||||
|
Name: "TWO",
|
||||||
|
URL: "http://example2.com",
|
||||||
|
Head: "silly",
|
||||||
|
Tail: "bolt",
|
||||||
|
Color: "#654321",
|
||||||
|
}
|
||||||
|
snakeStates := map[string]SnakeState{s1State.ID: s1State, s2State.ID: s2State}
|
||||||
|
|
||||||
|
rsb := rules.NewRulesetBuilder().
|
||||||
|
WithParams(map[string]string{
|
||||||
|
// standard
|
||||||
|
rules.ParamFoodSpawnChance: "11",
|
||||||
|
rules.ParamMinimumFood: "7",
|
||||||
|
rules.ParamHazardDamagePerTurn: "19",
|
||||||
|
rules.ParamHazardMap: "hz_spiral",
|
||||||
|
rules.ParamHazardMapAuthor: "altersaddle",
|
||||||
|
// squad
|
||||||
|
rules.ParamAllowBodyCollisions: "true",
|
||||||
|
rules.ParamSharedElimination: "false",
|
||||||
|
rules.ParamSharedHealth: "true",
|
||||||
|
rules.ParamSharedLength: "false",
|
||||||
|
// royale
|
||||||
|
rules.ParamShrinkEveryNTurns: "17",
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, gt := range []string{
|
||||||
|
rules.GameTypeStandard, rules.GameTypeRoyale, rules.GameTypeSolo,
|
||||||
|
rules.GameTypeWrapped, rules.GameTypeSquad, rules.GameTypeConstrictor,
|
||||||
|
} {
|
||||||
|
t.Run(gt, func(t *testing.T) {
|
||||||
|
// apply game type
|
||||||
|
ruleset := rsb.WithParams(map[string]string{rules.ParamGameType: gt}).Ruleset()
|
||||||
|
|
||||||
|
snakeRequest := getIndividualBoardStateForSnake(state, s1State, snakeStates, ruleset)
|
||||||
|
requestBody := serialiseSnakeRequest(snakeRequest)
|
||||||
|
t.Log(string(requestBody))
|
||||||
|
|
||||||
|
test.RequireJSONMatchesFixture(t, fmt.Sprintf("testdata/snake_request_body_%s.json", gt), string(requestBody))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
10
cli/commands/testdata/snake_request_body.json
vendored
10
cli/commands/testdata/snake_request_body.json
vendored
|
|
@ -11,13 +11,13 @@
|
||||||
"hazardMap": "",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 25
|
"shrinkEveryNTurns": 0
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": true,
|
"allowBodyCollisions": false,
|
||||||
"sharedElimination": true,
|
"sharedElimination": false,
|
||||||
"sharedHealth": true,
|
"sharedHealth": false,
|
||||||
"sharedLength": true
|
"sharedLength": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
108
cli/commands/testdata/snake_request_body_constrictor.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_constrictor.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "constrictor",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 0
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": false,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": false,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cli/commands/testdata/snake_request_body_royale.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_royale.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "royale",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 17
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": false,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": false,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cli/commands/testdata/snake_request_body_solo.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_solo.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "solo",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 0
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": false,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": false,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cli/commands/testdata/snake_request_body_squad.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_squad.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "squad",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 0
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": true,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": true,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cli/commands/testdata/snake_request_body_standard.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_standard.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "standard",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 0
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": false,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": false,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cli/commands/testdata/snake_request_body_wrapped.json
vendored
Normal file
108
cli/commands/testdata/snake_request_body_wrapped.json
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"game": {
|
||||||
|
"id": "",
|
||||||
|
"ruleset": {
|
||||||
|
"name": "wrapped",
|
||||||
|
"version": "cli",
|
||||||
|
"settings": {
|
||||||
|
"foodSpawnChance": 11,
|
||||||
|
"minimumFood": 7,
|
||||||
|
"hazardDamagePerTurn": 19,
|
||||||
|
"hazardMap": "hz_spiral",
|
||||||
|
"hazardMapAuthor": "altersaddle",
|
||||||
|
"royale": {
|
||||||
|
"shrinkEveryNTurns": 0
|
||||||
|
},
|
||||||
|
"squad": {
|
||||||
|
"allowBodyCollisions": false,
|
||||||
|
"sharedElimination": false,
|
||||||
|
"sharedHealth": false,
|
||||||
|
"sharedLength": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 500,
|
||||||
|
"source": ""
|
||||||
|
},
|
||||||
|
"turn": 0,
|
||||||
|
"board": {
|
||||||
|
"height": 11,
|
||||||
|
"width": 11,
|
||||||
|
"snakes": [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"name": "TWO",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 4,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#654321",
|
||||||
|
"head": "silly",
|
||||||
|
"tail": "bolt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"food": [],
|
||||||
|
"hazards": []
|
||||||
|
},
|
||||||
|
"you": {
|
||||||
|
"id": "one",
|
||||||
|
"name": "ONE",
|
||||||
|
"latency": "0",
|
||||||
|
"health": 0,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"x": 3,
|
||||||
|
"y": 3
|
||||||
|
},
|
||||||
|
"length": 1,
|
||||||
|
"shout": "",
|
||||||
|
"squad": "",
|
||||||
|
"customizations": {
|
||||||
|
"color": "#123456",
|
||||||
|
"head": "safe",
|
||||||
|
"tail": "curled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
|
import "github.com/BattlesnakeOfficial/rules"
|
||||||
|
|
||||||
func exampleSnakeRequest() SnakeRequest {
|
func exampleSnakeRequest() SnakeRequest {
|
||||||
return SnakeRequest{
|
return SnakeRequest{
|
||||||
Game: Game{
|
Game: Game{
|
||||||
|
|
@ -72,18 +74,18 @@ func exampleSnakeRequest() SnakeRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var exampleRulesetSettings = RulesetSettings{
|
var exampleRulesetSettings = rules.Settings{
|
||||||
FoodSpawnChance: 10,
|
FoodSpawnChance: 10,
|
||||||
MinimumFood: 20,
|
MinimumFood: 20,
|
||||||
HazardDamagePerTurn: 30,
|
HazardDamagePerTurn: 30,
|
||||||
HazardMap: "hz_spiral",
|
HazardMap: "hz_spiral",
|
||||||
HazardMapAuthor: "altersaddle",
|
HazardMapAuthor: "altersaddle",
|
||||||
|
|
||||||
RoyaleSettings: RoyaleSettings{
|
RoyaleSettings: rules.RoyaleSettings{
|
||||||
ShrinkEveryNTurns: 40,
|
ShrinkEveryNTurns: 40,
|
||||||
},
|
},
|
||||||
|
|
||||||
SquadSettings: SquadSettings{
|
SquadSettings: rules.SquadSettings{
|
||||||
AllowBodyCollisions: true,
|
AllowBodyCollisions: true,
|
||||||
SharedElimination: true,
|
SharedElimination: true,
|
||||||
SharedHealth: true,
|
SharedHealth: true,
|
||||||
|
|
|
||||||
|
|
@ -50,29 +50,17 @@ type Customizations struct {
|
||||||
type Ruleset struct {
|
type Ruleset struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Settings RulesetSettings `json:"settings"`
|
Settings rules.Settings `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RulesetSettings struct {
|
// RulesetSettings is deprecated: use rules.Settings instead
|
||||||
FoodSpawnChance int32 `json:"foodSpawnChance"`
|
type RulesetSettings rules.Settings
|
||||||
MinimumFood int32 `json:"minimumFood"`
|
|
||||||
HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"`
|
|
||||||
HazardMap string `json:"hazardMap"`
|
|
||||||
HazardMapAuthor string `json:"hazardMapAuthor"`
|
|
||||||
RoyaleSettings RoyaleSettings `json:"royale"`
|
|
||||||
SquadSettings SquadSettings `json:"squad"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoyaleSettings struct {
|
// RoyaleSettings is deprecated: use rules.RoyaleSettings instead
|
||||||
ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"`
|
type RoyaleSettings rules.RoyaleSettings
|
||||||
}
|
|
||||||
|
|
||||||
type SquadSettings struct {
|
// SquadSettings is deprecated: use rules.SquadSettings instead
|
||||||
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
type SquadSettings rules.SquadSettings
|
||||||
SharedElimination bool `json:"sharedElimination"`
|
|
||||||
SharedHealth bool `json:"sharedHealth"`
|
|
||||||
SharedLength bool `json:"sharedLength"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Coord represents a point on the board
|
// Coord represents a point on the board
|
||||||
type Coord struct {
|
type Coord struct {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
"github.com/BattlesnakeOfficial/rules/test"
|
"github.com/BattlesnakeOfficial/rules/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -18,7 +19,7 @@ func TestBuildSnakeRequestJSON(t *testing.T) {
|
||||||
|
|
||||||
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
||||||
snakeRequest := exampleSnakeRequest()
|
snakeRequest := exampleSnakeRequest()
|
||||||
snakeRequest.Game.Ruleset.Settings = RulesetSettings{}
|
snakeRequest.Game.Ruleset.Settings = rules.Settings{}
|
||||||
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,32 @@ type ConstrictorRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) Name() string { return "constrictor" }
|
func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor }
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
||||||
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
|
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
newBoardState := initialBoardState.Clone()
|
|
||||||
err = r.applyConstrictorRules(newBoardState)
|
r.removeFood(initialBoardState)
|
||||||
|
|
||||||
|
err = r.applyConstrictorRules(initialBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newBoardState, nil
|
return initialBoardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
|
|
||||||
nextState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
nextState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.removeFood(nextState)
|
||||||
|
|
||||||
err = r.applyConstrictorRules(nextState)
|
err = r.applyConstrictorRules(nextState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -35,10 +38,23 @@ func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves [
|
||||||
return nextState, nil
|
return nextState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) applyConstrictorRules(b *BoardState) error {
|
func (r *ConstrictorRuleset) removeFood(b *BoardState) {
|
||||||
|
_, _ = r.callStageFunc(RemoveFoodConstrictor, b, []SnakeMove{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
// Remove all food from the board
|
// Remove all food from the board
|
||||||
b.Food = []Point{}
|
b.Food = []Point{}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ConstrictorRuleset) applyConstrictorRules(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(GrowSnakesConstrictor, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GrowSnakesConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
// Set all snakes to max health and ensure they grow next turn
|
// Set all snakes to max health and ensure they grow next turn
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
b.Snakes[i].Health = SnakeMaxHealth
|
b.Snakes[i].Health = SnakeMaxHealth
|
||||||
|
|
@ -46,9 +62,9 @@ func (r *ConstrictorRuleset) applyConstrictorRules(b *BoardState) error {
|
||||||
tail := b.Snakes[i].Body[len(b.Snakes[i].Body)-1]
|
tail := b.Snakes[i].Body[len(b.Snakes[i].Body)-1]
|
||||||
subTail := b.Snakes[i].Body[len(b.Snakes[i].Body)-2]
|
subTail := b.Snakes[i].Body[len(b.Snakes[i].Body)-2]
|
||||||
if tail != subTail {
|
if tail != subTail {
|
||||||
r.growSnake(&b.Snakes[i])
|
growSnake(&b.Snakes[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
royale.go
42
royale.go
|
|
@ -13,7 +13,7 @@ type RoyaleRuleset struct {
|
||||||
ShrinkEveryNTurns int32
|
ShrinkEveryNTurns int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoyaleRuleset) Name() string { return "royale" }
|
func (r *RoyaleRuleset) Name() string { return GameTypeRoyale }
|
||||||
|
|
||||||
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
||||||
|
|
@ -26,7 +26,7 @@ func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []Snak
|
||||||
}
|
}
|
||||||
|
|
||||||
// Royale's only job is now to populate the hazards for next turn - StandardRuleset takes care of applying hazard damage.
|
// Royale's only job is now to populate the hazards for next turn - StandardRuleset takes care of applying hazard damage.
|
||||||
err = r.populateHazards(nextBoardState, prevState.Turn+1)
|
err = r.populateHazards(nextBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -34,20 +34,28 @@ func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []Snak
|
||||||
return nextBoardState, nil
|
return nextBoardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoyaleRuleset) populateHazards(b *BoardState, turn int32) error {
|
func (r *RoyaleRuleset) populateHazards(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(PopulateHazardsRoyale, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
b.Hazards = []Point{}
|
b.Hazards = []Point{}
|
||||||
|
|
||||||
if r.ShrinkEveryNTurns < 1 {
|
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||||
return errors.New("royale game can't shrink more frequently than every turn")
|
turn := b.Turn + 1
|
||||||
|
|
||||||
|
if settings.RoyaleSettings.ShrinkEveryNTurns < 1 {
|
||||||
|
return false, errors.New("royale game can't shrink more frequently than every turn")
|
||||||
}
|
}
|
||||||
|
|
||||||
if turn < r.ShrinkEveryNTurns {
|
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
randGenerator := rand.New(rand.NewSource(r.Seed))
|
randGenerator := rand.New(rand.NewSource(settings.RoyaleSettings.seed))
|
||||||
|
|
||||||
numShrinks := turn / r.ShrinkEveryNTurns
|
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
||||||
minX, maxX := int32(0), b.Width-1
|
minX, maxX := int32(0), b.Width-1
|
||||||
minY, maxY := int32(0), b.Height-1
|
minY, maxY := int32(0), b.Height-1
|
||||||
for i := int32(0); i < numShrinks; i++ {
|
for i := int32(0); i < numShrinks; i++ {
|
||||||
|
|
@ -71,5 +79,19 @@ func (r *RoyaleRuleset) populateHazards(b *BoardState, turn int32) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RoyaleRuleset) Settings() Settings {
|
||||||
|
s := r.StandardRuleset.Settings()
|
||||||
|
s.RoyaleSettings = RoyaleSettings{
|
||||||
|
seed: r.Seed,
|
||||||
|
ShrinkEveryNTurns: r.ShrinkEveryNTurns,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptor for integrating stages into RoyaleRuleset
|
||||||
|
func (r *RoyaleRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||||
|
return stage(boardState, r.Settings(), moves)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
b := &BoardState{
|
b := &BoardState{
|
||||||
Turn: test.Turn,
|
Turn: test.Turn - 1,
|
||||||
Width: test.Width,
|
Width: test.Width,
|
||||||
Height: test.Height,
|
Height: test.Height,
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.populateHazards(b, test.Turn)
|
err := r.populateHazards(b)
|
||||||
require.Equal(t, test.Error, err)
|
require.Equal(t, test.Error, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Obstacles should match
|
// Obstacles should match
|
||||||
|
|
@ -131,7 +131,7 @@ func TestRoyalDamageNextTurn(t *testing.T) {
|
||||||
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
||||||
nextState := prevState.Clone()
|
nextState := prevState.Clone()
|
||||||
nextState.Turn = turn - 1
|
nextState.Turn = turn - 1
|
||||||
err := r.populateHazards(nextState, turn)
|
err := r.populateHazards(nextState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nextState.Turn = turn
|
nextState.Turn = turn
|
||||||
return nextState
|
return nextState
|
||||||
|
|
|
||||||
181
ruleset.go
181
ruleset.go
|
|
@ -1,5 +1,9 @@
|
||||||
package rules
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
type RulesetError string
|
type RulesetError string
|
||||||
|
|
||||||
func (err RulesetError) Error() string { return string(err) }
|
func (err RulesetError) Error() string { return string(err) }
|
||||||
|
|
@ -24,6 +28,7 @@ const (
|
||||||
EliminatedByOutOfHealth = "out-of-health"
|
EliminatedByOutOfHealth = "out-of-health"
|
||||||
EliminatedByHeadToHeadCollision = "head-collision"
|
EliminatedByHeadToHeadCollision = "head-collision"
|
||||||
EliminatedByOutOfBounds = "wall-collision"
|
EliminatedByOutOfBounds = "wall-collision"
|
||||||
|
EliminatedBySquad = "squad-eliminated"
|
||||||
|
|
||||||
// TODO - Error consts
|
// TODO - Error consts
|
||||||
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
||||||
|
|
@ -31,8 +36,147 @@ const (
|
||||||
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
||||||
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
||||||
ErrorZeroLengthSnake = RulesetError("snake is length zero")
|
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 {
|
type Point struct {
|
||||||
X int32
|
X int32
|
||||||
Y int32
|
Y int32
|
||||||
|
|
@ -57,4 +201,41 @@ type Ruleset interface {
|
||||||
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
|
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
|
||||||
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
|
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
|
||||||
IsGameOver(state *BoardState) (bool, 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)
|
||||||
|
|
|
||||||
85
ruleset_internal_test.go
Normal file
85
ruleset_internal_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
// included to allow using -update-fixtures for every package without errors
|
||||||
|
|
||||||
|
_ "github.com/BattlesnakeOfficial/rules/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParamInt32(t *testing.T) {
|
||||||
|
require.Equal(t, int32(5), paramsInt32(nil, "test", 5), "nil map")
|
||||||
|
require.Equal(t, int32(10), paramsInt32(map[string]string{}, "foo", 10), "empty map")
|
||||||
|
require.Equal(t, int32(10), paramsInt32(map[string]string{"hullo": "there"}, "hullo", 10), "invalid value")
|
||||||
|
require.Equal(t, int32(20), paramsInt32(map[string]string{"bonjour": "20"}, "bonjour", 20), "valid value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamBool(t *testing.T) {
|
||||||
|
// missing values default to specified value
|
||||||
|
require.Equal(t, true, paramsBool(nil, "test", true), "nil map true")
|
||||||
|
require.Equal(t, false, paramsBool(nil, "test", false), "nil map false")
|
||||||
|
|
||||||
|
// missing values default to specified value
|
||||||
|
require.Equal(t, true, paramsBool(map[string]string{}, "foo", true), "empty map true")
|
||||||
|
require.Equal(t, false, paramsBool(map[string]string{}, "foo", false), "empty map false")
|
||||||
|
|
||||||
|
// invalid values (exist but not booL) default to false
|
||||||
|
require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", true), "invalid value default true")
|
||||||
|
require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", false), "invalid value default false")
|
||||||
|
|
||||||
|
// valid values ignore defaults
|
||||||
|
require.Equal(t, false, paramsBool(map[string]string{"bonjour": "false"}, "bonjour", false), "valid value false")
|
||||||
|
require.Equal(t, true, paramsBool(map[string]string{"bonjour": "true"}, "bonjour", false), "valid value true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesetError(t *testing.T) {
|
||||||
|
err := (error)(RulesetError("test error string"))
|
||||||
|
require.Equal(t, "test error string", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesetBuilderInternals(t *testing.T) {
|
||||||
|
|
||||||
|
// test Royale with seed
|
||||||
|
rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale})
|
||||||
|
require.Equal(t, int64(3), rsb.seed)
|
||||||
|
require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name())
|
||||||
|
require.Equal(t, int64(3), rsb.Ruleset().(*RoyaleRuleset).Seed)
|
||||||
|
|
||||||
|
// test squad configuration
|
||||||
|
rsb = NewRulesetBuilder().
|
||||||
|
WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeSquad,
|
||||||
|
}).
|
||||||
|
AddSnakeToSquad("snek1", "squad1").
|
||||||
|
AddSnakeToSquad("snek2", "squad1").
|
||||||
|
AddSnakeToSquad("snek3", "squad2").
|
||||||
|
AddSnakeToSquad("snek4", "squad2")
|
||||||
|
|
||||||
|
require.NotNil(t, rsb.Ruleset())
|
||||||
|
require.Equal(t, GameTypeSquad, rsb.Ruleset().Name())
|
||||||
|
require.Equal(t, 4, len(rsb.squads))
|
||||||
|
require.Equal(t, "squad1", rsb.Ruleset().(*SquadRuleset).SquadMap["snek1"])
|
||||||
|
require.Equal(t, "squad1", rsb.Ruleset().(*SquadRuleset).SquadMap["snek2"])
|
||||||
|
require.Equal(t, "squad2", rsb.Ruleset().(*SquadRuleset).SquadMap["snek3"])
|
||||||
|
require.Equal(t, "squad2", rsb.Ruleset().(*SquadRuleset).SquadMap["snek4"])
|
||||||
|
|
||||||
|
// test parameter merging
|
||||||
|
rsb = NewRulesetBuilder().
|
||||||
|
WithParams(map[string]string{
|
||||||
|
"someSetting": "some value",
|
||||||
|
"anotherSetting": "another value",
|
||||||
|
}).
|
||||||
|
WithParams(map[string]string{
|
||||||
|
"anotherSetting": "overridden value",
|
||||||
|
"aNewSetting": "a new value",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"someSetting": "some value",
|
||||||
|
"anotherSetting": "overridden value",
|
||||||
|
"aNewSetting": "a new value",
|
||||||
|
}, rsb.params, "multiple calls to WithParams should merge parameters")
|
||||||
|
}
|
||||||
192
ruleset_test.go
192
ruleset_test.go
|
|
@ -1,15 +1,193 @@
|
||||||
package rules
|
package rules_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
// included to allow using -update-fixtures for every package without errors
|
|
||||||
_ "github.com/BattlesnakeOfficial/rules/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRulesetError(t *testing.T) {
|
func TestStandardRulesetSettings(t *testing.T) {
|
||||||
err := (error)(RulesetError("test error string"))
|
ruleset := rules.StandardRuleset{
|
||||||
require.Equal(t, "test error string", err.Error())
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrappedRulesetSettings(t *testing.T) {
|
||||||
|
ruleset := rules.WrappedRuleset{
|
||||||
|
StandardRuleset: rules.StandardRuleset{
|
||||||
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSoloRulesetSettings(t *testing.T) {
|
||||||
|
ruleset := rules.SoloRuleset{
|
||||||
|
StandardRuleset: rules.StandardRuleset{
|
||||||
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoyaleRulesetSettings(t *testing.T) {
|
||||||
|
ruleset := rules.RoyaleRuleset{
|
||||||
|
Seed: 30,
|
||||||
|
ShrinkEveryNTurns: 12,
|
||||||
|
StandardRuleset: rules.StandardRuleset{
|
||||||
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.ShrinkEveryNTurns, ruleset.Settings().RoyaleSettings.ShrinkEveryNTurns)
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstrictorRulesetSettings(t *testing.T) {
|
||||||
|
ruleset := rules.ConstrictorRuleset{
|
||||||
|
StandardRuleset: rules.StandardRuleset{
|
||||||
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSquadRulesetSettings(t *testing.T) {
|
||||||
|
ruleset := rules.SquadRuleset{
|
||||||
|
AllowBodyCollisions: true,
|
||||||
|
SharedElimination: false,
|
||||||
|
SharedHealth: true,
|
||||||
|
SharedLength: false,
|
||||||
|
StandardRuleset: rules.StandardRuleset{
|
||||||
|
MinimumFood: 5,
|
||||||
|
FoodSpawnChance: 10,
|
||||||
|
HazardDamagePerTurn: 10,
|
||||||
|
HazardMap: "hz_spiral",
|
||||||
|
HazardMapAuthor: "altersaddle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ruleset.AllowBodyCollisions, ruleset.Settings().SquadSettings.AllowBodyCollisions)
|
||||||
|
assert.Equal(t, ruleset.SharedElimination, ruleset.Settings().SquadSettings.SharedElimination)
|
||||||
|
assert.Equal(t, ruleset.SharedHealth, ruleset.Settings().SquadSettings.SharedHealth)
|
||||||
|
assert.Equal(t, ruleset.SharedLength, ruleset.Settings().SquadSettings.SharedLength)
|
||||||
|
|
||||||
|
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
||||||
|
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
||||||
|
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
||||||
|
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
||||||
|
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesetBuilder(t *testing.T) {
|
||||||
|
// Test that a fresh instance can produce a Ruleset
|
||||||
|
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
||||||
|
require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().Ruleset().Name(), "should default to standard game")
|
||||||
|
|
||||||
|
// test nil safety / defaults
|
||||||
|
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
||||||
|
|
||||||
|
// make sure it works okay for lots of game types
|
||||||
|
expectedResults := []struct {
|
||||||
|
GameType string
|
||||||
|
Snakes map[string]string
|
||||||
|
}{
|
||||||
|
{GameType: rules.GameTypeStandard},
|
||||||
|
{GameType: rules.GameTypeWrapped},
|
||||||
|
{GameType: rules.GameTypeRoyale},
|
||||||
|
{GameType: rules.GameTypeSolo},
|
||||||
|
{GameType: rules.GameTypeSquad, Snakes: map[string]string{
|
||||||
|
"one": "s1",
|
||||||
|
"two": "s1",
|
||||||
|
"three": "s2",
|
||||||
|
"four": "s2",
|
||||||
|
"five": "s3",
|
||||||
|
"six": "s3",
|
||||||
|
"seven": "s4",
|
||||||
|
"eight": "s4",
|
||||||
|
}},
|
||||||
|
{GameType: rules.GameTypeConstrictor},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range expectedResults {
|
||||||
|
t.Run(expected.GameType, func(t *testing.T) {
|
||||||
|
rsb := rules.NewRulesetBuilder()
|
||||||
|
|
||||||
|
rsb.WithParams(map[string]string{
|
||||||
|
// apply the standard rule params
|
||||||
|
rules.ParamGameType: expected.GameType,
|
||||||
|
rules.ParamFoodSpawnChance: "10",
|
||||||
|
rules.ParamMinimumFood: "5",
|
||||||
|
rules.ParamHazardDamagePerTurn: "12",
|
||||||
|
rules.ParamHazardMap: "test",
|
||||||
|
rules.ParamHazardMapAuthor: "tester",
|
||||||
|
})
|
||||||
|
|
||||||
|
// add any snake squads
|
||||||
|
for id, squad := range expected.Snakes {
|
||||||
|
rsb = rsb.AddSnakeToSquad(id, squad)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, rsb.Ruleset())
|
||||||
|
require.Equal(t, expected.GameType, rsb.Ruleset().Name())
|
||||||
|
// All the standard settings should always be copied over
|
||||||
|
require.Equal(t, int32(10), rsb.Ruleset().Settings().FoodSpawnChance)
|
||||||
|
require.Equal(t, int32(12), rsb.Ruleset().Settings().HazardDamagePerTurn)
|
||||||
|
require.Equal(t, int32(5), rsb.Ruleset().Settings().MinimumFood)
|
||||||
|
require.Equal(t, "test", rsb.Ruleset().Settings().HazardMap)
|
||||||
|
require.Equal(t, "tester", rsb.Ruleset().Settings().HazardMapAuthor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStageFuncContract(t *testing.T) {
|
||||||
|
//nolint:gosimple
|
||||||
|
var stage rules.StageFunc
|
||||||
|
stage = func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
ended, err := stage(nil, rules.NewRulesetBuilder().Ruleset().Settings(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ended)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
solo.go
6
solo.go
|
|
@ -4,9 +4,13 @@ type SoloRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SoloRuleset) Name() string { return "solo" }
|
func (r *SoloRuleset) Name() string { return GameTypeSolo }
|
||||||
|
|
||||||
func (r *SoloRuleset) IsGameOver(b *BoardState) (bool, error) {
|
func (r *SoloRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
|
return r.callStageFunc(GameOverSolo, b, []SnakeMove{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
|
||||||
77
squad.go
77
squad.go
|
|
@ -16,9 +16,7 @@ type SquadRuleset struct {
|
||||||
SharedLength bool
|
SharedLength bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const EliminatedBySquad = "squad-eliminated"
|
func (r *SquadRuleset) Name() string { return GameTypeSquad }
|
||||||
|
|
||||||
func (r *SquadRuleset) Name() string { return "squad" }
|
|
||||||
|
|
||||||
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||||
|
|
@ -26,13 +24,11 @@ func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []Snake
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err = r.resurrectSquadBodyCollisions(nextBoardState)
|
err = r.resurrectSquadBodyCollisions(nextBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err = r.shareSquadAttributes(nextBoardState)
|
err = r.shareSquadAttributes(nextBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -41,38 +37,50 @@ func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []Snake
|
||||||
return nextBoardState, nil
|
return nextBoardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SquadRuleset) areSnakesOnSameSquad(snake *Snake, other *Snake) bool {
|
func areSnakesOnSameSquad(squadMap map[string]string, snake *Snake, other *Snake) bool {
|
||||||
return r.areSnakeIDsOnSameSquad(snake.ID, other.ID)
|
return areSnakeIDsOnSameSquad(squadMap, snake.ID, other.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SquadRuleset) areSnakeIDsOnSameSquad(snakeID string, otherID string) bool {
|
func areSnakeIDsOnSameSquad(squadMap map[string]string, snakeID string, otherID string) bool {
|
||||||
return r.SquadMap[snakeID] == r.SquadMap[otherID]
|
return squadMap[snakeID] == squadMap[otherID]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SquadRuleset) resurrectSquadBodyCollisions(b *BoardState) error {
|
func (r *SquadRuleset) resurrectSquadBodyCollisions(b *BoardState) error {
|
||||||
if !r.AllowBodyCollisions {
|
_, err := r.callStageFunc(ResurrectSnakesSquad, b, []SnakeMove{})
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if !settings.SquadSettings.AllowBodyCollisions {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
if snake.EliminatedCause == EliminatedByCollision {
|
if snake.EliminatedCause == EliminatedByCollision {
|
||||||
if snake.EliminatedBy == "" {
|
if snake.EliminatedBy == "" {
|
||||||
return errors.New("snake eliminated by collision and eliminatedby is not set")
|
return false, errors.New("snake eliminated by collision and eliminatedby is not set")
|
||||||
}
|
}
|
||||||
if snake.ID != snake.EliminatedBy && r.areSnakeIDsOnSameSquad(snake.ID, snake.EliminatedBy) {
|
if snake.ID != snake.EliminatedBy && areSnakeIDsOnSameSquad(settings.SquadSettings.squadMap, snake.ID, snake.EliminatedBy) {
|
||||||
snake.EliminatedCause = NotEliminated
|
snake.EliminatedCause = NotEliminated
|
||||||
snake.EliminatedBy = ""
|
snake.EliminatedBy = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
||||||
if !(r.SharedElimination || r.SharedLength || r.SharedHealth) {
|
_, err := r.callStageFunc(ShareAttributesSquad, b, []SnakeMove{})
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShareAttributesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
squadSettings := settings.SquadSettings
|
||||||
|
|
||||||
|
if !(squadSettings.SharedElimination || squadSettings.SharedLength || squadSettings.SharedHealth) {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
|
|
@ -83,21 +91,21 @@ func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
||||||
|
|
||||||
for j := 0; j < len(b.Snakes); j++ {
|
for j := 0; j < len(b.Snakes); j++ {
|
||||||
other := &b.Snakes[j]
|
other := &b.Snakes[j]
|
||||||
if r.areSnakesOnSameSquad(snake, other) {
|
if areSnakesOnSameSquad(squadSettings.squadMap, snake, other) {
|
||||||
if r.SharedHealth {
|
if squadSettings.SharedHealth {
|
||||||
if snake.Health < other.Health {
|
if snake.Health < other.Health {
|
||||||
snake.Health = other.Health
|
snake.Health = other.Health
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.SharedLength {
|
if squadSettings.SharedLength {
|
||||||
if len(snake.Body) == 0 || len(other.Body) == 0 {
|
if len(snake.Body) == 0 || len(other.Body) == 0 {
|
||||||
return errors.New("found snake of zero length")
|
return false, errors.New("found snake of zero length")
|
||||||
}
|
}
|
||||||
for len(snake.Body) < len(other.Body) {
|
for len(snake.Body) < len(other.Body) {
|
||||||
r.growSnake(snake)
|
growSnake(snake)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.SharedElimination {
|
if squadSettings.SharedElimination {
|
||||||
if snake.EliminatedCause == NotEliminated && other.EliminatedCause != NotEliminated {
|
if snake.EliminatedCause == NotEliminated && other.EliminatedCause != NotEliminated {
|
||||||
snake.EliminatedCause = EliminatedBySquad
|
snake.EliminatedCause = EliminatedBySquad
|
||||||
// We intentionally do not set snake.EliminatedBy because there might be multiple culprits.
|
// We intentionally do not set snake.EliminatedBy because there might be multiple culprits.
|
||||||
|
|
@ -108,10 +116,14 @@ func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
|
return r.callStageFunc(GameOverSquad, b, []SnakeMove{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GameOverSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
snakesRemaining := []*Snake{}
|
snakesRemaining := []*Snake{}
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
|
|
@ -120,7 +132,7 @@ func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(snakesRemaining); i++ {
|
for i := 0; i < len(snakesRemaining); i++ {
|
||||||
if !r.areSnakesOnSameSquad(snakesRemaining[i], snakesRemaining[0]) {
|
if !areSnakesOnSameSquad(settings.SquadSettings.squadMap, snakesRemaining[i], snakesRemaining[0]) {
|
||||||
// There are multiple squads remaining
|
// There are multiple squads remaining
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
@ -128,3 +140,20 @@ func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
// no snakes or single squad remaining
|
// no snakes or single squad remaining
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r SquadRuleset) Settings() Settings {
|
||||||
|
s := r.StandardRuleset.Settings()
|
||||||
|
s.SquadSettings = SquadSettings{
|
||||||
|
squadMap: r.SquadMap,
|
||||||
|
AllowBodyCollisions: r.AllowBodyCollisions,
|
||||||
|
SharedElimination: r.SharedElimination,
|
||||||
|
SharedHealth: r.SharedHealth,
|
||||||
|
SharedLength: r.SharedLength,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptor for integrating stages into SquadRuleset
|
||||||
|
func (r *SquadRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||||
|
return stage(boardState, r.Settings(), moves)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ func TestSquadAllowBodyCollisions(t *testing.T) {
|
||||||
func TestSquadAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
|
func TestSquadAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
|
||||||
boardState := &BoardState{
|
boardState := &BoardState{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
Snake{ID: "1", EliminatedCause: EliminatedByCollision},
|
{ID: "1", EliminatedCause: EliminatedByCollision},
|
||||||
Snake{ID: "2"},
|
{ID: "2"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r := SquadRuleset{
|
r := SquadRuleset{
|
||||||
|
|
@ -280,8 +280,8 @@ func TestSquadSharedElimination(t *testing.T) {
|
||||||
func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
||||||
boardState := &BoardState{
|
boardState := &BoardState{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
Snake{ID: "1"},
|
{ID: "1"},
|
||||||
Snake{ID: "2"},
|
{ID: "2"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r := SquadRuleset{
|
r := SquadRuleset{
|
||||||
|
|
|
||||||
130
standard.go
130
standard.go
|
|
@ -9,9 +9,11 @@ type StandardRuleset struct {
|
||||||
FoodSpawnChance int32 // [0, 100]
|
FoodSpawnChance int32 // [0, 100]
|
||||||
MinimumFood int32
|
MinimumFood int32
|
||||||
HazardDamagePerTurn int32
|
HazardDamagePerTurn int32
|
||||||
|
HazardMap string // optional
|
||||||
|
HazardMapAuthor string // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) Name() string { return "standard" }
|
func (r *StandardRuleset) Name() string { return GameTypeStandard }
|
||||||
|
|
||||||
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
||||||
// No-op
|
// No-op
|
||||||
|
|
@ -22,15 +24,11 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
||||||
// We specifically want to copy prevState, so as not to alter it directly.
|
// We specifically want to copy prevState, so as not to alter it directly.
|
||||||
nextState := prevState.Clone()
|
nextState := prevState.Clone()
|
||||||
|
|
||||||
// TODO: Gut check the BoardState?
|
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err := r.moveSnakes(nextState, moves)
|
err := r.moveSnakes(nextState, moves)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err = r.reduceSnakeHealth(nextState)
|
err = r.reduceSnakeHealth(nextState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -41,7 +39,6 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
// bvanvugt: We specifically want this to happen before elimination for two reasons:
|
// bvanvugt: We specifically want this to happen before elimination for two reasons:
|
||||||
// 1) We want snakes to be able to eat on their very last turn and still survive.
|
// 1) We want snakes to be able to eat on their very last turn and still survive.
|
||||||
// 2) So that head-to-head collisions on food still remove the food.
|
// 2) So that head-to-head collisions on food still remove the food.
|
||||||
|
|
@ -52,13 +49,11 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err = r.maybeSpawnFood(nextState)
|
err = r.maybeSpawnFood(nextState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LOG?
|
|
||||||
err = r.maybeEliminateSnakes(nextState)
|
err = r.maybeEliminateSnakes(nextState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -68,6 +63,16 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
|
_, err := r.callStageFunc(MoveSnakesStandard, b, moves)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
// If no moves are passed, pass on modifying the initial board state
|
||||||
|
if len(moves) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Sanity check that all non-eliminated snakes have moves and bodies.
|
// Sanity check that all non-eliminated snakes have moves and bodies.
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
|
|
@ -76,7 +81,7 @@ func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(snake.Body) == 0 {
|
if len(snake.Body) == 0 {
|
||||||
return ErrorZeroLengthSnake
|
return false, ErrorZeroLengthSnake
|
||||||
}
|
}
|
||||||
moveFound := false
|
moveFound := false
|
||||||
for _, move := range moves {
|
for _, move := range moves {
|
||||||
|
|
@ -86,7 +91,7 @@ func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !moveFound {
|
if !moveFound {
|
||||||
return ErrorNoMoveFound
|
return false, ErrorNoMoveFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +108,7 @@ func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
case MoveUp, MoveDown, MoveRight, MoveLeft:
|
case MoveUp, MoveDown, MoveRight, MoveLeft:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
appliedMove = r.getDefaultMove(snake.Body)
|
appliedMove = getDefaultMove(snake.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
newHead := Point{}
|
newHead := Point{}
|
||||||
|
|
@ -128,10 +133,10 @@ func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) getDefaultMove(snakeBody []Point) string {
|
func getDefaultMove(snakeBody []Point) string {
|
||||||
if len(snakeBody) >= 2 {
|
if len(snakeBody) >= 2 {
|
||||||
// Use neck to determine last move made
|
// Use neck to determine last move made
|
||||||
head, neck := snakeBody[0], snakeBody[1]
|
head, neck := snakeBody[0], snakeBody[1]
|
||||||
|
|
@ -160,15 +165,25 @@ func (r *StandardRuleset) getDefaultMove(snakeBody []Point) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
|
func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(ReduceSnakeHealthStandard, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReduceSnakeHealthStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error {
|
func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(DamageHazardsStandard, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
if snake.EliminatedCause != NotEliminated {
|
if snake.EliminatedCause != NotEliminated {
|
||||||
|
|
@ -189,21 +204,26 @@ func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snake is in a hazard, reduce health
|
// Snake is in a hazard, reduce health
|
||||||
snake.Health = snake.Health - r.HazardDamagePerTurn
|
snake.Health = snake.Health - settings.HazardDamagePerTurn
|
||||||
if snake.Health < 0 {
|
if snake.Health < 0 {
|
||||||
snake.Health = 0
|
snake.Health = 0
|
||||||
}
|
}
|
||||||
if r.snakeIsOutOfHealth(snake) {
|
if snakeIsOutOfHealth(snake) {
|
||||||
snake.EliminatedCause = EliminatedByOutOfHealth
|
snake.EliminatedCause = EliminatedByOutOfHealth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(EliminateSnakesStandard, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
// First order snake indices by length.
|
// First order snake indices by length.
|
||||||
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
||||||
snakeIndicesByLength := make([]int, len(b.Snakes))
|
snakeIndicesByLength := make([]int, len(b.Snakes))
|
||||||
|
|
@ -224,15 +244,15 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(snake.Body) <= 0 {
|
if len(snake.Body) <= 0 {
|
||||||
return ErrorZeroLengthSnake
|
return false, ErrorZeroLengthSnake
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.snakeIsOutOfHealth(snake) {
|
if snakeIsOutOfHealth(snake) {
|
||||||
snake.EliminatedCause = EliminatedByOutOfHealth
|
snake.EliminatedCause = EliminatedByOutOfHealth
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.snakeIsOutOfBounds(snake, b.Width, b.Height) {
|
if snakeIsOutOfBounds(snake, b.Width, b.Height) {
|
||||||
snake.EliminatedCause = EliminatedByOutOfBounds
|
snake.EliminatedCause = EliminatedByOutOfBounds
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -252,11 +272,11 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(snake.Body) <= 0 {
|
if len(snake.Body) <= 0 {
|
||||||
return ErrorZeroLengthSnake
|
return false, ErrorZeroLengthSnake
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for self-collisions first
|
// Check for self-collisions first
|
||||||
if r.snakeHasBodyCollided(snake, snake) {
|
if snakeHasBodyCollided(snake, snake) {
|
||||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||||
ID: snake.ID,
|
ID: snake.ID,
|
||||||
Cause: EliminatedBySelfCollision,
|
Cause: EliminatedBySelfCollision,
|
||||||
|
|
@ -272,7 +292,7 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
if other.EliminatedCause != NotEliminated {
|
if other.EliminatedCause != NotEliminated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if snake.ID != other.ID && r.snakeHasBodyCollided(snake, other) {
|
if snake.ID != other.ID && snakeHasBodyCollided(snake, other) {
|
||||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||||
ID: snake.ID,
|
ID: snake.ID,
|
||||||
Cause: EliminatedByCollision,
|
Cause: EliminatedByCollision,
|
||||||
|
|
@ -293,7 +313,7 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
if other.EliminatedCause != NotEliminated {
|
if other.EliminatedCause != NotEliminated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if snake.ID != other.ID && r.snakeHasLostHeadToHead(snake, other) {
|
if snake.ID != other.ID && snakeHasLostHeadToHead(snake, other) {
|
||||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||||
ID: snake.ID,
|
ID: snake.ID,
|
||||||
Cause: EliminatedByHeadToHeadCollision,
|
Cause: EliminatedByHeadToHeadCollision,
|
||||||
|
|
@ -320,14 +340,14 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) snakeIsOutOfHealth(s *Snake) bool {
|
func snakeIsOutOfHealth(s *Snake) bool {
|
||||||
return s.Health <= 0
|
return s.Health <= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) snakeIsOutOfBounds(s *Snake, boardWidth int32, boardHeight int32) bool {
|
func snakeIsOutOfBounds(s *Snake, boardWidth int32, boardHeight int32) bool {
|
||||||
for _, point := range s.Body {
|
for _, point := range s.Body {
|
||||||
if (point.X < 0) || (point.X >= boardWidth) {
|
if (point.X < 0) || (point.X >= boardWidth) {
|
||||||
return true
|
return true
|
||||||
|
|
@ -339,7 +359,7 @@ func (r *StandardRuleset) snakeIsOutOfBounds(s *Snake, boardWidth int32, boardHe
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
func snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
||||||
head := s.Body[0]
|
head := s.Body[0]
|
||||||
for i, body := range other.Body {
|
for i, body := range other.Body {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|
@ -351,7 +371,7 @@ func (r *StandardRuleset) snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
|
func snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
|
||||||
if s.Body[0].X == other.Body[0].X && s.Body[0].Y == other.Body[0].Y {
|
if s.Body[0].X == other.Body[0].X && s.Body[0].Y == other.Body[0].Y {
|
||||||
return len(s.Body) <= len(other.Body)
|
return len(s.Body) <= len(other.Body)
|
||||||
}
|
}
|
||||||
|
|
@ -359,6 +379,11 @@ func (r *StandardRuleset) snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
||||||
|
_, err := r.callStageFunc(FeedSnakesStandard, b, []SnakeMove{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func FeedSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
newFood := []Point{}
|
newFood := []Point{}
|
||||||
for _, food := range b.Food {
|
for _, food := range b.Food {
|
||||||
foodHasBeenEaten := false
|
foodHasBeenEaten := false
|
||||||
|
|
@ -371,7 +396,7 @@ func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if snake.Body[0].X == food.X && snake.Body[0].Y == food.Y {
|
if snake.Body[0].X == food.X && snake.Body[0].Y == food.Y {
|
||||||
r.feedSnake(snake)
|
feedSnake(snake)
|
||||||
foodHasBeenEaten = true
|
foodHasBeenEaten = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,31 +407,41 @@ func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Food = newFood
|
b.Food = newFood
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) feedSnake(snake *Snake) {
|
func feedSnake(snake *Snake) {
|
||||||
r.growSnake(snake)
|
growSnake(snake)
|
||||||
snake.Health = SnakeMaxHealth
|
snake.Health = SnakeMaxHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) growSnake(snake *Snake) {
|
func growSnake(snake *Snake) {
|
||||||
if len(snake.Body) > 0 {
|
if len(snake.Body) > 0 {
|
||||||
snake.Body = append(snake.Body, snake.Body[len(snake.Body)-1])
|
snake.Body = append(snake.Body, snake.Body[len(snake.Body)-1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
|
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
|
||||||
numCurrentFood := int32(len(b.Food))
|
_, err := r.callStageFunc(SpawnFoodStandard, b, []SnakeMove{})
|
||||||
if numCurrentFood < r.MinimumFood {
|
return err
|
||||||
return PlaceFoodRandomly(b, r.MinimumFood-numCurrentFood)
|
|
||||||
} else if r.FoodSpawnChance > 0 && int32(rand.Intn(100)) < r.FoodSpawnChance {
|
|
||||||
return PlaceFoodRandomly(b, 1)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
numCurrentFood := int32(len(b.Food))
|
||||||
|
if numCurrentFood < settings.MinimumFood {
|
||||||
|
return false, PlaceFoodRandomly(b, settings.MinimumFood-numCurrentFood)
|
||||||
|
}
|
||||||
|
if settings.FoodSpawnChance > 0 && int32(rand.Intn(100)) < settings.FoodSpawnChance {
|
||||||
|
return false, PlaceFoodRandomly(b, 1)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
|
return r.callStageFunc(GameOverStandard, b, []SnakeMove{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
numSnakesRemaining := 0
|
numSnakesRemaining := 0
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
|
|
@ -415,3 +450,18 @@ func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
}
|
}
|
||||||
return numSnakesRemaining <= 1, nil
|
return numSnakesRemaining <= 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r StandardRuleset) Settings() Settings {
|
||||||
|
return Settings{
|
||||||
|
FoodSpawnChance: r.FoodSpawnChance,
|
||||||
|
MinimumFood: r.MinimumFood,
|
||||||
|
HazardDamagePerTurn: r.HazardDamagePerTurn,
|
||||||
|
HazardMap: r.HazardMap,
|
||||||
|
HazardMapAuthor: r.HazardMapAuthor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptor for integrating stages into StandardRuleset
|
||||||
|
func (r *StandardRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||||
|
return stage(boardState, r.Settings(), moves)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,9 @@ var standardCaseErrNoMoveFound = gameTestCase{
|
||||||
Food: []Point{{0, 0}, {1, 0}},
|
Food: []Point{{0, 0}, {1, 0}},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
},
|
},
|
||||||
[]SnakeMove{},
|
[]SnakeMove{
|
||||||
|
{ID: "one", Move: MoveUp},
|
||||||
|
},
|
||||||
ErrorNoMoveFound,
|
ErrorNoMoveFound,
|
||||||
nil,
|
nil,
|
||||||
}
|
}
|
||||||
|
|
@ -767,9 +769,8 @@ func TestGetDefaultMove(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
actualMove := r.getDefaultMove(test.SnakeBody)
|
actualMove := getDefaultMove(test.SnakeBody)
|
||||||
require.Equal(t, test.ExpectedMove, actualMove)
|
require.Equal(t, test.ExpectedMove, actualMove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -835,10 +836,9 @@ func TestSnakeIsOutOfHealth(t *testing.T) {
|
||||||
{Health: math.MaxInt32, Expected: false},
|
{Health: math.MaxInt32, Expected: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
s := &Snake{Health: test.Health}
|
s := &Snake{Health: test.Health}
|
||||||
require.Equal(t, test.Expected, r.snakeIsOutOfHealth(s), "Health: %+v", test.Health)
|
require.Equal(t, test.Expected, snakeIsOutOfHealth(s), "Health: %+v", test.Health)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -877,14 +877,13 @@ func TestSnakeIsOutOfBounds(t *testing.T) {
|
||||||
{Point{X: math.MaxInt32, Y: math.MaxInt32}, true},
|
{Point{X: math.MaxInt32, Y: math.MaxInt32}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
// Test with point as head
|
// Test with point as head
|
||||||
s := Snake{Body: []Point{test.Point}}
|
s := Snake{Body: []Point{test.Point}}
|
||||||
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point)
|
require.Equal(t, test.Expected, snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point)
|
||||||
// Test with point as body
|
// Test with point as body
|
||||||
s = Snake{Body: []Point{{0, 0}, {0, 0}, test.Point}}
|
s = Snake{Body: []Point{{0, 0}, {0, 0}, test.Point}}
|
||||||
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point)
|
require.Equal(t, test.Expected, snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -915,10 +914,9 @@ func TestSnakeHasBodyCollidedSelf(t *testing.T) {
|
||||||
{[]Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true},
|
{[]Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
s := Snake{Body: test.Body}
|
s := Snake{Body: test.Body}
|
||||||
require.Equal(t, test.Expected, r.snakeHasBodyCollided(&s, &s), "Body%q", s.Body)
|
require.Equal(t, test.Expected, snakeHasBodyCollided(&s, &s), "Body%q", s.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -966,11 +964,10 @@ func TestSnakeHasBodyCollidedOther(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
s := &Snake{Body: test.SnakeBody}
|
s := &Snake{Body: test.SnakeBody}
|
||||||
o := &Snake{Body: test.OtherBody}
|
o := &Snake{Body: test.OtherBody}
|
||||||
require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, o), "Snake%q Other%q", s.Body, o.Body)
|
require.Equal(t, test.Expected, snakeHasBodyCollided(s, o), "Snake%q Other%q", s.Body, o.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1031,12 +1028,11 @@ func TestSnakeHasLostHeadToHead(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
s := Snake{Body: test.SnakeBody}
|
s := Snake{Body: test.SnakeBody}
|
||||||
o := Snake{Body: test.OtherBody}
|
o := Snake{Body: test.OtherBody}
|
||||||
require.Equal(t, test.Expected, r.snakeHasLostHeadToHead(&s, &o), "Snake%q Other%q", s.Body, o.Body)
|
require.Equal(t, test.Expected, snakeHasLostHeadToHead(&s, &o), "Snake%q Other%q", s.Body, o.Body)
|
||||||
require.Equal(t, test.ExpectedOpposite, r.snakeHasLostHeadToHead(&o, &s), "Snake%q Other%q", s.Body, o.Body)
|
require.Equal(t, test.ExpectedOpposite, snakeHasLostHeadToHead(&o, &s), "Snake%q Other%q", s.Body, o.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
wrapped.go
37
wrapped.go
|
|
@ -4,17 +4,7 @@ type WrappedRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WrappedRuleset) Name() string { return "wrapped" }
|
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
||||||
|
|
||||||
func replace(value, min, max int32) int32 {
|
|
||||||
if value < min {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
nextState := prevState.Clone()
|
nextState := prevState.Clone()
|
||||||
|
|
@ -53,19 +43,34 @@ func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []Sna
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WrappedRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
func (r *WrappedRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||||
err := r.StandardRuleset.moveSnakes(b, moves)
|
_, err := r.callStageFunc(MoveSnakesWrapped, b, moves)
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
_, err := MoveSnakesStandard(b, settings, moves)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
if snake.EliminatedCause != NotEliminated {
|
if snake.EliminatedCause != NotEliminated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
snake.Body[0].X = replace(snake.Body[0].X, 0, b.Width-1)
|
snake.Body[0].X = wrap(snake.Body[0].X, 0, b.Width-1)
|
||||||
snake.Body[0].Y = replace(snake.Body[0].Y, 0, b.Height-1)
|
snake.Body[0].Y = wrap(snake.Body[0].Y, 0, b.Height-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(value, min, max int32) int32 {
|
||||||
|
if value < min {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package rules
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -247,6 +248,20 @@ func TestEdgeCrossingEating(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
// no wrap
|
||||||
|
assert.Equal(t, int32(0), wrap(0, 0, 0))
|
||||||
|
assert.Equal(t, int32(0), wrap(0, 1, 0))
|
||||||
|
assert.Equal(t, int32(0), wrap(0, 0, 1))
|
||||||
|
assert.Equal(t, int32(1), wrap(1, 0, 1))
|
||||||
|
|
||||||
|
// wrap to min
|
||||||
|
assert.Equal(t, int32(0), wrap(2, 0, 1))
|
||||||
|
|
||||||
|
// wrap to max
|
||||||
|
assert.Equal(t, int32(1), wrap(-1, 0, 1))
|
||||||
|
}
|
||||||
|
|
||||||
// Checks that snakes moving out of bounds get wrapped to the other side.
|
// Checks that snakes moving out of bounds get wrapped to the other side.
|
||||||
var wrappedCaseMoveAndWrap = gameTestCase{
|
var wrappedCaseMoveAndWrap = gameTestCase{
|
||||||
"Wrapped Case Move and Wrap",
|
"Wrapped Case Move and Wrap",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue