Skip to content

Commit e6e36ce

Browse files
authored
DEV-1703: Avoid spawning food on hazards for islands and bridges map (#112)
* move PlaceFoodFixed and PlaceSnakesInQuadrants to maps package * don't spawn food on hazards in islands/rivers and bridges maps
1 parent 35e5a53 commit e6e36ce

File tree

5 files changed

+184
-114
lines changed

5 files changed

+184
-114
lines changed

board.go

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -189,39 +189,39 @@ func PlaceManySnakesDistributed(rand Rand, b *BoardState, snakeIDs []string) err
189189
hOffset := quadHSpace / 3
190190
vOffset := quadVSpace / 3
191191

192-
quads := make([]randomPositionBucket, 4)
192+
quads := make([]RandomPositionBucket, 4)
193193

194194
// quad 1
195-
quads[0] = randomPositionBucket{}
196-
quads[0].fill(
195+
quads[0] = RandomPositionBucket{}
196+
quads[0].Fill(
197197
Point{X: hOffset, Y: vOffset},
198198
Point{X: quadHSpace - hOffset, Y: vOffset},
199199
Point{X: hOffset, Y: quadVSpace - vOffset},
200200
Point{X: quadHSpace - hOffset, Y: quadVSpace - vOffset},
201201
)
202202

203203
// quad 2
204-
quads[1] = randomPositionBucket{}
204+
quads[1] = RandomPositionBucket{}
205205
for _, p := range quads[0].positions {
206-
quads[1].fill(Point{X: b.Width - p.X - 1, Y: p.Y})
206+
quads[1].Fill(Point{X: b.Width - p.X - 1, Y: p.Y})
207207
}
208208

209209
// quad 3
210-
quads[2] = randomPositionBucket{}
210+
quads[2] = RandomPositionBucket{}
211211
for _, p := range quads[0].positions {
212-
quads[2].fill(Point{X: p.X, Y: b.Height - p.Y - 1})
212+
quads[2].Fill(Point{X: p.X, Y: b.Height - p.Y - 1})
213213
}
214214

215215
// quad 4
216-
quads[3] = randomPositionBucket{}
216+
quads[3] = RandomPositionBucket{}
217217
for _, p := range quads[0].positions {
218-
quads[3].fill(Point{X: b.Width - p.X - 1, Y: b.Height - p.Y - 1})
218+
quads[3].Fill(Point{X: b.Width - p.X - 1, Y: b.Height - p.Y - 1})
219219
}
220220

221221
currentQuad := rand.Intn(4) // randomly pick a quadrant to start from
222222
// evenly distribute snakes across quadrants, randomly, by rotating through the quadrants
223223
for i := 0; i < len(b.Snakes); i++ {
224-
p, err := quads[currentQuad].take(rand)
224+
p, err := quads[currentQuad].Take(rand)
225225
if err != nil {
226226
return err
227227
}
@@ -235,51 +235,15 @@ func PlaceManySnakesDistributed(rand Rand, b *BoardState, snakeIDs []string) err
235235
return nil
236236
}
237237

238-
func PlaceSnakesInQuadrants(rand Rand, b *BoardState, quadrants [][]Point) error {
239-
240-
if len(quadrants) != 4 {
241-
return RulesetError("invalid start point configuration - not divided into quadrants")
242-
}
243-
244-
// make sure all quadrants have the same number of positions
245-
for i := 1; i < 4; i++ {
246-
if len(quadrants[i]) != len(quadrants[0]) {
247-
return RulesetError("invalid start point configuration - quadrants aren't even")
248-
}
249-
}
250-
251-
quads := make([]randomPositionBucket, 4)
252-
for i := 0; i < 4; i++ {
253-
quads[i].fill(quadrants[i]...)
254-
}
255-
256-
currentQuad := rand.Intn(4) // randomly pick a quadrant to start from
257-
258-
// evenly distribute snakes across quadrants, randomly, by rotating through the quadrants
259-
for i := 0; i < len(b.Snakes); i++ {
260-
p, err := quads[currentQuad].take(rand)
261-
if err != nil {
262-
return err
263-
}
264-
for j := 0; j < SnakeStartSize; j++ {
265-
b.Snakes[i].Body = append(b.Snakes[i].Body, p)
266-
}
267-
268-
currentQuad = (currentQuad + 1) % 4
269-
}
270-
271-
return nil
272-
}
273-
274-
type randomPositionBucket struct {
238+
type RandomPositionBucket struct {
275239
positions []Point
276240
}
277241

278-
func (rpb *randomPositionBucket) fill(p ...Point) {
242+
func (rpb *RandomPositionBucket) Fill(p ...Point) {
279243
rpb.positions = append(rpb.positions, p...)
280244
}
281245

282-
func (rpb *randomPositionBucket) take(rand Rand) (Point, error) {
246+
func (rpb *RandomPositionBucket) Take(rand Rand) (Point, error) {
283247
if len(rpb.positions) == 0 {
284248
return Point{}, RulesetError("no more positions available")
285249
}
@@ -359,6 +323,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error {
359323
return PlaceFoodRandomly(rand, b, len(b.Snakes))
360324
}
361325

326+
// Deprecated: will be replaced by maps.PlaceFoodFixed
362327
func PlaceFoodFixed(rand Rand, b *BoardState) error {
363328
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
364329

maps/helpers.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,112 @@ func isOnBoard(w, h, x, y int) bool {
177177

178178
return true
179179
}
180+
181+
func PlaceSnakesInQuadrants(rand rules.Rand, editor Editor, snakes []rules.Snake, quadrants [][]rules.Point) error {
182+
if len(quadrants) != 4 {
183+
return rules.RulesetError("invalid start point configuration - not divided into quadrants")
184+
}
185+
186+
// make sure all quadrants have the same number of positions
187+
for i := 1; i < 4; i++ {
188+
if len(quadrants[i]) != len(quadrants[0]) {
189+
return rules.RulesetError("invalid start point configuration - quadrants aren't even")
190+
}
191+
}
192+
193+
quads := make([]rules.RandomPositionBucket, 4)
194+
for i := 0; i < 4; i++ {
195+
quads[i].Fill(quadrants[i]...)
196+
}
197+
198+
currentQuad := rand.Intn(4) // randomly pick a quadrant to start from
199+
200+
// evenly distribute snakes across quadrants, randomly, by rotating through the quadrants
201+
for _, snake := range snakes {
202+
p, err := quads[currentQuad].Take(rand)
203+
if err != nil {
204+
return err
205+
}
206+
207+
editor.PlaceSnake(snake.ID, []rules.Point{p, p, p}, rules.SnakeMaxHealth)
208+
209+
currentQuad = (currentQuad + 1) % 4
210+
}
211+
212+
return nil
213+
}
214+
215+
func PlaceFoodFixed(rand rules.Rand, initialBoardState *rules.BoardState, editor Editor) error {
216+
width, height := initialBoardState.Width, initialBoardState.Height
217+
centerCoord := rules.Point{X: (width - 1) / 2, Y: (height - 1) / 2}
218+
219+
isSmallBoard := width*height < rules.BoardSizeMedium*rules.BoardSizeMedium
220+
221+
// Up to 4 snakes can be placed such that food is nearby on small boards.
222+
// Otherwise, we skip this and only try to place food in the center.
223+
snakeBodies := editor.SnakeBodies()
224+
if len(snakeBodies) <= 4 || !isSmallBoard {
225+
// Place 1 food within exactly 2 moves of each snake, but never towards the center or in a corner
226+
for _, snakeBody := range snakeBodies {
227+
snakeHead := snakeBody[0]
228+
possibleFoodLocations := []rules.Point{
229+
{X: snakeHead.X - 1, Y: snakeHead.Y - 1},
230+
{X: snakeHead.X - 1, Y: snakeHead.Y + 1},
231+
{X: snakeHead.X + 1, Y: snakeHead.Y - 1},
232+
{X: snakeHead.X + 1, Y: snakeHead.Y + 1},
233+
}
234+
235+
// Remove any invalid/unwanted positions
236+
availableFoodLocations := []rules.Point{}
237+
for _, p := range possibleFoodLocations {
238+
239+
// Don't place in the center
240+
if centerCoord == p {
241+
continue
242+
}
243+
244+
// Ignore points already occupied by food or hazards
245+
if editor.IsOccupied(p, true, true, true) {
246+
continue
247+
}
248+
249+
// Food must be further than snake from center on at least one axis
250+
isAwayFromCenter := false
251+
if p.X < snakeHead.X && snakeHead.X < centerCoord.X {
252+
isAwayFromCenter = true
253+
} else if centerCoord.X < snakeHead.X && snakeHead.X < p.X {
254+
isAwayFromCenter = true
255+
} else if p.Y < snakeHead.Y && snakeHead.Y < centerCoord.Y {
256+
isAwayFromCenter = true
257+
} else if centerCoord.Y < snakeHead.Y && snakeHead.Y < p.Y {
258+
isAwayFromCenter = true
259+
}
260+
if !isAwayFromCenter {
261+
continue
262+
}
263+
264+
// Don't spawn food in corners
265+
if (p.X == 0 || p.X == (width-1)) && (p.Y == 0 || p.Y == (height-1)) {
266+
continue
267+
}
268+
269+
availableFoodLocations = append(availableFoodLocations, p)
270+
}
271+
272+
if len(availableFoodLocations) <= 0 {
273+
return rules.ErrorNoRoomForFood
274+
}
275+
276+
// Select randomly from available locations
277+
placedFood := availableFoodLocations[rand.Intn(len(availableFoodLocations))]
278+
editor.AddFood(placedFood)
279+
}
280+
}
281+
282+
// Finally, try to place 1 food in center of board for dramatic purposes
283+
if !editor.IsOccupied(centerCoord, true, true, true) {
284+
editor.AddFood(centerCoord)
285+
}
286+
287+
return nil
288+
}

maps/helpers_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,33 @@ func TestUpdateBoard(t *testing.T) {
115115
require.Equal(t, []rules.Point{{X: 3, Y: 4}, {X: 3, Y: 5}, {X: 2, Y: 2}}, boardState.Hazards)
116116
})
117117
}
118+
119+
func TestPlaceFoodFixed(t *testing.T) {
120+
initialBoardState := rules.NewBoardState(rules.BoardSizeMedium, rules.BoardSizeMedium)
121+
editor := maps.NewBoardStateEditor(initialBoardState.Clone())
122+
123+
editor.PlaceSnake("1", []rules.Point{{X: 1, Y: 1}}, 100)
124+
editor.PlaceSnake("2", []rules.Point{{X: 9, Y: 1}}, 100)
125+
editor.PlaceSnake("3", []rules.Point{{X: 4, Y: 9}}, 100)
126+
editor.PlaceSnake("4", []rules.Point{{X: 6, Y: 6}}, 100)
127+
128+
// Hazards everywhere except for the expected food placements
129+
for x := 0; x < initialBoardState.Width; x++ {
130+
for y := 0; y < initialBoardState.Height; y++ {
131+
editor.AddHazard(rules.Point{X: x, Y: y})
132+
}
133+
}
134+
editor.RemoveHazard(rules.Point{X: 0, Y: 2})
135+
editor.RemoveHazard(rules.Point{X: 8, Y: 0})
136+
editor.RemoveHazard(rules.Point{X: 3, Y: 10})
137+
editor.RemoveHazard(rules.Point{X: 7, Y: 7})
138+
139+
err := maps.PlaceFoodFixed(rules.MaxRand, initialBoardState, editor)
140+
require.NoError(t, err)
141+
142+
food := editor.Food()
143+
require.Contains(t, food, rules.Point{X: 0, Y: 2})
144+
require.Contains(t, food, rules.Point{X: 8, Y: 0})
145+
require.Contains(t, food, rules.Point{X: 3, Y: 10})
146+
require.Contains(t, food, rules.Point{X: 7, Y: 7})
147+
}

0 commit comments

Comments
 (0)