Skip to content

Commit 0ad4fce

Browse files
committed
Now for something completely different
1 parent 85206f4 commit 0ad4fce

File tree

3 files changed

+236
-19
lines changed

3 files changed

+236
-19
lines changed

cmd/server/server.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"time"
66

77
"github.com/bethanyj28/battlesnek/internal/battle"
8-
"github.com/bethanyj28/battlesnek/internal/battle/simple"
8+
"github.com/bethanyj28/battlesnek/internal/battle/clairvoyant"
99
"github.com/sirupsen/logrus"
1010
)
1111

@@ -19,7 +19,7 @@ func newServer(addr string, timeout time.Duration, logger *logrus.Logger) *serve
1919
svr := &server{logger: logger}
2020
svr.buildHTTPServer(addr, timeout)
2121

22-
svr.snake = simple.NewSnake()
22+
svr.snake = clairvoyant.NewSnake(5)
2323

2424
return svr
2525
}

internal/battle/clairvoyant/clairvoyant.go

+5-17
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,24 @@ type Snake struct {
77
lookahead int
88
}
99

10-
// NewClairvoyantSnake creates a new clairvoyant snake
11-
func NewClairvoyantSnake(lookahead int) Snake {
10+
// NewSnake creates a new clairvoyant snake
11+
func NewSnake(lookahead int) *Snake {
1212
if lookahead == 0 {
1313
lookahead = 1
1414
}
15-
return Snake{lookahead: lookahead}
15+
return &Snake{lookahead: lookahead}
1616
}
1717

1818
// Move decides which move is ideal based on seeing the future
1919
func (s *Snake) Move(state internal.GameState) (internal.Action, error) {
20-
moves := drillDown(state, s.lookahead)
21-
22-
return internal.Action{Move: moves.move}, nil
20+
return internal.Action{Move: findOptimal(state, s.lookahead)}, nil
2321
}
2422

2523
// Info ensures the snake is stylin and profilin
2624
func (s *Snake) Info() internal.Style {
2725
return internal.Style{
28-
Color: "#00cc99",
26+
Color: "#76A5AF",
2927
Head: "beluga",
3028
Tail: "freckled",
3129
}
3230
}
33-
34-
type route struct {
35-
move string
36-
food int
37-
hazard int
38-
}
39-
40-
func drillDown(state internal.GameState, lookahead int) route {
41-
return route{}
42-
}

internal/battle/clairvoyant/util.go

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package clairvoyant
2+
3+
import (
4+
"log"
5+
6+
"github.com/bethanyj28/battlesnek/internal"
7+
)
8+
9+
type direction int
10+
11+
const (
12+
left direction = iota + 1
13+
right
14+
up
15+
down
16+
)
17+
18+
// Convert direction to string
19+
func (d direction) String() string {
20+
return [...]string{"", "left", "right", "up", "down"}[d]
21+
}
22+
23+
type boardObject int
24+
25+
const (
26+
food boardObject = iota + 1
27+
otherSnake
28+
you
29+
hazard
30+
)
31+
32+
func (b boardObject) String() string {
33+
return [...]string{"", "F", "O", "Y", "H"}[b]
34+
}
35+
36+
// Simplifies ugly types
37+
type matrix map[int]map[int][]string
38+
type choices map[direction]internal.Coord
39+
40+
func convertBoardToMatrix(state internal.GameState) matrix {
41+
board := state.Board
42+
grid := matrix{}
43+
for _, f := range board.Food {
44+
grid = addCoordToMatrix(f, grid, food.String())
45+
}
46+
47+
for _, snake := range board.Snakes {
48+
obj := otherSnake
49+
if snake.ID == state.You.ID {
50+
obj = you
51+
}
52+
for _, coord := range snake.Body {
53+
grid = addCoordToMatrix(coord, grid, obj.String())
54+
}
55+
}
56+
57+
for _, h := range board.Hazards {
58+
grid = addCoordToMatrix(h, grid, hazard.String())
59+
}
60+
61+
return grid
62+
}
63+
64+
func addCoordToMatrix(coord internal.Coord, grid matrix, object string) matrix {
65+
if _, ok := grid[coord.X]; !ok {
66+
grid[coord.X] = map[int][]string{}
67+
}
68+
69+
if _, ok := grid[coord.X][coord.Y]; !ok {
70+
grid[coord.X][coord.Y] = []string{object}
71+
return grid
72+
}
73+
74+
grid[coord.X][coord.Y] = append(grid[coord.X][coord.Y], object)
75+
return grid
76+
}
77+
78+
func potentialPositions(head internal.Coord) choices {
79+
return choices{
80+
left: internal.Coord{X: head.X - 1, Y: head.Y},
81+
right: internal.Coord{X: head.X + 1, Y: head.Y},
82+
up: internal.Coord{X: head.X, Y: head.Y + 1},
83+
down: internal.Coord{X: head.X, Y: head.Y - 1},
84+
}
85+
}
86+
87+
func checkPossible(initialState internal.GameState, grid matrix, potential internal.Coord, otherSnakeLength map[string]int32) bool {
88+
// check walls
89+
if potential.X >= initialState.Board.Width || potential.X < 0 {
90+
return false
91+
}
92+
93+
if potential.Y >= initialState.Board.Height || potential.Y < 0 {
94+
return false
95+
}
96+
97+
// check hazards
98+
space := grid[potential.X][potential.Y]
99+
for _, obj := range space {
100+
if obj == you.String() || obj == otherSnake.String() {
101+
return false
102+
}
103+
104+
if l, ok := otherSnakeLength[obj]; ok {
105+
if l > initialState.You.Length {
106+
return false
107+
}
108+
}
109+
}
110+
111+
return true
112+
}
113+
114+
func moveEnemiesForward(oldHead, newHead internal.Coord, snake internal.Battlesnake, grid matrix) matrix {
115+
objList := grid[oldHead.X][oldHead.Y]
116+
objList = append(objList, otherSnake.String())
117+
for i, obj := range objList {
118+
if obj == snake.ID {
119+
grid[oldHead.X][oldHead.Y] = append(objList[:i], objList[i+1:]...)
120+
break
121+
}
122+
}
123+
124+
return addCoordToMatrix(newHead, grid, snake.ID)
125+
}
126+
127+
func mapSnakeLengths(snakes []internal.Battlesnake) map[string]int32 {
128+
snakeLengths := map[string]int32{}
129+
for _, snake := range snakes {
130+
snakeLengths[snake.ID] = snake.Length
131+
}
132+
133+
return snakeLengths
134+
}
135+
136+
type route struct {
137+
numEval int
138+
numDeaths int
139+
numHazard int
140+
numFood int
141+
}
142+
143+
func findOptimal(state internal.GameState, movesLeft int) string {
144+
grid := convertBoardToMatrix(state)
145+
for _, snake := range state.Board.Snakes {
146+
if snake.ID == state.You.ID {
147+
continue
148+
}
149+
p := potentialPositions(snake.Head)
150+
for _, pp := range p {
151+
grid = moveEnemiesForward(snake.Head, pp, snake, grid)
152+
}
153+
}
154+
155+
health := float64(state.You.Health) / 100
156+
potential := potentialPositions(state.You.Head)
157+
analysis := map[direction]float64{}
158+
otherSnakes := mapSnakeLengths(state.Board.Snakes)
159+
for d, p := range potential {
160+
r := lookahead(movesLeft, route{}, state, otherSnakes, grid, p)
161+
if r.numDeaths == r.numEval {
162+
continue
163+
}
164+
survival := float64(r.numDeaths) / (float64(r.numEval * r.numEval))
165+
hazardRisk := ((1 - health) * float64(r.numHazard)) / float64(r.numEval*r.numEval)
166+
foodRisk := (health * float64(r.numEval-r.numFood)) / float64(r.numEval*r.numEval)
167+
sum := survival
168+
denom := 1
169+
170+
sum += hazardRisk
171+
denom++
172+
173+
sum += foodRisk
174+
denom++
175+
176+
analysis[d] = sum / float64(denom)
177+
log.Printf("route for %s:%+v", d.String(), r)
178+
}
179+
180+
log.Print(analysis)
181+
182+
minScore := 1.0
183+
optimal := "up"
184+
for d, s := range analysis {
185+
if s < minScore {
186+
optimal = d.String()
187+
minScore = s
188+
}
189+
}
190+
return optimal
191+
}
192+
193+
func lookahead(movesLeft int, r route, initialState internal.GameState, otherSnakes map[string]int32, grid matrix, option internal.Coord) route {
194+
if movesLeft <= 0 {
195+
return r
196+
}
197+
198+
r.numEval++
199+
200+
if !checkPossible(initialState, grid, option, otherSnakes) {
201+
r.numDeaths++
202+
return r
203+
}
204+
205+
if obj, ok := grid[option.X][option.Y]; ok {
206+
for _, o := range obj {
207+
if o == food.String() {
208+
r.numFood++
209+
}
210+
211+
if o == hazard.String() {
212+
r.numHazard++
213+
}
214+
}
215+
}
216+
217+
potential := potentialPositions(option)
218+
grid = addCoordToMatrix(option, grid, you.String())
219+
for _, p := range potential {
220+
movesLeft--
221+
nr := lookahead(movesLeft, r, initialState, otherSnakes, grid, p)
222+
r.numEval += nr.numEval
223+
r.numDeaths += nr.numDeaths
224+
r.numHazard += nr.numHazard
225+
r.numFood += nr.numFood
226+
}
227+
228+
return r
229+
}

0 commit comments

Comments
 (0)