Skip to content

Commit

Permalink
get closest snake when comparing moves
Browse files Browse the repository at this point in the history
  • Loading branch information
aswanson-nr committed Feb 27, 2022
1 parent c9c7e4c commit 771c841
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 225 deletions.
164 changes: 6 additions & 158 deletions src/lookahead_snake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getNeighbors,
printGrid,
hasDuplicates,
getClosestSnake,
} from "./utils/board";
import {
cloneGameState,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 17 additions & 1 deletion src/utils/board.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 ";
Expand Down
66 changes: 1 addition & 65 deletions test/lookahead_snake.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 771c841

Please sign in to comment.