DEV-765 pipeline refactor (#64)

Refactor rulesets into smaller composable operations

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

View file

@ -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,
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 {
@ -382,22 +366,9 @@ func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
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,
},
},
Name: ruleset.Name(),
Version: "cli", // TODO: Use GitHub Release Version
Settings: ruleset.Settings(),
}}
}

View file

@ -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))
})
}
}

View file

@ -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
}
}
},

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}