BITNODE: IPvGO territory control strategy game (#934)

This commit is contained in:
Michael Ficocelli
2023-12-26 11:45:27 -05:00
committed by GitHub
parent c6141f2adf
commit 7ef12a0323
68 changed files with 7833 additions and 17 deletions

View File

@@ -0,0 +1,282 @@
import { getBoardFromSimplifiedBoardState, getSimplifiedBoardState } from "../../../src/Go/boardAnalysis/boardAnalysis";
import {
cheatPlayTwoMoves,
cheatRemoveRouter,
cheatRepairOfflineNode,
getChains,
getControlledEmptyNodes,
getLiberties,
getValidMoves,
handlePassTurn,
invalidMoveResponse,
makePlayerMove,
resetBoardState,
} from "../../../src/Go/effects/netscriptGoImplementation";
import { Player, setPlayer } from "@player";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import "../../../src/Faction/Factions";
import { opponents, playerColors, playTypes } from "../../../src/Go/boardState/goConstants";
import { getNewBoardState } from "../../../src/Go/boardState/boardState";
jest.mock("../../../src/Faction/Factions", () => ({
Factions: {},
}));
setPlayer(new PlayerObject());
describe("Netscript Go API unit tests", () => {
describe("makeMove() tests", () => {
it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await makePlayerMove(mockLogger, 0, 0);
expect(result).toEqual(invalidMoveResponse);
expect(mockLogger).toHaveBeenCalledWith("ERROR: Invalid move: That node is already occupied by a piece");
});
it("should update the board with valid player moves", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....."];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await makePlayerMove(mockLogger, 1, 0);
expect(result.success).toEqual(true);
expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
expect(Player.go.boardState.board[1]?.[0]?.player).toEqual(playerColors.black);
expect(Player.go.boardState.board[0]?.[0]?.player).toEqual(playerColors.empty);
});
});
describe("passTurn() tests", () => {
it("should handle pass attempts", async () => {
Player.go.boardState = getNewBoardState(7);
const mockLogger = jest.fn();
const result = await handlePassTurn(mockLogger);
expect(result.success).toEqual(true);
expect(result.type).toEqual(playTypes.move);
});
});
describe("getBoardState() tests", () => {
it("should correctly return a string version of the bard state", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"];
const boardState = getBoardFromSimplifiedBoardState(board);
const result = getSimplifiedBoardState(boardState.board);
expect(result).toEqual(board);
});
});
describe("resetBoardState() tests", () => {
it("should set the player's board to the requested size and opponent", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
const mockError = jest.fn();
const newBoard = resetBoardState(mockError, opponents.SlumSnakes, 9);
expect(newBoard?.[0].length).toEqual(9);
expect(Player.go.boardState.board.length).toEqual(9);
expect(Player.go.boardState.ai).toEqual(opponents.SlumSnakes);
});
it("should throw an error if an invalid opponent is requested", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
const mockError = jest.fn();
resetBoardState(mockError, "fake opponent", 9);
expect(mockError).toHaveBeenCalledWith(
"Invalid opponent requested (fake opponent), valid options are Netburners, Slum Snakes, The Black Hand, Tetrads, Daedalus, Illuminati",
);
});
it("should throw an error if an invalid size is requested", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
const mockError = jest.fn();
resetBoardState(mockError, opponents.TheBlackHand, 31337);
expect(mockError).toHaveBeenCalledWith("Invalid subnet size requested (31337, size must be 5, 7, 9, or 13");
});
});
describe("getValidMoves() unit tests", () => {
it("should return all valid and invalid moves on the board", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const result = getValidMoves();
expect(result).toEqual([
[false, false, false, false, false],
[false, false, false, false, false],
[true, false, false, false, false],
[false, false, false, false, false],
[false, true, false, true, false],
]);
});
});
describe("getChains() unit tests", () => {
it("should assign an ID to all contiguous chains on the board", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const result = getChains();
expect(result[4][0]).toEqual(result[3][4]);
expect(result[2][1]).toEqual(result[1][3]);
expect(result[0][0]).toEqual(result[1][0]);
expect(result[0][4]).toEqual(null);
});
});
describe("getLiberties() unit tests", () => {
it("should display the number of connected empty nodes for each chain on the board", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const result = getLiberties();
expect(result).toEqual([
[1, 1, 2, -1, -1],
[1, 4, -1, 4, -1],
[-1, 4, 4, 4, 4],
[3, 3, 3, 3, 3],
[3, -1, 3, -1, 3],
]);
});
});
describe("getControlledEmptyNodes() unit tests", () => {
it("should show the owner of each empty node, if a single player has fully encircled it", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const result = getControlledEmptyNodes();
expect(result).toEqual(["...O#", "..O.O", "?....", ".....", ".X.X."]);
});
});
describe("cheatPlayTwoMoves() tests", () => {
it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatPlayTwoMoves(mockLogger, 0, 0, 1, 0, 0, 0);
expect(result).toEqual(invalidMoveResponse);
expect(mockLogger).toHaveBeenCalledWith("The point 0,0 is not empty, so you cannot place a router there.");
});
it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0);
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4");
expect(result.success).toEqual(true);
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.black);
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.black);
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.empty);
});
it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1);
console.log(result);
expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
expect(result.success).toEqual(false);
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.empty);
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.empty);
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.white);
});
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
Player.go.boardState.cheatCount = 1;
const mockLogger = jest.fn();
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
console.log(result);
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(result.success).toEqual(false);
expect(Player.go.boardState.history.length).toEqual(0);
});
});
describe("cheatRemoveRouter() tests", () => {
it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatRemoveRouter(mockLogger, 1, 0, 0, 0);
expect(result).toEqual(invalidMoveResponse);
expect(mockLogger).toHaveBeenCalledWith(
"The point 1,0 does not have a router on it, so you cannot clear this point with removeRouter().",
);
});
it("should remove the router if the move is valid", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatRemoveRouter(mockLogger, 0, 0, 0, 0);
expect(result.success).toEqual(true);
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared.");
expect(Player.go.boardState.board[0][0]?.player).toEqual(playerColors.empty);
});
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
Player.go.boardState.cheatCount = 1;
const mockLogger = jest.fn();
const result = await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
console.log(result);
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(result.success).toEqual(false);
expect(Player.go.boardState.history.length).toEqual(0);
});
});
describe("cheatRepairOfflineNode() tests", () => {
it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....#"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatRepairOfflineNode(mockLogger, 0, 0);
expect(result).toEqual(invalidMoveResponse);
expect(mockLogger).toHaveBeenCalledWith("The node 0,0 is not offline, so you cannot repair the node.");
});
it("should update the board with the repaired node if the cheat is successful", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....#"];
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
const mockLogger = jest.fn();
const result = await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired.");
expect(result.success).toEqual(true);
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.empty);
});
});
});

View File

@@ -0,0 +1,47 @@
import { setPlayer } from "@player";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import {
getAllEyes,
getAllValidMoves,
getBoardFromSimplifiedBoardState,
} from "../../../src/Go/boardAnalysis/boardAnalysis";
import { playerColors } from "../../../src/Go/boardState/goConstants";
import { findAnyMatchedPatterns } from "../../../src/Go/boardAnalysis/patternMatching";
setPlayer(new PlayerObject());
describe("Go board analysis tests", () => {
it("identifies chains and liberties", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."];
const boardState = getBoardFromSimplifiedBoardState(board);
expect(boardState.board[0]?.[0]?.liberties?.length).toEqual(1);
expect(boardState.board[0]?.[1]?.liberties?.length).toEqual(3);
});
it("identifies all points that are part of 'eyes' on the board", async () => {
const board = ["..O..", "OOOOO", "..XXX", "..XX.", "..X.X"];
const boardState = getBoardFromSimplifiedBoardState(board);
const whitePlayerEyes = getAllEyes(boardState, playerColors.white).flat().flat();
const blackPlayerEyes = getAllEyes(boardState, playerColors.black).flat().flat();
expect(whitePlayerEyes?.length).toEqual(4);
expect(blackPlayerEyes?.length).toEqual(2);
});
it("identifies strong patterns on the board", async () => {
const board = [".....", ".....", ".....", ".....", ".OXO."];
const boardState = getBoardFromSimplifiedBoardState(board);
const point = await findAnyMatchedPatterns(
boardState,
playerColors.white,
getAllValidMoves(boardState, playerColors.white),
true,
0,
);
expect(point?.x).toEqual(3);
expect(point?.y).toEqual(2);
});
});

40
test/jest/Go/goAI.test.ts Normal file
View File

@@ -0,0 +1,40 @@
import { getBoardFromSimplifiedBoardState } from "../../../src/Go/boardAnalysis/boardAnalysis";
import { opponents, playerColors } from "../../../src/Go/boardState/goConstants";
import { getMove } from "../../../src/Go/boardAnalysis/goAI";
import { setPlayer } from "@player";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import "../../../src/Faction/Factions";
jest.mock("../../../src/Faction/Factions", () => ({
Factions: {},
}));
setPlayer(new PlayerObject());
describe("Go AI tests", () => {
it("prioritizes capture for Black Hand", async () => {
const board = ["XO...", ".....", ".....", ".....", "....."];
const boardState = getBoardFromSimplifiedBoardState(board, opponents.TheBlackHand);
const move = await getMove(boardState, playerColors.white, opponents.TheBlackHand);
expect([move.x, move.y]).toEqual([1, 0]);
});
it("prioritizes defense for Slum Snakes", async () => {
const board = ["OX...", ".....", ".....", ".....", "....."];
const boardState = getBoardFromSimplifiedBoardState(board, opponents.SlumSnakes);
const move = await getMove(boardState, playerColors.white, opponents.SlumSnakes);
expect([move.x, move.y]).toEqual([1, 0]);
});
it("prioritizes eye creation moves for Illuminati", async () => {
const board = ["...O...", "OOOO...", ".......", ".......", ".......", ".......", "......."];
const boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus);
const move = await getMove(boardState, playerColors.white, opponents.Daedalus, 0);
console.log(move);
expect([move.x, move.y]).toEqual([0, 1]);
});
});

View File

@@ -1198,6 +1198,457 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"wantedGainRate": 0,
},
},
"go": {
"boardState": {
"ai": "Netburners",
"board": [
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 0,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 1,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 2,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 3,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 4,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 5,
"y": 6,
},
],
[
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 0,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 1,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 2,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 3,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 4,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 5,
},
{
"chain": "",
"liberties": null,
"player": "Empty",
"x": 6,
"y": 6,
},
],
],
"cheatCount": 0,
"history": [],
"passCount": 0,
"previousPlayer": "White",
},
"previousGameFinalBoardState": null,
"status": {
"????????????": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"Daedalus": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"Illuminati": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"Netburners": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"No AI": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"Slum Snakes": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"Tetrads": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
"The Black Hand": {
"favor": 0,
"highestWinStreak": 0,
"losses": 0,
"nodePower": 0,
"nodes": 0,
"oldWinStreak": 0,
"winStreak": 0,
"wins": 0,
},
},
},
"hacknetNodes": [],
"has4SData": false,
"has4SDataTixApi": false,