From 0ad4fce104c54b52080518d11b2b219b2ccab9a1 Mon Sep 17 00:00:00 2001 From: Bethany Janos Date: Mon, 4 Oct 2021 21:38:43 -0400 Subject: [PATCH] Now for something completely different --- cmd/server/server.go | 4 +- internal/battle/clairvoyant/clairvoyant.go | 22 +- internal/battle/clairvoyant/util.go | 229 +++++++++++++++++++++ 3 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 internal/battle/clairvoyant/util.go diff --git a/cmd/server/server.go b/cmd/server/server.go index 5b805b4..ab7c20e 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -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" ) @@ -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 } diff --git a/internal/battle/clairvoyant/clairvoyant.go b/internal/battle/clairvoyant/clairvoyant.go index 66c0e91..8469ede 100644 --- a/internal/battle/clairvoyant/clairvoyant.go +++ b/internal/battle/clairvoyant/clairvoyant.go @@ -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{} -} diff --git a/internal/battle/clairvoyant/util.go b/internal/battle/clairvoyant/util.go new file mode 100644 index 0000000..81eb768 --- /dev/null +++ b/internal/battle/clairvoyant/util.go @@ -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 +}