From 771c84191d7208f69fc9ff968ed354975451fc97 Mon Sep 17 00:00:00 2001 From: aswanson-nr Date: Sat, 26 Feb 2022 19:23:40 -0800 Subject: [PATCH] get closest snake when comparing moves --- src/lookahead_snake.ts | 164 ++--------------------------------- src/utils/board.ts | 18 +++- test/lookahead_snake.test.ts | 66 +------------- test/utils/board.test.ts | 121 +++++++++++++++++++++++++- 4 files changed, 144 insertions(+), 225 deletions(-) diff --git a/src/lookahead_snake.ts b/src/lookahead_snake.ts index c8312bb..7d01112 100644 --- a/src/lookahead_snake.ts +++ b/src/lookahead_snake.ts @@ -10,6 +10,7 @@ import { getNeighbors, printGrid, hasDuplicates, + getClosestSnake, } from "./utils/board"; import { cloneGameState, @@ -115,160 +116,6 @@ export const voronoi = (gs: GameStateSim): number => { // add to queue }; -/** - * Counts a node as 'owned' if a root can reach it before any other roots. - * returns each root with the total number of owned nodes for that root. - * Does not factor in moving backward (illegal in battlesnake), which may cause some issues with snakes of length 2. - **/ -export const voronoriCounts = ( - grid: Grid, - roots: Coord[] -): { root: Coord; score: number }[] => { - const height = grid.length; - const width = grid[0].length; - const scores = roots.map((root) => ({ root, score: 0 })); - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const node = grid[y][x]; - if (node.hasSnake && !node.hasSnakeTail) { - continue; - } - - const pathsAsc = roots - .map((r) => BFS(grid, r, node.coord)) - .filter((p) => p.length > 0) - .sort((a, b) => a.length - b.length); - const [p1, p2] = pathsAsc; - - if (!p1 || (p2 && p1.length === p2.length)) { - // space unreachable or is unowned - continue; - } - - const isOwner = isCoordEqual(p1[0].coord); - const ownerScore = scores.find((s) => isOwner(s.root)); - if (ownerScore) { - ownerScore.score += 1; - } - } - } - - return scores; -}; - -export const voronoriCountsCrowFlies = ( - grid: Grid, - roots: Coord[] -): { root: Coord; score: number }[] => { - const height = grid.length; - const width = grid[0].length; - const scores = roots.map((root) => ({ root, score: 0 })); - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const node = grid[y][x]; - if (node.hasSnake && !node.hasSnakeTail) { - continue; - } - - const pathsAsc = roots - .map((r) => ({ - root: r, - distance: Math.sqrt( - Math.pow(r.x - node.coord.x, 2) + Math.pow(r.y - node.coord.y, 2) - ), - })) - .sort((a, b) => a.distance - b.distance); - const [p1, p2] = pathsAsc; - - const isOwner = isCoordEqual(p1.root); - const ownerScore = scores.find((s) => isOwner(s.root)); - if (ownerScore) { - ownerScore.score += 1; - } - } - } - - return scores; -}; - -const nodeHeuristic = ( - grid: Grid, - { board, you, game, turn }: GameState, - node: Node, - runVoronoi: boolean = true -): number => { - const isWrapped = game.ruleset.name === "wrapped"; - let total = 0; - const snakes = board.snakes; - const food = board.food; - const isCurrent = isCoordEqual(node.coord); - const enemySnakes = snakes.filter((s) => s.id !== you.id); - const snakesWithMoves: { - snake: Battlesnake; - possibleMoves: Node[]; - }[] = enemySnakes.map((s) => ({ - snake: s, - possibleMoves: getMoves(grid, s.body, isWrapped), - })); - const smallerSnakes = snakesWithMoves.filter( - ({ snake }) => snake.length < you.length - ); - const largerSnakes = snakesWithMoves.filter( - ({ snake }) => snake.length >= you.length - ); - const isPossibleKillMove = - smallerSnakes - .flatMap(prop("possibleMoves")) - .map(prop("coord")) - .filter(isCurrent).length > 0; - - const isPossibleDeathMove = - largerSnakes - .flatMap(prop("possibleMoves")) - .map(prop("coord")) - .filter(isCurrent).length > 0; - - if (isPossibleKillMove) { - total += 1000; - } - - if (isPossibleDeathMove) { - total += -10000; - } - - // Factor in distance to food - const orderedFood = food - .map((f) => BFS(grid, node.coord, f)) - .sort((a, b) => a.length - b.length); - const a = 60; // much hungrier in the beginning - const b = turn < 60 ? 1 : 5; - orderedFood.forEach((foodPath) => { - // TODO: handle hazards when there isn't food, could also factor in the number of hazard spaces on the path - const foodDistance = foodPath.length; - total += a * Math.atan(you.health - foodDistance / b); - }); - - if (runVoronoi) { - const voronoiScores = voronoriCountsCrowFlies(grid, [ - node.coord, - ...enemySnakes.map(prop("head")), - ]); - const voronoiScore = voronoiScores.find((s) => isCurrent(s.root)); - - if (voronoiScore) { - total += voronoiScore.score * 1; // should be a hyper parameter - } - } - - if (node.hasHazard) { - total = total / 26; - } - - return total; -}; - const stateHeuristic = (gs: GameStateSim): number => { const { you, board, turn, grid } = gs; const otherSnakes = gs.board.snakes.filter((s) => s.id !== gs.you.id); @@ -436,12 +283,13 @@ export const alphabeta = ( } else { let value = Infinity; - const enemy = board.snakes.find((s) => s.id !== you.id); - if (enemy) { - const enemyMove = getMoves(grid, enemy.body, isWrapped); + const enemySnakes = board.snakes.filter((s) => s.id !== you.id); + const closestSnake = getClosestSnake(grid, move, enemySnakes, isWrapped); + if (closestSnake) { + const enemyMove = getMoves(grid, closestSnake.body, isWrapped); for (const pm of enemyMove) { const ns = cloneGameState(gs); - addMove(ns, enemy, pm.coord); + addMove(ns, closestSnake, pm.coord); const nextTurn = resolveTurn(ns); const max = alphabeta(nextTurn, move, depth - 1, alpha, beta, true); diff --git a/src/utils/board.ts b/src/utils/board.ts index cbcfb9b..4f00351 100644 --- a/src/utils/board.ts +++ b/src/utils/board.ts @@ -1,6 +1,6 @@ import { Battlesnake, Board, Coord, Ruleset } from "../types"; import { log, prop } from "./general"; -import { createQueue } from "./queue"; +import { createQueue, Queue } from "./queue"; export interface Node { coord: Coord; @@ -182,6 +182,22 @@ export const hasDuplicates = (coords: Coord[]): boolean => { return false; }; +export const getClosestSnake = ( + grid: Grid, + root: Coord, + snakes: Battlesnake[], + isWrapped = false +): Battlesnake | undefined => { + const snakePaths = snakes + .map((s) => ({ snake: s, path: BFS(grid, root, s.head, isWrapped) })) + .filter((sp) => sp.path.length < 1) + .sort((a, b) => a.path.length - b.path.length); + + if (snakePaths[0]) { + return snakePaths[0].snake; + } +}; + export const printGrid = (grid: Grid) => { const nodeChar = " _ "; const snakeChar = " s "; diff --git a/test/lookahead_snake.test.ts b/test/lookahead_snake.test.ts index c7ceb9d..e488752 100644 --- a/test/lookahead_snake.test.ts +++ b/test/lookahead_snake.test.ts @@ -1,9 +1,4 @@ -import { - determineMove, - voronoriCounts, - voronoi, - alphabeta, -} from "../src/lookahead_snake"; +import { determineMove, voronoi, alphabeta } from "../src/lookahead_snake"; import { createBoard, createGameState, createSnake } from "./utils"; import { createGrid } from "../src/utils/board"; import { resolveTurn } from "../src/utils/game_sim"; @@ -44,65 +39,6 @@ describe("alphabeta", () => { }); }); - describe("voronoriCounts", () => { - it("returns count for 1 snake", () => { - const snake1 = createSnake([{ x: 0, y: 0 }]); - const grid = createGrid(createBoard(3, [], [snake1])); - const [vCounts] = voronoriCounts(grid, [{ x: 0, y: 0 }]); - expect(vCounts.score).toBe(9); - }); - - it("returns counts for 2 snakes", () => { - // _ _ _ _ _ - // _ _ _ h s - // _ _ _ _ s - // _ _ _ _ _ - // k e e _ _ - const snake1 = createSnake([ - { x: 3, y: 3 }, - { x: 4, y: 3 }, - { x: 4, y: 2 }, - ]); - const snake2 = createSnake([ - { x: 0, y: 0 }, - { x: 1, y: 0 }, - { x: 2, y: 0 }, - ]); - const grid = createGrid(createBoard(5, [], [snake1, snake2])); - const [s1VCount, s2VCount] = voronoriCounts(grid, [ - snake1.head, - snake2.head, - ]); - console.log(s1VCount, s2VCount); - expect(s1VCount.score).toBe(13); - expect(s2VCount.score).toBe(3); - }); - - it("returns counts that include snake going backwards, DELETE ME WHEN FIXED AND THIS TEST FAILS", () => { - // _ _ _ _ _ - // _ _ _ h s - // _ _ _ _ _ - // _ _ _ _ _ - // k e _ _ _ - const snake1 = createSnake([ - { x: 3, y: 3 }, - { x: 4, y: 3 }, - ]); - const snake2 = createSnake([ - { x: 0, y: 0 }, - { x: 1, y: 0 }, - ]); - const grid = createGrid(createBoard(5, [], [snake1, snake2])); - const [s1VCount, s2VCount] = voronoriCounts(grid, [ - snake1.head, - snake2.head, - ]); - console.log(s1VCount, s2VCount); - expect(s1VCount.score).toBe(12); // Should actually be 14 - expect(s2VCount.score).toBe(5); // Should actually be 4 - }); - }); - describe("determineMove", () => { // TODO: These types of move requests are taking a very long way to process // need to figure out why, because these requests should not be increasing the pathfinding by so much diff --git a/test/utils/board.test.ts b/test/utils/board.test.ts index d42619e..d6b7a1a 100644 --- a/test/utils/board.test.ts +++ b/test/utils/board.test.ts @@ -1,7 +1,36 @@ -import { createGrid, BFS, getNeighbors, getMoves } from "../../src/utils/board"; +import { + createGrid, + BFS, + getNeighbors, + getMoves, + hasDuplicates, + getClosestSnake, +} from "../../src/utils/board"; import { createBoard, createSnake } from "../utils"; describe("utils/board", () => { + describe("hasDuplicates", () => { + it("detects duplicates", () => { + const snake = [ + { x: 0, y: 1 }, + { x: 0, y: 2 }, + { x: 0, y: 2 }, + ]; + expect(hasDuplicates(snake)).toBe(true); + }); + it("detects no duplicates", () => { + const snake = [ + { x: 0, y: 1 }, + { x: 0, y: 2 }, + { x: 0, y: 3 }, + ]; + expect(hasDuplicates(snake)).toBe(false); + }); + it("handles array with one element", () => { + const snake = [{ x: 0, y: 1 }]; + expect(hasDuplicates(snake)).toBe(false); + }); + }); describe("createBoard", () => { it("generates 0x0 grid", () => { const grid = createGrid(createBoard(0)); @@ -146,4 +175,94 @@ describe("utils/board", () => { expect(neighbors[1].coord).toEqual({ x: 4, y: 1 }); }); }); + describe("getClosestSnake", () => { + it("returns snake", () => { + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ r _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ s s s _ _ _ _ _ + // _ _ _ s _ h _ _ _ _ _ + // _ _ _ s s s _ _ _ _ _ + + const snake = createSnake([ + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 4, y: 0 }, + { x: 3, y: 0 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + { x: 4, y: 2 }, + { x: 5, y: 2 }, + ]); + + const grid = createGrid(createBoard(11, [], [snake])); + const cSnake = getClosestSnake(grid, { x: 8, y: 4 }, [snake]); + expect(cSnake).toBeDefined(); + expect(cSnake?.head).toEqual({ x: 5, y: 1 }); + }); + + it("returns closest snake", () => { + // k _ _ _ _ _ _ _ _ _ _ + // s _ _ _ _ _ _ _ _ _ _ + // s _ _ _ _ _ _ _ _ _ _ + // s _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ _ _ _ _ _ r _ _ + // _ _ _ _ _ _ _ _ _ _ _ + // _ _ _ s s s _ _ _ _ _ + // _ _ _ s _ h _ _ _ _ _ + // _ _ _ s s s _ _ _ _ _ + + const snake = createSnake([ + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 4, y: 0 }, + { x: 3, y: 0 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + { x: 4, y: 2 }, + { x: 5, y: 2 }, + ]); + + const snake2 = createSnake([ + { x: 0, y: 10 }, + { x: 0, y: 9 }, + { x: 0, y: 8 }, + { x: 0, y: 7 }, + ]); + const grid = createGrid(createBoard(11, [], [snake, snake2])); + const cSnake = getClosestSnake(grid, { x: 8, y: 4 }, [snake, snake2]); + expect(cSnake).toBeDefined(); + expect(cSnake?.head).toEqual({ x: 5, y: 1 }); + }); + + it("handles no snakes", () => { + const snake = createSnake([ + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 4, y: 0 }, + { x: 3, y: 0 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + { x: 4, y: 2 }, + { x: 5, y: 2 }, + ]); + + const snake2 = createSnake([ + { x: 0, y: 10 }, + { x: 0, y: 9 }, + { x: 0, y: 8 }, + { x: 0, y: 7 }, + ]); + const grid = createGrid(createBoard(11, [], [])); + const cSnake = getClosestSnake(grid, { x: 8, y: 4 }, [snake, snake2]); + expect(cSnake).not.toBeDefined(); + }); + }); });