diff --git a/board.go b/board.go index b3f0e1f..d9ab796 100644 --- a/board.go +++ b/board.go @@ -189,11 +189,11 @@ func PlaceManySnakesDistributed(rand Rand, b *BoardState, snakeIDs []string) err hOffset := quadHSpace / 3 vOffset := quadVSpace / 3 - quads := make([]randomPositionBucket, 4) + quads := make([]RandomPositionBucket, 4) // quad 1 - quads[0] = randomPositionBucket{} - quads[0].fill( + quads[0] = RandomPositionBucket{} + quads[0].Fill( Point{X: hOffset, Y: vOffset}, Point{X: quadHSpace - hOffset, Y: vOffset}, Point{X: hOffset, Y: quadVSpace - vOffset}, @@ -201,27 +201,27 @@ func PlaceManySnakesDistributed(rand Rand, b *BoardState, snakeIDs []string) err ) // quad 2 - quads[1] = randomPositionBucket{} + quads[1] = RandomPositionBucket{} for _, p := range quads[0].positions { - quads[1].fill(Point{X: b.Width - p.X - 1, Y: p.Y}) + quads[1].Fill(Point{X: b.Width - p.X - 1, Y: p.Y}) } // quad 3 - quads[2] = randomPositionBucket{} + quads[2] = RandomPositionBucket{} for _, p := range quads[0].positions { - quads[2].fill(Point{X: p.X, Y: b.Height - p.Y - 1}) + quads[2].Fill(Point{X: p.X, Y: b.Height - p.Y - 1}) } // quad 4 - quads[3] = randomPositionBucket{} + quads[3] = RandomPositionBucket{} for _, p := range quads[0].positions { - quads[3].fill(Point{X: b.Width - p.X - 1, Y: b.Height - p.Y - 1}) + quads[3].Fill(Point{X: b.Width - p.X - 1, Y: b.Height - p.Y - 1}) } 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) + p, err := quads[currentQuad].Take(rand) if err != nil { return err } @@ -235,51 +235,15 @@ func PlaceManySnakesDistributed(rand Rand, b *BoardState, snakeIDs []string) err 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 { +type RandomPositionBucket struct { positions []Point } -func (rpb *randomPositionBucket) fill(p ...Point) { +func (rpb *RandomPositionBucket) Fill(p ...Point) { rpb.positions = append(rpb.positions, p...) } -func (rpb *randomPositionBucket) take(rand Rand) (Point, error) { +func (rpb *RandomPositionBucket) Take(rand Rand) (Point, error) { if len(rpb.positions) == 0 { return Point{}, RulesetError("no more positions available") } @@ -359,6 +323,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error { return PlaceFoodRandomly(rand, b, len(b.Snakes)) } +// Deprecated: will be replaced by maps.PlaceFoodFixed func PlaceFoodFixed(rand Rand, b *BoardState) error { centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} diff --git a/maps/helpers.go b/maps/helpers.go index 7906625..68b190c 100644 --- a/maps/helpers.go +++ b/maps/helpers.go @@ -177,3 +177,112 @@ func isOnBoard(w, h, x, y int) bool { return true } + +func PlaceSnakesInQuadrants(rand rules.Rand, editor Editor, snakes []rules.Snake, quadrants [][]rules.Point) error { + if len(quadrants) != 4 { + return rules.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 rules.RulesetError("invalid start point configuration - quadrants aren't even") + } + } + + quads := make([]rules.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 _, snake := range snakes { + p, err := quads[currentQuad].Take(rand) + if err != nil { + return err + } + + editor.PlaceSnake(snake.ID, []rules.Point{p, p, p}, rules.SnakeMaxHealth) + + currentQuad = (currentQuad + 1) % 4 + } + + return nil +} + +func PlaceFoodFixed(rand rules.Rand, initialBoardState *rules.BoardState, editor Editor) error { + width, height := initialBoardState.Width, initialBoardState.Height + centerCoord := rules.Point{X: (width - 1) / 2, Y: (height - 1) / 2} + + isSmallBoard := width*height < rules.BoardSizeMedium*rules.BoardSizeMedium + + // Up to 4 snakes can be placed such that food is nearby on small boards. + // Otherwise, we skip this and only try to place food in the center. + snakeBodies := editor.SnakeBodies() + if len(snakeBodies) <= 4 || !isSmallBoard { + // Place 1 food within exactly 2 moves of each snake, but never towards the center or in a corner + for _, snakeBody := range snakeBodies { + snakeHead := snakeBody[0] + possibleFoodLocations := []rules.Point{ + {X: snakeHead.X - 1, Y: snakeHead.Y - 1}, + {X: snakeHead.X - 1, Y: snakeHead.Y + 1}, + {X: snakeHead.X + 1, Y: snakeHead.Y - 1}, + {X: snakeHead.X + 1, Y: snakeHead.Y + 1}, + } + + // Remove any invalid/unwanted positions + availableFoodLocations := []rules.Point{} + for _, p := range possibleFoodLocations { + + // Don't place in the center + if centerCoord == p { + continue + } + + // Ignore points already occupied by food or hazards + if editor.IsOccupied(p, true, true, true) { + continue + } + + // Food must be further than snake from center on at least one axis + isAwayFromCenter := false + if p.X < snakeHead.X && snakeHead.X < centerCoord.X { + isAwayFromCenter = true + } else if centerCoord.X < snakeHead.X && snakeHead.X < p.X { + isAwayFromCenter = true + } else if p.Y < snakeHead.Y && snakeHead.Y < centerCoord.Y { + isAwayFromCenter = true + } else if centerCoord.Y < snakeHead.Y && snakeHead.Y < p.Y { + isAwayFromCenter = true + } + if !isAwayFromCenter { + continue + } + + // Don't spawn food in corners + if (p.X == 0 || p.X == (width-1)) && (p.Y == 0 || p.Y == (height-1)) { + continue + } + + availableFoodLocations = append(availableFoodLocations, p) + } + + if len(availableFoodLocations) <= 0 { + return rules.ErrorNoRoomForFood + } + + // Select randomly from available locations + placedFood := availableFoodLocations[rand.Intn(len(availableFoodLocations))] + editor.AddFood(placedFood) + } + } + + // Finally, try to place 1 food in center of board for dramatic purposes + if !editor.IsOccupied(centerCoord, true, true, true) { + editor.AddFood(centerCoord) + } + + return nil +} diff --git a/maps/helpers_test.go b/maps/helpers_test.go index 1d4fc0d..fdf7e5b 100644 --- a/maps/helpers_test.go +++ b/maps/helpers_test.go @@ -115,3 +115,33 @@ func TestUpdateBoard(t *testing.T) { require.Equal(t, []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards) }) } + +func TestPlaceFoodFixed(t *testing.T) { + initialBoardState := rules.NewBoardState(rules.BoardSizeMedium, rules.BoardSizeMedium) + editor := maps.NewBoardStateEditor(initialBoardState.Clone()) + + editor.PlaceSnake("1", []rules.Point{{X: 1, Y: 1}}, 100) + editor.PlaceSnake("2", []rules.Point{{X: 9, Y: 1}}, 100) + editor.PlaceSnake("3", []rules.Point{{X: 4, Y: 9}}, 100) + editor.PlaceSnake("4", []rules.Point{{X: 6, Y: 6}}, 100) + + // Hazards everywhere except for the expected food placements + for x := 0; x < initialBoardState.Width; x++ { + for y := 0; y < initialBoardState.Height; y++ { + editor.AddHazard(rules.Point{X: x, Y: y}) + } + } + editor.RemoveHazard(rules.Point{X: 0, Y: 2}) + editor.RemoveHazard(rules.Point{X: 8, Y: 0}) + editor.RemoveHazard(rules.Point{X: 3, Y: 10}) + editor.RemoveHazard(rules.Point{X: 7, Y: 7}) + + err := maps.PlaceFoodFixed(rules.MaxRand, initialBoardState, editor) + require.NoError(t, err) + + food := editor.Food() + require.Contains(t, food, rules.Point{X: 0, Y: 2}) + require.Contains(t, food, rules.Point{X: 8, Y: 0}) + require.Contains(t, food, rules.Point{X: 3, Y: 10}) + require.Contains(t, food, rules.Point{X: 7, Y: 7}) +} diff --git a/maps/rivers_and_bridges.go b/maps/rivers_and_bridges.go index 8f542db..a27b119 100644 --- a/maps/rivers_and_bridges.go +++ b/maps/rivers_and_bridges.go @@ -12,59 +12,23 @@ func init() { globalRegistry.RegisterMap("hz_islands_bridges_lg", IslandsAndBridgesLargeHazardsMap{}) } -func setupRiverAndBridgesBoard(startingPositions [][]rules.Point, hazards []rules.Point, lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - width := lastBoardState.Width - height := lastBoardState.Height - - numSnakes := len(lastBoardState.Snakes) - if numSnakes == 0 { - return rules.RulesetError("too few snakes - at least one snake must be present") - } - +func setupRiverAndBridgesBoard(startingPositions [][]rules.Point, hazards []rules.Point, initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { 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, startingPositions) + err := PlaceSnakesInQuadrants(rand, editor, initialBoardState.Snakes, startingPositions) if err != nil { return err } - 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) } + err = PlaceFoodFixed(rand, initialBoardState, editor) + if err != nil { + return err + } + return nil } @@ -100,12 +64,11 @@ Each river has one or two 1-square "bridges" over them`, } } -func (m RiverAndBridgesMediumHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(lastBoardState.Width, lastBoardState.Height) { - return rules.RulesetError("This map can only be played on a 11x11 board") +func (m RiverAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } - - return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, lastBoardState, settings, editor) + return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, initialBoardState, settings, editor) } func (m RiverAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { @@ -171,12 +134,12 @@ Each river has one or two 1-square "bridges" over them`, } } -func (m RiverAndBridgesLargeHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(lastBoardState.Width, lastBoardState.Height) { - return rules.RulesetError("This map can only be played on a 19x19 board") +func (m RiverAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } - return setupRiverAndBridgesBoard(riversAndBridgesLargeStartPositions, riversAndBridgesLargeHazards, lastBoardState, settings, editor) + return setupRiverAndBridgesBoard(riversAndBridgesLargeStartPositions, riversAndBridgesLargeHazards, initialBoardState, settings, editor) } func (m RiverAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { @@ -270,12 +233,12 @@ Each river has one or two 1-square "bridges" over them`, } } -func (m RiverAndBridgesExtraLargeHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(lastBoardState.Width, lastBoardState.Height) { - return rules.RulesetError("This map can only be played on a 25x25 board") +func (m RiverAndBridgesExtraLargeHazardsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } - return setupRiverAndBridgesBoard(riversAndBridgesExtraLargeStartPositions, riversAndBridgesExtraLargeHazards, lastBoardState, settings, editor) + return setupRiverAndBridgesBoard(riversAndBridgesExtraLargeStartPositions, riversAndBridgesExtraLargeHazards, initialBoardState, settings, editor) } func (m RiverAndBridgesExtraLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { @@ -384,12 +347,12 @@ func (m IslandsAndBridgesMediumHazardsMap) Meta() Metadata { } } -func (m IslandsAndBridgesMediumHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(lastBoardState.Width, lastBoardState.Height) { - return rules.RulesetError("This map can only be played on a 11x11 board") +func (m IslandsAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } - return setupRiverAndBridgesBoard(islandsAndBridgesMediumStartPositions, islandsAndBridgesMediumHazards, lastBoardState, settings, editor) + return setupRiverAndBridgesBoard(islandsAndBridgesMediumStartPositions, islandsAndBridgesMediumHazards, initialBoardState, settings, editor) } func (m IslandsAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { @@ -470,12 +433,12 @@ func (m IslandsAndBridgesLargeHazardsMap) Meta() Metadata { } } -func (m IslandsAndBridgesLargeHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(lastBoardState.Width, lastBoardState.Height) { - return rules.RulesetError("This map can only be played on a 19x19 board") +func (m IslandsAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } - return setupRiverAndBridgesBoard(islandsAndBridgesLargeStartPositions, islandsAndBridgesLargeHazards, lastBoardState, settings, editor) + return setupRiverAndBridgesBoard(islandsAndBridgesLargeStartPositions, islandsAndBridgesLargeHazards, initialBoardState, settings, editor) } func (m IslandsAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { diff --git a/maps/rivers_and_bridges_test.go b/maps/rivers_and_bridges_test.go index 4d9e675..f3cbb8c 100644 --- a/maps/rivers_and_bridges_test.go +++ b/maps/rivers_and_bridges_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestRiversAndBridgetsHazardsMap(t *testing.T) { +func TestRiversAndBridgesHazardsMap(t *testing.T) { // check error handling m := maps.RiverAndBridgesMediumHazardsMap{} settings := rules.Settings{} @@ -40,5 +40,8 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) { err = test.Map.SetupBoard(state, settings, editor) require.NoError(t, err) require.NotEmpty(t, state.Hazards) + require.Len(t, state.Food, 1) + food := state.Food[0] + require.NotContains(t, state.Hazards, food) } }