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 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{
|
||||
Use: "play",
|
||||
Short: "Play a game of Battlesnake locally.",
|
||||
Long: "Play a game of Battlesnake locally.",
|
||||
Run: run,
|
||||
PreRun: playPreRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -90,6 +101,10 @@ func init() {
|
|||
playCmd.Flags().SortFlags = false
|
||||
}
|
||||
|
||||
func playPreRun(cmd *cobra.Command, args []string) {
|
||||
initialiseGameConfig()
|
||||
}
|
||||
|
||||
var run = func(cmd *cobra.Command, args []string) {
|
||||
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 {
|
||||
var ruleset rules.Ruleset
|
||||
var royale rules.RoyaleRuleset
|
||||
rb := rules.NewRulesetBuilder().WithSeed(seed).WithParams(defaultConfig)
|
||||
|
||||
standard := rules.StandardRuleset{
|
||||
FoodSpawnChance: FoodSpawnChance,
|
||||
MinimumFood: MinimumFood,
|
||||
HazardDamagePerTurn: 0,
|
||||
for _, s := range snakeStates {
|
||||
rb.AddSnakeToSquad(s.ID, s.Squad)
|
||||
}
|
||||
|
||||
switch GameType {
|
||||
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
|
||||
return rb.Ruleset()
|
||||
|
||||
}
|
||||
|
||||
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{
|
||||
Name: ruleset.Name(),
|
||||
Version: "cli", // TODO: Use GitHub Release Version
|
||||
Settings: client.RulesetSettings{
|
||||
HazardDamagePerTurn: HazardDamagePerTurn,
|
||||
FoodSpawnChance: FoodSpawnChance,
|
||||
MinimumFood: MinimumFood,
|
||||
RoyaleSettings: client.RoyaleSettings{
|
||||
ShrinkEveryNTurns: ShrinkEveryNTurns,
|
||||
},
|
||||
SquadSettings: client.SquadSettings{
|
||||
AllowBodyCollisions: true,
|
||||
SharedElimination: true,
|
||||
SharedHealth: true,
|
||||
SharedLength: true,
|
||||
},
|
||||
},
|
||||
Settings: ruleset.Settings(),
|
||||
}}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
|
|
@ -35,8 +36,69 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
|||
s1State.ID: s1State,
|
||||
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)
|
||||
|
||||
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": "",
|
||||
"hazardMapAuthor": "",
|
||||
"royale": {
|
||||
"shrinkEveryNTurns": 25
|
||||
"shrinkEveryNTurns": 0
|
||||
},
|
||||
"squad": {
|
||||
"allowBodyCollisions": true,
|
||||
"sharedElimination": true,
|
||||
"sharedHealth": true,
|
||||
"sharedLength": true
|
||||
"allowBodyCollisions": false,
|
||||
"sharedElimination": false,
|
||||
"sharedHealth": false,
|
||||
"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
|
||||
|
||||
import "github.com/BattlesnakeOfficial/rules"
|
||||
|
||||
func exampleSnakeRequest() SnakeRequest {
|
||||
return SnakeRequest{
|
||||
Game: Game{
|
||||
|
|
@ -72,18 +74,18 @@ func exampleSnakeRequest() SnakeRequest {
|
|||
}
|
||||
}
|
||||
|
||||
var exampleRulesetSettings = RulesetSettings{
|
||||
var exampleRulesetSettings = rules.Settings{
|
||||
FoodSpawnChance: 10,
|
||||
MinimumFood: 20,
|
||||
HazardDamagePerTurn: 30,
|
||||
HazardMap: "hz_spiral",
|
||||
HazardMapAuthor: "altersaddle",
|
||||
|
||||
RoyaleSettings: RoyaleSettings{
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 40,
|
||||
},
|
||||
|
||||
SquadSettings: SquadSettings{
|
||||
SquadSettings: rules.SquadSettings{
|
||||
AllowBodyCollisions: true,
|
||||
SharedElimination: true,
|
||||
SharedHealth: true,
|
||||
|
|
|
|||
|
|
@ -50,29 +50,17 @@ type Customizations struct {
|
|||
type Ruleset struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Settings RulesetSettings `json:"settings"`
|
||||
Settings rules.Settings `json:"settings"`
|
||||
}
|
||||
|
||||
type RulesetSettings 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"`
|
||||
}
|
||||
// RulesetSettings is deprecated: use rules.Settings instead
|
||||
type RulesetSettings rules.Settings
|
||||
|
||||
type RoyaleSettings struct {
|
||||
ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"`
|
||||
}
|
||||
// RoyaleSettings is deprecated: use rules.RoyaleSettings instead
|
||||
type RoyaleSettings rules.RoyaleSettings
|
||||
|
||||
type SquadSettings struct {
|
||||
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
||||
SharedElimination bool `json:"sharedElimination"`
|
||||
SharedHealth bool `json:"sharedHealth"`
|
||||
SharedLength bool `json:"sharedLength"`
|
||||
}
|
||||
// SquadSettings is deprecated: use rules.SquadSettings instead
|
||||
type SquadSettings rules.SquadSettings
|
||||
|
||||
// Coord represents a point on the board
|
||||
type Coord struct {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/BattlesnakeOfficial/rules/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -18,7 +19,7 @@ func TestBuildSnakeRequestJSON(t *testing.T) {
|
|||
|
||||
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
||||
snakeRequest := exampleSnakeRequest()
|
||||
snakeRequest.Game.Ruleset.Settings = RulesetSettings{}
|
||||
snakeRequest.Game.Ruleset.Settings = rules.Settings{}
|
||||
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,29 +4,32 @@ type ConstrictorRuleset struct {
|
|||
StandardRuleset
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) Name() string { return "constrictor" }
|
||||
func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor }
|
||||
|
||||
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
||||
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newBoardState := initialBoardState.Clone()
|
||||
err = r.applyConstrictorRules(newBoardState)
|
||||
|
||||
r.removeFood(initialBoardState)
|
||||
|
||||
err = r.applyConstrictorRules(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newBoardState, nil
|
||||
return initialBoardState, nil
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
|
||||
nextState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.removeFood(nextState)
|
||||
|
||||
err = r.applyConstrictorRules(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -35,10 +38,23 @@ func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves [
|
|||
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
|
||||
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
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
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]
|
||||
subTail := b.Snakes[i].Body[len(b.Snakes[i].Body)-2]
|
||||
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
|
||||
}
|
||||
|
||||
func (r *RoyaleRuleset) Name() string { return "royale" }
|
||||
func (r *RoyaleRuleset) Name() string { return GameTypeRoyale }
|
||||
|
||||
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
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.
|
||||
err = r.populateHazards(nextBoardState, prevState.Turn+1)
|
||||
err = r.populateHazards(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -34,20 +34,28 @@ func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []Snak
|
|||
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{}
|
||||
|
||||
if r.ShrinkEveryNTurns < 1 {
|
||||
return errors.New("royale game can't shrink more frequently than every turn")
|
||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||
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 {
|
||||
return nil
|
||||
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
||||
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
|
||||
minY, maxY := int32(0), b.Height-1
|
||||
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 {
|
||||
b := &BoardState{
|
||||
Turn: test.Turn,
|
||||
Turn: test.Turn - 1,
|
||||
Width: test.Width,
|
||||
Height: test.Height,
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ func TestRoyaleHazards(t *testing.T) {
|
|||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||
}
|
||||
|
||||
err := r.populateHazards(b, test.Turn)
|
||||
err := r.populateHazards(b)
|
||||
require.Equal(t, test.Error, err)
|
||||
if err == nil {
|
||||
// Obstacles should match
|
||||
|
|
@ -131,7 +131,7 @@ func TestRoyalDamageNextTurn(t *testing.T) {
|
|||
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
||||
nextState := prevState.Clone()
|
||||
nextState.Turn = turn - 1
|
||||
err := r.populateHazards(nextState, turn)
|
||||
err := r.populateHazards(nextState)
|
||||
require.NoError(t, err)
|
||||
nextState.Turn = turn
|
||||
return nextState
|
||||
|
|
|
|||
181
ruleset.go
181
ruleset.go
|
|
@ -1,5 +1,9 @@
|
|||
package rules
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type RulesetError string
|
||||
|
||||
func (err RulesetError) Error() string { return string(err) }
|
||||
|
|
@ -24,6 +28,7 @@ const (
|
|||
EliminatedByOutOfHealth = "out-of-health"
|
||||
EliminatedByHeadToHeadCollision = "head-collision"
|
||||
EliminatedByOutOfBounds = "wall-collision"
|
||||
EliminatedBySquad = "squad-eliminated"
|
||||
|
||||
// TODO - Error consts
|
||||
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
||||
|
|
@ -31,8 +36,147 @@ const (
|
|||
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
||||
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
||||
ErrorZeroLengthSnake = RulesetError("snake is length zero")
|
||||
|
||||
// Ruleset / game type names
|
||||
GameTypeConstrictor = "constrictor"
|
||||
GameTypeRoyale = "royale"
|
||||
GameTypeSolo = "solo"
|
||||
GameTypeSquad = "squad"
|
||||
GameTypeStandard = "standard"
|
||||
GameTypeWrapped = "wrapped"
|
||||
|
||||
// Game creation parameter names
|
||||
ParamGameType = "name"
|
||||
ParamFoodSpawnChance = "foodSpawnChance"
|
||||
ParamMinimumFood = "minimumFood"
|
||||
ParamHazardDamagePerTurn = "damagePerTurn"
|
||||
ParamHazardMap = "hazardMap"
|
||||
ParamHazardMapAuthor = "hazardMapAuthor"
|
||||
ParamShrinkEveryNTurns = "shrinkEveryNTurns"
|
||||
ParamAllowBodyCollisions = "allowBodyCollisions"
|
||||
ParamSharedElimination = "sharedElimination"
|
||||
ParamSharedHealth = "sharedHealth"
|
||||
ParamSharedLength = "sharedLength"
|
||||
)
|
||||
|
||||
type rulesetBuilder struct {
|
||||
params map[string]string // game customisation parameters
|
||||
seed int64 // used for random events in games
|
||||
squads map[string]string // Snake ID -> Squad Name
|
||||
}
|
||||
|
||||
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
|
||||
func NewRulesetBuilder() *rulesetBuilder {
|
||||
return &rulesetBuilder{
|
||||
params: map[string]string{},
|
||||
squads: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithParams accepts a map of game parameters for customizing games.
|
||||
//
|
||||
// Parameters are copied. If called multiple times, parameters are merged such that:
|
||||
// - existing keys in both maps get overwritten by the new ones
|
||||
// - existing keys not present in the new map will be retained
|
||||
// - non-existing keys only in the new map will be added
|
||||
//
|
||||
// Unrecognised parameters will be ignored and default values will be used.
|
||||
// Invalid parameters (i.e. a non-numerical value where one is expected), will be ignored
|
||||
// and default values will be used.
|
||||
func (rb *rulesetBuilder) WithParams(params map[string]string) *rulesetBuilder {
|
||||
for k, v := range params {
|
||||
rb.params[k] = v
|
||||
}
|
||||
return rb
|
||||
}
|
||||
|
||||
// WithSeed sets the seed used for randomisation by certain game modes.
|
||||
func (rb *rulesetBuilder) WithSeed(seed int64) *rulesetBuilder {
|
||||
rb.seed = seed
|
||||
return rb
|
||||
}
|
||||
|
||||
// AddSnakeToSquad adds the specified snake (by ID) to a squad with the given name.
|
||||
// This configuration may be ignored by game modes if they do not support squads.
|
||||
func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBuilder {
|
||||
rb.squads[snakeID] = squadName
|
||||
return rb
|
||||
}
|
||||
|
||||
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
||||
func (rb rulesetBuilder) Ruleset() Ruleset {
|
||||
standardRuleset := &StandardRuleset{
|
||||
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
|
||||
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
|
||||
HazardDamagePerTurn: paramsInt32(rb.params, ParamHazardDamagePerTurn, 0),
|
||||
HazardMap: rb.params[ParamHazardMap],
|
||||
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
||||
}
|
||||
|
||||
name, ok := rb.params[ParamGameType]
|
||||
if !ok {
|
||||
return standardRuleset
|
||||
}
|
||||
|
||||
switch name {
|
||||
case GameTypeConstrictor:
|
||||
return &ConstrictorRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
}
|
||||
case GameTypeRoyale:
|
||||
return &RoyaleRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
Seed: rb.seed,
|
||||
ShrinkEveryNTurns: paramsInt32(rb.params, ParamShrinkEveryNTurns, 0),
|
||||
}
|
||||
case GameTypeSolo:
|
||||
return &SoloRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
}
|
||||
case GameTypeWrapped:
|
||||
return &WrappedRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
}
|
||||
case GameTypeSquad:
|
||||
squadMap := map[string]string{}
|
||||
for id, squad := range rb.squads {
|
||||
squadMap[id] = squad
|
||||
}
|
||||
return &SquadRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
SquadMap: squadMap,
|
||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
||||
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
||||
}
|
||||
}
|
||||
return standardRuleset
|
||||
}
|
||||
|
||||
// paramsBool returns the boolean value for the specified parameter.
|
||||
// If the parameter doesn't exist, the default value will be returned.
|
||||
// If the parameter does exist, but is not "true", false will be returned.
|
||||
func paramsBool(params map[string]string, paramName string, defaultValue bool) bool {
|
||||
if val, ok := params[paramName]; ok {
|
||||
return val == "true"
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// paramsInt32 returns the int32 value for the specified parameter.
|
||||
// If the parameter doesn't exist, the default value will be returned.
|
||||
// If the parameter does exist, but is not a valid int, the default value will be returned.
|
||||
func paramsInt32(params map[string]string, paramName string, defaultValue int32) int32 {
|
||||
if val, ok := params[paramName]; ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
return int32(i)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
X int32
|
||||
Y int32
|
||||
|
|
@ -57,4 +201,41 @@ type Ruleset interface {
|
|||
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
|
||||
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
|
||||
IsGameOver(state *BoardState) (bool, error)
|
||||
// Settings provides the game settings that are relevant to the ruleset.
|
||||
Settings() Settings
|
||||
}
|
||||
|
||||
// Settings contains all settings relevant to a game.
|
||||
// It is used by game logic to take a previous game state and produce a next game state.
|
||||
type Settings struct {
|
||||
FoodSpawnChance int32 `json:"foodSpawnChance"`
|
||||
MinimumFood int32 `json:"minimumFood"`
|
||||
HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"`
|
||||
HazardMap string `json:"hazardMap"`
|
||||
HazardMapAuthor string `json:"hazardMapAuthor"`
|
||||
RoyaleSettings RoyaleSettings `json:"royale"`
|
||||
SquadSettings SquadSettings `json:"squad"`
|
||||
}
|
||||
|
||||
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||
type RoyaleSettings struct {
|
||||
seed int64
|
||||
ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"`
|
||||
}
|
||||
|
||||
// SquadSettings contains settings that are specific to the "squad" game mode
|
||||
type SquadSettings struct {
|
||||
squadMap map[string]string
|
||||
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
||||
SharedElimination bool `json:"sharedElimination"`
|
||||
SharedHealth bool `json:"sharedHealth"`
|
||||
SharedLength bool `json:"sharedLength"`
|
||||
}
|
||||
|
||||
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
|
||||
// It is expected to modify the boardState directly.
|
||||
// The return values are a boolean (to indicate whether the game has ended as a result of the stage)
|
||||
// and an error if any errors occurred during the stage.
|
||||
//
|
||||
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
||||
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
|
||||
|
|
|
|||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
err := (error)(RulesetError("test error string"))
|
||||
require.Equal(t, "test error string", err.Error())
|
||||
func TestStandardRulesetSettings(t *testing.T) {
|
||||
ruleset := 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 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
|
||||
}
|
||||
|
||||
func (r *SoloRuleset) Name() string { return "solo" }
|
||||
func (r *SoloRuleset) Name() string { return GameTypeSolo }
|
||||
|
||||
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++ {
|
||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||
return false, nil
|
||||
|
|
|
|||
77
squad.go
77
squad.go
|
|
@ -16,9 +16,7 @@ type SquadRuleset struct {
|
|||
SharedLength bool
|
||||
}
|
||||
|
||||
const EliminatedBySquad = "squad-eliminated"
|
||||
|
||||
func (r *SquadRuleset) Name() string { return "squad" }
|
||||
func (r *SquadRuleset) Name() string { return GameTypeSquad }
|
||||
|
||||
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||
|
|
@ -26,13 +24,11 @@ func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []Snake
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
err = r.resurrectSquadBodyCollisions(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
err = r.shareSquadAttributes(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -41,38 +37,50 @@ func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []Snake
|
|||
return nextBoardState, nil
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) areSnakesOnSameSquad(snake *Snake, other *Snake) bool {
|
||||
return r.areSnakeIDsOnSameSquad(snake.ID, other.ID)
|
||||
func areSnakesOnSameSquad(squadMap map[string]string, snake *Snake, other *Snake) bool {
|
||||
return areSnakeIDsOnSameSquad(squadMap, snake.ID, other.ID)
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) areSnakeIDsOnSameSquad(snakeID string, otherID string) bool {
|
||||
return r.SquadMap[snakeID] == r.SquadMap[otherID]
|
||||
func areSnakeIDsOnSameSquad(squadMap map[string]string, snakeID string, otherID string) bool {
|
||||
return squadMap[snakeID] == squadMap[otherID]
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) resurrectSquadBodyCollisions(b *BoardState) error {
|
||||
if !r.AllowBodyCollisions {
|
||||
return nil
|
||||
_, err := r.callStageFunc(ResurrectSnakesSquad, b, []SnakeMove{})
|
||||
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++ {
|
||||
snake := &b.Snakes[i]
|
||||
if snake.EliminatedCause == EliminatedByCollision {
|
||||
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.EliminatedBy = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
||||
if !(r.SharedElimination || r.SharedLength || r.SharedHealth) {
|
||||
return nil
|
||||
_, err := r.callStageFunc(ShareAttributesSquad, b, []SnakeMove{})
|
||||
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++ {
|
||||
|
|
@ -83,21 +91,21 @@ func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
|||
|
||||
for j := 0; j < len(b.Snakes); j++ {
|
||||
other := &b.Snakes[j]
|
||||
if r.areSnakesOnSameSquad(snake, other) {
|
||||
if r.SharedHealth {
|
||||
if areSnakesOnSameSquad(squadSettings.squadMap, snake, other) {
|
||||
if squadSettings.SharedHealth {
|
||||
if snake.Health < other.Health {
|
||||
snake.Health = other.Health
|
||||
}
|
||||
}
|
||||
if r.SharedLength {
|
||||
if squadSettings.SharedLength {
|
||||
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) {
|
||||
r.growSnake(snake)
|
||||
growSnake(snake)
|
||||
}
|
||||
}
|
||||
if r.SharedElimination {
|
||||
if squadSettings.SharedElimination {
|
||||
if snake.EliminatedCause == NotEliminated && other.EliminatedCause != NotEliminated {
|
||||
snake.EliminatedCause = EliminatedBySquad
|
||||
// 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) {
|
||||
return r.callStageFunc(GameOverSquad, b, []SnakeMove{})
|
||||
}
|
||||
|
||||
func GameOverSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
snakesRemaining := []*Snake{}
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
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++ {
|
||||
if !r.areSnakesOnSameSquad(snakesRemaining[i], snakesRemaining[0]) {
|
||||
if !areSnakesOnSameSquad(settings.SquadSettings.squadMap, snakesRemaining[i], snakesRemaining[0]) {
|
||||
// There are multiple squads remaining
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -128,3 +140,20 @@ func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|||
// no snakes or single squad remaining
|
||||
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) {
|
||||
boardState := &BoardState{
|
||||
Snakes: []Snake{
|
||||
Snake{ID: "1", EliminatedCause: EliminatedByCollision},
|
||||
Snake{ID: "2"},
|
||||
{ID: "1", EliminatedCause: EliminatedByCollision},
|
||||
{ID: "2"},
|
||||
},
|
||||
}
|
||||
r := SquadRuleset{
|
||||
|
|
@ -280,8 +280,8 @@ func TestSquadSharedElimination(t *testing.T) {
|
|||
func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
||||
boardState := &BoardState{
|
||||
Snakes: []Snake{
|
||||
Snake{ID: "1"},
|
||||
Snake{ID: "2"},
|
||||
{ID: "1"},
|
||||
{ID: "2"},
|
||||
},
|
||||
}
|
||||
r := SquadRuleset{
|
||||
|
|
|
|||
130
standard.go
130
standard.go
|
|
@ -9,9 +9,11 @@ type StandardRuleset struct {
|
|||
FoodSpawnChance int32 // [0, 100]
|
||||
MinimumFood 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) {
|
||||
// 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.
|
||||
nextState := prevState.Clone()
|
||||
|
||||
// TODO: Gut check the BoardState?
|
||||
|
||||
// TODO: LOG?
|
||||
err := r.moveSnakes(nextState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
err = r.reduceSnakeHealth(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -41,7 +39,6 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
// 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.
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
err = r.maybeSpawnFood(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: LOG?
|
||||
err = r.maybeEliminateSnakes(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -68,6 +63,16 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn
|
|||
}
|
||||
|
||||
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.
|
||||
for i := 0; i < len(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 {
|
||||
return ErrorZeroLengthSnake
|
||||
return false, ErrorZeroLengthSnake
|
||||
}
|
||||
moveFound := false
|
||||
for _, move := range moves {
|
||||
|
|
@ -86,7 +91,7 @@ func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
|||
}
|
||||
}
|
||||
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:
|
||||
break
|
||||
default:
|
||||
appliedMove = r.getDefaultMove(snake.Body)
|
||||
appliedMove = getDefaultMove(snake.Body)
|
||||
}
|
||||
|
||||
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 {
|
||||
// Use neck to determine last move made
|
||||
head, neck := snakeBody[0], snakeBody[1]
|
||||
|
|
@ -160,15 +165,25 @@ func (r *StandardRuleset) getDefaultMove(snakeBody []Point) string {
|
|||
}
|
||||
|
||||
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++ {
|
||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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++ {
|
||||
snake := &b.Snakes[i]
|
||||
if snake.EliminatedCause != NotEliminated {
|
||||
|
|
@ -189,21 +204,26 @@ func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error {
|
|||
}
|
||||
|
||||
// Snake is in a hazard, reduce health
|
||||
snake.Health = snake.Health - r.HazardDamagePerTurn
|
||||
snake.Health = snake.Health - settings.HazardDamagePerTurn
|
||||
if snake.Health < 0 {
|
||||
snake.Health = 0
|
||||
}
|
||||
if r.snakeIsOutOfHealth(snake) {
|
||||
if snakeIsOutOfHealth(snake) {
|
||||
snake.EliminatedCause = EliminatedByOutOfHealth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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.
|
||||
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
||||
snakeIndicesByLength := make([]int, len(b.Snakes))
|
||||
|
|
@ -224,15 +244,15 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
|||
continue
|
||||
}
|
||||
if len(snake.Body) <= 0 {
|
||||
return ErrorZeroLengthSnake
|
||||
return false, ErrorZeroLengthSnake
|
||||
}
|
||||
|
||||
if r.snakeIsOutOfHealth(snake) {
|
||||
if snakeIsOutOfHealth(snake) {
|
||||
snake.EliminatedCause = EliminatedByOutOfHealth
|
||||
continue
|
||||
}
|
||||
|
||||
if r.snakeIsOutOfBounds(snake, b.Width, b.Height) {
|
||||
if snakeIsOutOfBounds(snake, b.Width, b.Height) {
|
||||
snake.EliminatedCause = EliminatedByOutOfBounds
|
||||
continue
|
||||
}
|
||||
|
|
@ -252,11 +272,11 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
|||
continue
|
||||
}
|
||||
if len(snake.Body) <= 0 {
|
||||
return ErrorZeroLengthSnake
|
||||
return false, ErrorZeroLengthSnake
|
||||
}
|
||||
|
||||
// Check for self-collisions first
|
||||
if r.snakeHasBodyCollided(snake, snake) {
|
||||
if snakeHasBodyCollided(snake, snake) {
|
||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||
ID: snake.ID,
|
||||
Cause: EliminatedBySelfCollision,
|
||||
|
|
@ -272,7 +292,7 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
|||
if other.EliminatedCause != NotEliminated {
|
||||
continue
|
||||
}
|
||||
if snake.ID != other.ID && r.snakeHasBodyCollided(snake, other) {
|
||||
if snake.ID != other.ID && snakeHasBodyCollided(snake, other) {
|
||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||
ID: snake.ID,
|
||||
Cause: EliminatedByCollision,
|
||||
|
|
@ -293,7 +313,7 @@ func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
|||
if other.EliminatedCause != NotEliminated {
|
||||
continue
|
||||
}
|
||||
if snake.ID != other.ID && r.snakeHasLostHeadToHead(snake, other) {
|
||||
if snake.ID != other.ID && snakeHasLostHeadToHead(snake, other) {
|
||||
collisionEliminations = append(collisionEliminations, CollisionElimination{
|
||||
ID: snake.ID,
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if (point.X < 0) || (point.X >= boardWidth) {
|
||||
return true
|
||||
|
|
@ -339,7 +359,7 @@ func (r *StandardRuleset) snakeIsOutOfBounds(s *Snake, boardWidth int32, boardHe
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
||||
func snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
||||
head := s.Body[0]
|
||||
for i, body := range other.Body {
|
||||
if i == 0 {
|
||||
|
|
@ -351,7 +371,7 @@ func (r *StandardRuleset) snakeHasBodyCollided(s *Snake, other *Snake) bool {
|
|||
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 {
|
||||
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 {
|
||||
_, err := r.callStageFunc(FeedSnakesStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func FeedSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
newFood := []Point{}
|
||||
for _, food := range b.Food {
|
||||
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 {
|
||||
r.feedSnake(snake)
|
||||
feedSnake(snake)
|
||||
foodHasBeenEaten = true
|
||||
}
|
||||
}
|
||||
|
|
@ -382,31 +407,41 @@ func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
|||
}
|
||||
|
||||
b.Food = newFood
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) feedSnake(snake *Snake) {
|
||||
r.growSnake(snake)
|
||||
func feedSnake(snake *Snake) {
|
||||
growSnake(snake)
|
||||
snake.Health = SnakeMaxHealth
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) growSnake(snake *Snake) {
|
||||
func growSnake(snake *Snake) {
|
||||
if len(snake.Body) > 0 {
|
||||
snake.Body = append(snake.Body, snake.Body[len(snake.Body)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
|
||||
numCurrentFood := int32(len(b.Food))
|
||||
if numCurrentFood < r.MinimumFood {
|
||||
return PlaceFoodRandomly(b, r.MinimumFood-numCurrentFood)
|
||||
} else if r.FoodSpawnChance > 0 && int32(rand.Intn(100)) < r.FoodSpawnChance {
|
||||
return PlaceFoodRandomly(b, 1)
|
||||
_, err := r.callStageFunc(SpawnFoodStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
return r.callStageFunc(GameOverStandard, b, []SnakeMove{})
|
||||
}
|
||||
|
||||
func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
numSnakesRemaining := 0
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||
|
|
@ -415,3 +450,18 @@ func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|||
}
|
||||
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}},
|
||||
Hazards: []Point{},
|
||||
},
|
||||
[]SnakeMove{},
|
||||
[]SnakeMove{
|
||||
{ID: "one", Move: MoveUp},
|
||||
},
|
||||
ErrorNoMoveFound,
|
||||
nil,
|
||||
}
|
||||
|
|
@ -767,9 +769,8 @@ func TestGetDefaultMove(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
for _, test := range tests {
|
||||
actualMove := r.getDefaultMove(test.SnakeBody)
|
||||
actualMove := getDefaultMove(test.SnakeBody)
|
||||
require.Equal(t, test.ExpectedMove, actualMove)
|
||||
}
|
||||
}
|
||||
|
|
@ -835,10 +836,9 @@ func TestSnakeIsOutOfHealth(t *testing.T) {
|
|||
{Health: math.MaxInt32, Expected: false},
|
||||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
for _, test := range tests {
|
||||
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},
|
||||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
for _, test := range tests {
|
||||
// Test with point as head
|
||||
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
|
||||
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},
|
||||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
for _, test := range tests {
|
||||
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 {
|
||||
s := &Snake{Body: test.SnakeBody}
|
||||
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 {
|
||||
s := Snake{Body: test.SnakeBody}
|
||||
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.ExpectedOpposite, r.snakeHasLostHeadToHead(&o, &s), "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, 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
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) Name() string { return "wrapped" }
|
||||
|
||||
func replace(value, min, max int32) int32 {
|
||||
if value < min {
|
||||
return max
|
||||
}
|
||||
if value > max {
|
||||
return min
|
||||
}
|
||||
return value
|
||||
}
|
||||
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
||||
|
||||
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
nextState := prevState.Clone()
|
||||
|
|
@ -53,19 +43,34 @@ func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []Sna
|
|||
}
|
||||
|
||||
func (r *WrappedRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||
err := r.StandardRuleset.moveSnakes(b, moves)
|
||||
if err != nil {
|
||||
_, err := r.callStageFunc(MoveSnakesWrapped, b, moves)
|
||||
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++ {
|
||||
snake := &b.Snakes[i]
|
||||
if snake.EliminatedCause != NotEliminated {
|
||||
continue
|
||||
}
|
||||
snake.Body[0].X = replace(snake.Body[0].X, 0, b.Width-1)
|
||||
snake.Body[0].Y = replace(snake.Body[0].Y, 0, b.Height-1)
|
||||
snake.Body[0].X = wrap(snake.Body[0].X, 0, b.Width-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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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.
|
||||
var wrappedCaseMoveAndWrap = gameTestCase{
|
||||
"Wrapped Case Move and Wrap",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue