Skip to content

Commit

Permalink
Now for something completely different
Browse files Browse the repository at this point in the history
  • Loading branch information
bethanyj28 committed Oct 5, 2021
1 parent 85206f4 commit 0ad4fce
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 19 deletions.
4 changes: 2 additions & 2 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"time"

"github.com/bethanyj28/battlesnek/internal/battle"
"github.com/bethanyj28/battlesnek/internal/battle/simple"
"github.com/bethanyj28/battlesnek/internal/battle/clairvoyant"
"github.com/sirupsen/logrus"
)

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

svr.snake = simple.NewSnake()
svr.snake = clairvoyant.NewSnake(5)

return svr
}
22 changes: 5 additions & 17 deletions internal/battle/clairvoyant/clairvoyant.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,24 @@ type Snake struct {
lookahead int
}

// NewClairvoyantSnake creates a new clairvoyant snake
func NewClairvoyantSnake(lookahead int) Snake {
// NewSnake creates a new clairvoyant snake
func NewSnake(lookahead int) *Snake {
if lookahead == 0 {
lookahead = 1
}
return Snake{lookahead: lookahead}
return &Snake{lookahead: lookahead}
}

// Move decides which move is ideal based on seeing the future
func (s *Snake) Move(state internal.GameState) (internal.Action, error) {
moves := drillDown(state, s.lookahead)

return internal.Action{Move: moves.move}, nil
return internal.Action{Move: findOptimal(state, s.lookahead)}, nil
}

// Info ensures the snake is stylin and profilin
func (s *Snake) Info() internal.Style {
return internal.Style{
Color: "#00cc99",
Color: "#76A5AF",
Head: "beluga",
Tail: "freckled",
}
}

type route struct {
move string
food int
hazard int
}

func drillDown(state internal.GameState, lookahead int) route {
return route{}
}
229 changes: 229 additions & 0 deletions internal/battle/clairvoyant/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package clairvoyant

import (
"log"

"github.com/bethanyj28/battlesnek/internal"
)

type direction int

const (
left direction = iota + 1
right
up
down
)

// Convert direction to string
func (d direction) String() string {
return [...]string{"", "left", "right", "up", "down"}[d]
}

type boardObject int

const (
food boardObject = iota + 1
otherSnake
you
hazard
)

func (b boardObject) String() string {
return [...]string{"", "F", "O", "Y", "H"}[b]
}

// Simplifies ugly types
type matrix map[int]map[int][]string
type choices map[direction]internal.Coord

func convertBoardToMatrix(state internal.GameState) matrix {
board := state.Board
grid := matrix{}
for _, f := range board.Food {
grid = addCoordToMatrix(f, grid, food.String())
}

for _, snake := range board.Snakes {
obj := otherSnake
if snake.ID == state.You.ID {
obj = you
}
for _, coord := range snake.Body {
grid = addCoordToMatrix(coord, grid, obj.String())
}
}

for _, h := range board.Hazards {
grid = addCoordToMatrix(h, grid, hazard.String())
}

return grid
}

func addCoordToMatrix(coord internal.Coord, grid matrix, object string) matrix {
if _, ok := grid[coord.X]; !ok {
grid[coord.X] = map[int][]string{}
}

if _, ok := grid[coord.X][coord.Y]; !ok {
grid[coord.X][coord.Y] = []string{object}
return grid
}

grid[coord.X][coord.Y] = append(grid[coord.X][coord.Y], object)
return grid
}

func potentialPositions(head internal.Coord) choices {
return choices{
left: internal.Coord{X: head.X - 1, Y: head.Y},
right: internal.Coord{X: head.X + 1, Y: head.Y},
up: internal.Coord{X: head.X, Y: head.Y + 1},
down: internal.Coord{X: head.X, Y: head.Y - 1},
}
}

func checkPossible(initialState internal.GameState, grid matrix, potential internal.Coord, otherSnakeLength map[string]int32) bool {
// check walls
if potential.X >= initialState.Board.Width || potential.X < 0 {
return false
}

if potential.Y >= initialState.Board.Height || potential.Y < 0 {
return false
}

// check hazards
space := grid[potential.X][potential.Y]
for _, obj := range space {
if obj == you.String() || obj == otherSnake.String() {
return false
}

if l, ok := otherSnakeLength[obj]; ok {
if l > initialState.You.Length {
return false
}
}
}

return true
}

func moveEnemiesForward(oldHead, newHead internal.Coord, snake internal.Battlesnake, grid matrix) matrix {
objList := grid[oldHead.X][oldHead.Y]
objList = append(objList, otherSnake.String())
for i, obj := range objList {
if obj == snake.ID {
grid[oldHead.X][oldHead.Y] = append(objList[:i], objList[i+1:]...)
break
}
}

return addCoordToMatrix(newHead, grid, snake.ID)
}

func mapSnakeLengths(snakes []internal.Battlesnake) map[string]int32 {
snakeLengths := map[string]int32{}
for _, snake := range snakes {
snakeLengths[snake.ID] = snake.Length
}

return snakeLengths
}

type route struct {
numEval int
numDeaths int
numHazard int
numFood int
}

func findOptimal(state internal.GameState, movesLeft int) string {
grid := convertBoardToMatrix(state)
for _, snake := range state.Board.Snakes {
if snake.ID == state.You.ID {
continue
}
p := potentialPositions(snake.Head)
for _, pp := range p {
grid = moveEnemiesForward(snake.Head, pp, snake, grid)
}
}

health := float64(state.You.Health) / 100
potential := potentialPositions(state.You.Head)
analysis := map[direction]float64{}
otherSnakes := mapSnakeLengths(state.Board.Snakes)
for d, p := range potential {
r := lookahead(movesLeft, route{}, state, otherSnakes, grid, p)
if r.numDeaths == r.numEval {
continue
}
survival := float64(r.numDeaths) / (float64(r.numEval * r.numEval))
hazardRisk := ((1 - health) * float64(r.numHazard)) / float64(r.numEval*r.numEval)
foodRisk := (health * float64(r.numEval-r.numFood)) / float64(r.numEval*r.numEval)
sum := survival
denom := 1

sum += hazardRisk
denom++

sum += foodRisk
denom++

analysis[d] = sum / float64(denom)
log.Printf("route for %s:%+v", d.String(), r)
}

log.Print(analysis)

minScore := 1.0
optimal := "up"
for d, s := range analysis {
if s < minScore {
optimal = d.String()
minScore = s
}
}
return optimal
}

func lookahead(movesLeft int, r route, initialState internal.GameState, otherSnakes map[string]int32, grid matrix, option internal.Coord) route {
if movesLeft <= 0 {
return r
}

r.numEval++

if !checkPossible(initialState, grid, option, otherSnakes) {
r.numDeaths++
return r
}

if obj, ok := grid[option.X][option.Y]; ok {
for _, o := range obj {
if o == food.String() {
r.numFood++
}

if o == hazard.String() {
r.numHazard++
}
}
}

potential := potentialPositions(option)
grid = addCoordToMatrix(option, grid, you.String())
for _, p := range potential {
movesLeft--
nr := lookahead(movesLeft, r, initialState, otherSnakes, grid, p)
r.numEval += nr.numEval
r.numDeaths += nr.numDeaths
r.numHazard += nr.numHazard
r.numFood += nr.numFood
}

return r
}

0 comments on commit 0ad4fce

Please sign in to comment.