diff --git a/board.go b/board.go index 20ae30b..4b2c9bd 100644 --- a/board.go +++ b/board.go @@ -144,9 +144,70 @@ func PlaceSnakesFixed(rand Rand, b *BoardState, snakeIDs []string) error { } } + return nil } +func PlaceSnakesInQuadrants(rand Rand, b *BoardState, quadrants [][]Point) error { + + if len(quadrants) != 4 { + return RulesetError("invalid start point configuration - not divided into quadrants") + } + + // make sure all quadrants have the same number of positions + for i := 1; i < 4; i++ { + if len(quadrants[i]) != len(quadrants[0]) { + return RulesetError("invalid start point configuration - quadrants aren't even") + } + } + + quads := make([]randomPositionBucket, 4) + for i := 0; i < 4; i++ { + quads[i].fill(quadrants[i]...) + } + + currentQuad := rand.Intn(4) // randomly pick a quadrant to start from + + // evenly distribute snakes across quadrants, randomly, by rotating through the quadrants + for i := 0; i < len(b.Snakes); i++ { + p, err := quads[currentQuad].take(rand) + if err != nil { + return err + } + for j := 0; j < SnakeStartSize; j++ { + b.Snakes[i].Body = append(b.Snakes[i].Body, p) + } + + currentQuad = (currentQuad + 1) % 4 + } + + return nil +} + +type randomPositionBucket struct { + positions []Point +} + +func (rpb *randomPositionBucket) fill(p ...Point) { + rpb.positions = append(rpb.positions, p...) +} + +func (rpb *randomPositionBucket) take(rand Rand) (Point, error) { + if len(rpb.positions) == 0 { + return Point{}, RulesetError("no more positions available") + } + + // randomly pick the next position + idx := rand.Intn(len(rpb.positions)) + p := rpb.positions[idx] + + // remove that position from the list using the fast slice removal method + rpb.positions[idx] = rpb.positions[len(rpb.positions)-1] + rpb.positions = rpb.positions[:len(rpb.positions)-1] + + return p, nil +} + func PlaceSnakesRandomly(rand Rand, b *BoardState, snakeIDs []string) error { b.Snakes = make([]Snake, len(snakeIDs)) diff --git a/maps/hazards.go b/maps/hazards.go index 4ba0f66..46203a4 100644 --- a/maps/hazards.go +++ b/maps/hazards.go @@ -565,20 +565,68 @@ Each river has one or two 1-square "bridges" over them`, Author: "Battlesnake", Version: 1, MinPlayers: 1, - MaxPlayers: 8, + MaxPlayers: 12, BoardSizes: FixedSizes(Dimensions{11, 11}, Dimensions{19, 19}, Dimensions{25, 25}), } } func (m RiverAndBridgesHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if err := (StandardMap{}).SetupBoard(lastBoardState, settings, editor); err != nil { + width := lastBoardState.Width + height := lastBoardState.Height + hazards, ok := riversAndBridgesHazards[rules.Point{X: width, Y: height}] + if !ok { + return rules.RulesetError("board size is not supported by this map") + } + startPositions, ok := riversAndBridgesStartPositions[rules.Point{X: width, Y: height}] + if !ok { + return rules.RulesetError("board size is not supported by this map") + } + + numSnakes := len(lastBoardState.Snakes) + if numSnakes == 0 { + return rules.RulesetError("too few snakes - at least one snake must be present") + } + + rand := settings.GetRand(0) + + snakeIDs := make([]string, 0, len(lastBoardState.Snakes)) + for _, snake := range lastBoardState.Snakes { + snakeIDs = append(snakeIDs, snake.ID) + } + + tempBoardState := rules.NewBoardState(width, height) + tempBoardState.Snakes = make([]rules.Snake, len(snakeIDs)) + + for i := 0; i < len(snakeIDs); i++ { + tempBoardState.Snakes[i] = rules.Snake{ + ID: snakeIDs[i], + Health: rules.SnakeMaxHealth, + } + } + err := rules.PlaceSnakesInQuadrants(rand, tempBoardState, startPositions) + if err != nil { return err } - hazards, ok := riversAndBridgesMaps[rules.Point{X: lastBoardState.Width, Y: lastBoardState.Height}] - if !ok { - return rules.RulesetError("Board size is not supported by this map") + err = rules.PlaceFoodFixed(rand, tempBoardState) + if err != nil { + return err } + + // Copy food from temp board state + for _, f := range tempBoardState.Food { + // skip the center food + if f.X == lastBoardState.Width/2 && f.Y == lastBoardState.Height/2 { + continue + } + editor.AddFood(f) + } + + // Copy snakes from temp board state + for _, snake := range tempBoardState.Snakes { + editor.PlaceSnake(snake.ID, snake.Body, snake.Health) + } + for _, p := range hazards { editor.AddHazard(p) } @@ -587,10 +635,117 @@ func (m RiverAndBridgesHazardsMap) SetupBoard(lastBoardState *rules.BoardState, } func (m RiverAndBridgesHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - return StandardMap{}.UpdateBoard(lastBoardState, settings, editor) + rand := settings.GetRand(lastBoardState.Turn) + + foodNeeded := checkFoodNeedingPlacement(rand, settings, lastBoardState) + if foodNeeded > 0 { + pts := m.getUnoccupiedPoints(lastBoardState) + placeFoodRandomlyAtPositions(rand, lastBoardState, editor, foodNeeded, pts) + } + + return nil } -var riversAndBridgesMaps = map[rules.Point][]rules.Point{ +func (m RiverAndBridgesHazardsMap) getUnoccupiedPoints(lastBoardState *rules.BoardState) []rules.Point { + unoccupiedPoints := rules.GetUnoccupiedPoints(lastBoardState, false) + + var totallyUnoccupiedPoints []rules.Point + // we want to avoid placing food on hazards in this map + for _, p := range unoccupiedPoints { + isHazard := false + for _, h := range lastBoardState.Hazards { + if p == h { + isHazard = true + break + } + } + + if !isHazard { + totallyUnoccupiedPoints = append(totallyUnoccupiedPoints, p) + } + } + + return totallyUnoccupiedPoints +} + +var riversAndBridgesStartPositions = map[rules.Point][][]rules.Point{ + {X: 11, Y: 11}: { + { + {X: 1, Y: 1}, + {X: 3, Y: 3}, + {X: 1, Y: 3}, + }, + { + {X: 9, Y: 9}, + {X: 7, Y: 7}, + {X: 9, Y: 7}, + }, + { + {X: 1, Y: 9}, + {X: 3, Y: 7}, + {X: 3, Y: 9}, + }, + { + {X: 9, Y: 3}, + {X: 9, Y: 1}, + {X: 7, Y: 3}, + }, + }, + {X: 19, Y: 19}: { + { + {X: 1, Y: 1}, + {X: 5, Y: 1}, + {X: 1, Y: 5}, + {X: 5, Y: 5}, + }, + { + {X: 17, Y: 1}, + {X: 17, Y: 5}, + {X: 13, Y: 5}, + {X: 13, Y: 1}, + }, + { + {X: 1, Y: 17}, + {X: 5, Y: 17}, + {X: 1, Y: 13}, + {X: 5, Y: 13}, + }, + { + {X: 17, Y: 17}, + {X: 17, Y: 13}, + {X: 13, Y: 17}, + {X: 13, Y: 13}, + }, + }, + {X: 25, Y: 25}: { + { + {X: 1, Y: 1}, + {X: 9, Y: 9}, + {X: 9, Y: 1}, + {X: 1, Y: 9}, + }, + { + {X: 23, Y: 23}, + {X: 15, Y: 15}, + {X: 23, Y: 15}, + {X: 15, Y: 23}, + }, + { + {X: 15, Y: 1}, + {X: 15, Y: 9}, + {X: 23, Y: 9}, + {X: 23, Y: 1}, + }, + { + {X: 9, Y: 23}, + {X: 1, Y: 23}, + {X: 9, Y: 15}, + {X: 1, Y: 15}, + }, + }, +} + +var riversAndBridgesHazards = map[rules.Point][]rules.Point{ {X: 11, Y: 11}: { {X: 5, Y: 10}, {X: 5, Y: 9}, diff --git a/maps/hazards_internal_test.go b/maps/hazards_internal_test.go new file mode 100644 index 0000000..636049e --- /dev/null +++ b/maps/hazards_internal_test.go @@ -0,0 +1,46 @@ +package maps + +import ( + "fmt" + "testing" + + "github.com/BattlesnakeOfficial/rules" + "github.com/stretchr/testify/require" +) + +func TestRiversAndBridgesSnakePlacement(t *testing.T) { + m := RiverAndBridgesHazardsMap{} + settings := rules.Settings{} + + // check all the supported sizes + for _, size := range []int{11, 19, 25} { + initialState := rules.NewBoardState(size, size) + startPositions := riversAndBridgesStartPositions[rules.Point{X: size, Y: size}] + maxSnakes := len(startPositions) + for i := 0; i < maxSnakes; i++ { + initialState.Snakes = append(initialState.Snakes, rules.Snake{ID: fmt.Sprint(i), Body: []rules.Point{}}) + } + + nextState := rules.NewBoardState(size, size) + editor := NewBoardStateEditor(nextState) + err := m.SetupBoard(initialState, settings, editor) + require.NoError(t, err) + for _, s := range nextState.Snakes { + require.Len(t, s.Body, rules.SnakeStartSize, "Placed snakes should have the right length") + require.Equal(t, s.Health, rules.SnakeMaxHealth, "Placed snakes should have the right health") + require.NotEmpty(t, s.ID, "Snake ID shouldn't be empty (should get copied when placed)") + + // Check that the snake is placed at one of the specified start positions + validStart := false + for _, q := range startPositions { + for i := 0; i < len(q); i++ { + if q[i].X == s.Body[0].X && q[i].Y == s.Body[0].Y { + validStart = true + break + } + } + } + require.True(t, validStart, "Snake must be placed in one of the specified start positions") + } + } +} diff --git a/maps/hazards_test.go b/maps/hazards_test.go index 9dfd62c..1341512 100644 --- a/maps/hazards_test.go +++ b/maps/hazards_test.go @@ -103,13 +103,13 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) { // check all the supported sizes for _, size := range []int{11, 19, 25} { state = rules.NewBoardState(size, size) + state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}}) editor = maps.NewBoardStateEditor(state) require.Empty(t, state.Hazards) err = m.SetupBoard(state, settings, editor) require.NoError(t, err) require.NotEmpty(t, state.Hazards) } - } func TestSpiralHazardsMap(t *testing.T) { diff --git a/maps/standard.go b/maps/standard.go index 14b8051..10e769e 100644 --- a/maps/standard.go +++ b/maps/standard.go @@ -54,34 +54,45 @@ func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings ru func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(lastBoardState.Turn) - minFood := int(settings.MinimumFood) - foodSpawnChance := int(settings.FoodSpawnChance) - numCurrentFood := len(lastBoardState.Food) - if numCurrentFood < minFood { - placeFoodRandomly(rand, lastBoardState, editor, minFood-numCurrentFood) - return nil - } - if foodSpawnChance > 0 && (100-rand.Intn(100)) < foodSpawnChance { - placeFoodRandomly(rand, lastBoardState, editor, 1) - return nil + foodNeeded := checkFoodNeedingPlacement(rand, settings, lastBoardState) + if foodNeeded > 0 { + placeFoodRandomly(rand, lastBoardState, editor, foodNeeded) } return nil } -func placeFoodRandomly(rand rules.Rand, b *rules.BoardState, editor Editor, n int) { - unoccupiedPoints := rules.GetUnoccupiedPoints(b, false) +func checkFoodNeedingPlacement(rand rules.Rand, settings rules.Settings, state *rules.BoardState) int { + minFood := int(settings.MinimumFood) + foodSpawnChance := int(settings.FoodSpawnChance) + numCurrentFood := len(state.Food) - if len(unoccupiedPoints) < n { - n = len(unoccupiedPoints) + if numCurrentFood < minFood { + return minFood - numCurrentFood + } + if foodSpawnChance > 0 && (100-rand.Intn(100)) < foodSpawnChance { + return 1 } - rand.Shuffle(len(unoccupiedPoints), func(i int, j int) { - unoccupiedPoints[i], unoccupiedPoints[j] = unoccupiedPoints[j], unoccupiedPoints[i] + return 0 +} + +func placeFoodRandomly(rand rules.Rand, b *rules.BoardState, editor Editor, n int) { + unoccupiedPoints := rules.GetUnoccupiedPoints(b, false) + placeFoodRandomlyAtPositions(rand, b, editor, n, unoccupiedPoints) +} + +func placeFoodRandomlyAtPositions(rand rules.Rand, b *rules.BoardState, editor Editor, n int, positions []rules.Point) { + if len(positions) < n { + n = len(positions) + } + + rand.Shuffle(len(positions), func(i int, j int) { + positions[i], positions[j] = positions[j], positions[i] }) for i := 0; i < n; i++ { - editor.AddFood(unoccupiedPoints[i]) + editor.AddFood(positions[i]) } }