mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 22:38:34 +02:00
IPVGO: Support scripts playing against each other as each color on "No AI" boards (#1917)
This is a big change with a *lot* of moving parts. The largest part of it is enabling scripts to `playAsWhite` as a parameter to many Go functions. In the implementation, this involved a significant rewrite of `opponentNextTurn` promise handling. A number of other changes and bugfixes are included: * Fixes the issue where handicap stones are added on game load. * Better typing for error callbacks. * Throw errors instead of deadlocking on bad cheat usage. * Return always-resolved gameOver promise after game end * Added a new `resetStats` api function. --------- Co-authored-by: David Walker <d0sboots@gmail.com>
This commit is contained in:
committed by
GitHub
parent
de6b202341
commit
c8d2c9f769
@@ -6,12 +6,15 @@
|
||||
|
||||
Make a move on the IPvGO subnet game board, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI.
|
||||
|
||||
playAsWhite is optional, and attempts to make a move as the white player. Only can be used when playing against "No AI".
|
||||
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
makeMove(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -25,6 +28,7 @@ makeMove(
|
||||
| --- | --- | --- |
|
||||
| x | number | |
|
||||
| y | number | |
|
||||
| playAsWhite | (not declared) | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ export interface Go
|
||||
| [getGameState()](./bitburner.go.getgamestate.md) | Gets the status of the current game. Shows the current player, current score, and the previous move coordinates. Previous move coordinates will be \[-1, -1\] for a pass, or if there are no prior moves. |
|
||||
| [getMoveHistory()](./bitburner.go.getmovehistory.md) | <p>Returns all the prior moves in the current game, as an array of simple board states.</p><p>For example, a single 5x5 prior move board might look like this:</p><p>\[<br/> "XX.O.",<br/> "X..OO",<br/> ".XO..",<br/> "XXO.\#",<br/> ".XO.\#",<br/> \]</p> |
|
||||
| [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. |
|
||||
| [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet game board, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI. |
|
||||
| [opponentNextTurn(logOpponentMove)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. |
|
||||
| [passTurn()](./bitburner.go.passturn.md) | <p>Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.</p><p>This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.</p> |
|
||||
| [makeMove(x, y, playAsWhite)](./bitburner.go.makemove.md) | <p>Make a move on the IPvGO subnet game board, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI.</p><p>playAsWhite is optional, and attempts to make a move as the white player. Only can be used when playing against "No AI".</p> |
|
||||
| [opponentNextTurn(logOpponentMove, playAsWhite)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. |
|
||||
| [passTurn(passAsWhite)](./bitburner.go.passturn.md) | <p>Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.</p><p>This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.</p><p>passAsWhite is optional, and attempts to pass while playing as the white player. Only can be used when playing against "No AI".</p> |
|
||||
| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.</p><p>Note that some factions will have a few routers already on the subnet after a reset.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",</p> |
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ Returns a promise that resolves with the success or failure state of your last m
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
opponentNextTurn(logOpponentMove?: boolean): Promise<{
|
||||
opponentNextTurn(
|
||||
logOpponentMove?: boolean,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
@@ -21,6 +24,7 @@ opponentNextTurn(logOpponentMove?: boolean): Promise<{
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| logOpponentMove | boolean | _(Optional)_ optional, defaults to true. if false prevents logging opponent move |
|
||||
| playAsWhite | (not declared) | _(Optional)_ optional. If true, waits to get the next move the black player makes. Intended to be used when playing as white when the opponent is set to "No AI" |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -8,15 +8,24 @@ Pass the player's turn rather than making a move, and await the opponent's respo
|
||||
|
||||
This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.
|
||||
|
||||
passAsWhite is optional, and attempts to pass while playing as the white player. Only can be used when playing against "No AI".
|
||||
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
passTurn(): Promise<{
|
||||
passTurn(passAsWhite = false): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
}>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| passAsWhite | (not declared) | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }>
|
||||
|
||||
@@ -14,10 +14,12 @@ Note that the \[0\]\[0\] point is shown on the bottom-left on the visual board (
|
||||
|
||||
Also note that, when given a custom board state, only one prior move can be analyzed. This means that the superko rules (no duplicate board states in the full game history) is not supported; you will have to implement your own analysis for that.
|
||||
|
||||
playAsWhite is optional, and gets the current valid moves for the white player. Intended to be used when playing as white when the opponent is set to "No AI"
|
||||
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
|
||||
getValidMoves(boardState?: string[], priorBoardState?: string[], playAsWhite = false): boolean[][];
|
||||
```
|
||||
|
||||
## Parameters
|
||||
@@ -26,6 +28,7 @@ getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
|
||||
| --- | --- | --- |
|
||||
| boardState | string\[\] | _(Optional)_ |
|
||||
| priorBoardState | string\[\] | _(Optional)_ |
|
||||
| playAsWhite | (not declared) | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -20,5 +20,6 @@ export interface GoAnalysis
|
||||
| [getControlledEmptyNodes(boardState)](./bitburner.goanalysis.getcontrolledemptynodes.md) | <p>Returns 'X' for black, 'O' for white, or '?' for each empty point to indicate which player controls that empty point. If no single player fully encircles the empty space, it is shown as contested with '?'. "\#" are dead nodes that are not part of the subnet.</p><p>Takes an optional boardState argument; by default uses the current board state.</p><p>Filled points of any color are indicated with '.'</p><p>In this example, white encircles some space in the top-left, black encircles some in the top-right, and between their routers is contested space in the center: <pre lang="javascript"> \[ "OO..?", "OO.?.", "O.?.X", ".?.XX", "?..X\#", \] </pre></p> |
|
||||
| [getLiberties(boardState)](./bitburner.goanalysis.getliberties.md) | <p>Returns a number for each point, representing how many open nodes its network/chain is connected to. Empty nodes and dead nodes are shown as -1 liberties.</p><p>Takes an optional boardState argument; by default uses the current board state.</p><p>For example, a 5x5 board might look like this. The chain in the top-left touches 5 total empty nodes, and the one in the center touches four. The group in the bottom-right only has one liberty; it is in danger of being captured! <pre lang="javascript"> \[ \[-1, 5,-1,-1, 2\], \[ 5, 5,-1,-1,-1\], \[-1,-1, 4,-1,-1\], \[ 3,-1,-1, 3, 1\], \[ 3,-1,-1, 3, 1\], \] </pre></p> |
|
||||
| [getStats()](./bitburner.goanalysis.getstats.md) | <p>Displays the game history, captured nodes, and gained bonuses for each opponent you have played against.</p><p>The details are keyed by opponent name, in this structure:</p><p><pre lang="javascript"> { <OpponentName>: { wins: number, losses: number, winStreak: number, highestWinStreak: number, favor: number, bonusPercent: number, bonusDescription: string, } } </pre></p> |
|
||||
| [getValidMoves(boardState, priorBoardState)](./bitburner.goanalysis.getvalidmoves.md) | <p>Shows if each point on the board is a valid move for the player. By default, analyzes the current board state. Takes an optional boardState (and an optional prior-move boardState, if desired) to analyze a custom board.</p><p>The true/false validity of each move can be retrieved via the X and Y coordinates of the move. <code>const validMoves = ns.go.analysis.getValidMoves();</code></p><p><code>const moveIsValid = validMoves[x][y];</code></p><p>Note that the \[0\]\[0\] point is shown on the bottom-left on the visual board (as is traditional), and each string represents a vertical column on the board. In other words, the printed example above can be understood to be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO subnet tab.</p><p>Also note that, when given a custom board state, only one prior move can be analyzed. This means that the superko rules (no duplicate board states in the full game history) is not supported; you will have to implement your own analysis for that.</p> |
|
||||
| [getValidMoves(boardState, priorBoardState, playAsWhite)](./bitburner.goanalysis.getvalidmoves.md) | <p>Shows if each point on the board is a valid move for the player. By default, analyzes the current board state. Takes an optional boardState (and an optional prior-move boardState, if desired) to analyze a custom board.</p><p>The true/false validity of each move can be retrieved via the X and Y coordinates of the move. <code>const validMoves = ns.go.analysis.getValidMoves();</code></p><p><code>const moveIsValid = validMoves[x][y];</code></p><p>Note that the \[0\]\[0\] point is shown on the bottom-left on the visual board (as is traditional), and each string represents a vertical column on the board. In other words, the printed example above can be understood to be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO subnet tab.</p><p>Also note that, when given a custom board state, only one prior move can be analyzed. This means that the superko rules (no duplicate board states in the full game history) is not supported; you will have to implement your own analysis for that.</p><p>playAsWhite is optional, and gets the current valid moves for the white player. Intended to be used when playing as white when the opponent is set to "No AI"</p> |
|
||||
| [resetStats(resetAll)](./bitburner.goanalysis.resetstats.md) | Reset all win/loss and winstreak records for the No AI opponent. |
|
||||
|
||||
|
||||
24
markdown/bitburner.goanalysis.resetstats.md
Normal file
24
markdown/bitburner.goanalysis.resetstats.md
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [GoAnalysis](./bitburner.goanalysis.md) > [resetStats](./bitburner.goanalysis.resetstats.md)
|
||||
|
||||
## GoAnalysis.resetStats() method
|
||||
|
||||
Reset all win/loss and winstreak records for the No AI opponent.
|
||||
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
resetStats(resetAll = false): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| resetAll | (not declared) | _(Optional)_ if true, reset win/loss records for all opponents. Leaves node power and bonuses unchanged. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
void
|
||||
|
||||
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
|
||||
destroyNode(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -27,8 +28,9 @@ destroyNode(
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| x | number | |
|
||||
| y | number | |
|
||||
| x | number | x coordinate of empty node to destroy |
|
||||
| y | number | y coordinate of empty node to destroy |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -9,8 +9,15 @@ Returns the number of times you've attempted to cheat in the current game.
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getCheatCount(): number;
|
||||
getCheatCount(playAsWhite = false): number;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
number
|
||||
|
||||
@@ -11,7 +11,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getCheatSuccessChance(cheatCount?: number): number;
|
||||
getCheatSuccessChance(cheatCount?: number, playAsWhite = false): number;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
@@ -19,6 +19,7 @@ getCheatSuccessChance(cheatCount?: number): number;
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| cheatCount | number | _(Optional)_ Optional override for the number of cheats already attempted. Defaults to the number of cheats attempted in the current game. |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ export interface GoCheat
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [destroyNode(x, y)](./bitburner.gocheat.destroynode.md) | <p>Attempts to destroy an empty node, leaving an offline dead space that does not count as territory or provide open node access to adjacent routers.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [getCheatCount()](./bitburner.gocheat.getcheatcount.md) | Returns the number of times you've attempted to cheat in the current game. |
|
||||
| [getCheatSuccessChance(cheatCount)](./bitburner.gocheat.getcheatsuccesschance.md) | <p>Returns your chance of successfully playing one of the special moves in the ns.go.cheat API. Scales up with your crime success rate stat. Scales down with the number of times you've attempted to cheat in the current game.</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [playTwoMoves(x1, y1, x2, y2)](./bitburner.gocheat.playtwomoves.md) | <p>Attempts to place two routers at once on empty nodes. Note that this ignores other move restrictions, so you can suicide your own routers if they have no access to empty ports and do not capture any enemy routers.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [removeRouter(x, y)](./bitburner.gocheat.removerouter.md) | <p>Attempts to remove an existing router, leaving an empty node behind.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [repairOfflineNode(x, y)](./bitburner.gocheat.repairofflinenode.md) | <p>Attempts to repair an offline node, leaving an empty playable node behind.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [destroyNode(x, y, playAsWhite)](./bitburner.gocheat.destroynode.md) | <p>Attempts to destroy an empty node, leaving an offline dead space that does not count as territory or provide open node access to adjacent routers.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [getCheatCount(playAsWhite)](./bitburner.gocheat.getcheatcount.md) | Returns the number of times you've attempted to cheat in the current game. |
|
||||
| [getCheatSuccessChance(cheatCount, playAsWhite)](./bitburner.gocheat.getcheatsuccesschance.md) | <p>Returns your chance of successfully playing one of the special moves in the ns.go.cheat API. Scales up with your crime success rate stat. Scales down with the number of times you've attempted to cheat in the current game.</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [playTwoMoves(x1, y1, x2, y2, playAsWhite)](./bitburner.gocheat.playtwomoves.md) | <p>Attempts to place two routers at once on empty nodes. Note that this ignores other move restrictions, so you can suicide your own routers if they have no access to empty ports and do not capture any enemy routers.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [removeRouter(x, y, playAsWhite)](./bitburner.gocheat.removerouter.md) | <p>Attempts to remove an existing router, leaving an empty node behind.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
| [repairOfflineNode(x, y, playAsWhite)](./bitburner.gocheat.repairofflinenode.md) | <p>Attempts to repair an offline node, leaving an empty playable node behind.</p><p>Success chance can be seen via ns.go.getCheatSuccessChance()</p><p>Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a small (\~10%) chance you will instantly be ejected from the subnet.</p> |
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ playTwoMoves(
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -29,10 +30,11 @@ playTwoMoves(
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| x1 | number | |
|
||||
| y1 | number | |
|
||||
| x2 | number | |
|
||||
| y2 | number | |
|
||||
| x1 | number | x coordinate of first move to make |
|
||||
| y1 | number | y coordinate of first move to make |
|
||||
| x2 | number | x coordinate of second move to make |
|
||||
| y2 | number | y coordinate of second move to make |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
|
||||
removeRouter(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -27,8 +28,9 @@ removeRouter(
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| x | number | |
|
||||
| y | number | |
|
||||
| x | number | x coordinate of router to remove |
|
||||
| y | number | y coordinate of router to remove |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
|
||||
repairOfflineNode(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -27,8 +28,9 @@ repairOfflineNode(
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| x | number | |
|
||||
| y | number | |
|
||||
| x | number | x coordinate of offline node to repair |
|
||||
| y | number | y coordinate of offline node to repair |
|
||||
| playAsWhite | (not declared) | _(Optional)_ Optional override for playing as white. Can only be used when playing on a 'No AI' board. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
||||
16
src/Go/Go.ts
16
src/Go/Go.ts
@@ -1,26 +1,26 @@
|
||||
import type { BoardState, OpponentStats, Play } from "./Types";
|
||||
import type { BoardState, OpponentStats } from "./Types";
|
||||
|
||||
import { GoPlayType, type GoOpponent } from "@enums";
|
||||
import { getRecordValues, PartialRecord } from "../Types/Record";
|
||||
import type { GoOpponent } from "@enums";
|
||||
import { getRecordKeys, PartialRecord } from "../Types/Record";
|
||||
import { resetAI } from "./boardAnalysis/goAI";
|
||||
import { getNewBoardState } from "./boardState/boardState";
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
import { newOpponentStats } from "./Constants";
|
||||
|
||||
export class GoObject {
|
||||
// Todo: Make previous game a slimmer interface
|
||||
previousGame: BoardState | null = null;
|
||||
currentGame: BoardState = getNewBoardState(7);
|
||||
stats: PartialRecord<GoOpponent, OpponentStats> = {};
|
||||
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
|
||||
storedCycles: number = 0;
|
||||
|
||||
prestigeAugmentation() {
|
||||
for (const stats of getRecordValues(this.stats)) {
|
||||
stats.nodePower = 0;
|
||||
stats.nodes = 0;
|
||||
stats.winStreak = 0;
|
||||
for (const opponent of getRecordKeys(Go.stats)) {
|
||||
Go.stats[opponent] = newOpponentStats();
|
||||
}
|
||||
}
|
||||
prestigeSourceFile() {
|
||||
resetAI();
|
||||
this.previousGame = null;
|
||||
this.currentGame = getNewBoardState(7);
|
||||
this.stats = {};
|
||||
|
||||
@@ -2,18 +2,20 @@ import type { BoardState, OpponentStats, SimpleBoard } from "./Types";
|
||||
import type { PartialRecord } from "../Types/Record";
|
||||
|
||||
import { Truthy } from "lodash";
|
||||
import { GoColor, GoOpponent, GoPlayType } from "@enums";
|
||||
import { GoColor, GoOpponent } from "@enums";
|
||||
import { Go } from "./Go";
|
||||
import { boardStateFromSimpleBoard, getPreviousMove, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
|
||||
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
|
||||
import { assertLoadingType } from "../utils/TypeAssertion";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
import { boardSizes } from "./Constants";
|
||||
import { isInteger, isNumber } from "../types";
|
||||
import { makeAIMove } from "./boardAnalysis/goAI";
|
||||
import { handleNextTurn, resetAI } from "./boardAnalysis/goAI";
|
||||
|
||||
type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
|
||||
type CurrentGameSaveData = PreviousGameSaveData & {
|
||||
previousBoard?: string;
|
||||
cheatCount: number;
|
||||
cheatCountForWhite: number;
|
||||
passCount: number;
|
||||
};
|
||||
|
||||
@@ -36,8 +38,10 @@ export function getGoSave(): SaveFormat {
|
||||
currentGame: {
|
||||
ai: Go.currentGame.ai,
|
||||
board: simpleBoardFromBoard(Go.currentGame.board),
|
||||
previousBoard: Go.currentGame.previousBoards[0] ?? "",
|
||||
previousPlayer: Go.currentGame.previousPlayer,
|
||||
cheatCount: Go.currentGame.cheatCount,
|
||||
cheatCountForWhite: Go.currentGame.cheatCount,
|
||||
passCount: Go.currentGame.passCount,
|
||||
},
|
||||
stats: Go.stats,
|
||||
@@ -82,21 +86,10 @@ export function loadGo(data: unknown): boolean {
|
||||
Go.stats = stats;
|
||||
Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
|
||||
|
||||
// If it's the AI's turn, initiate their turn, which will populate nextTurn
|
||||
if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) {
|
||||
makeAIMove(currentGame).catch((error) => {
|
||||
showError(new Error(`Error while making first IPvGO AI move: ${error}`, { cause: error }));
|
||||
});
|
||||
}
|
||||
// If it's not the AI's turn and we're not in gameover status, initialize nextTurn promise based on the previous move/pass
|
||||
else if (currentGame.previousPlayer) {
|
||||
const previousMove = getPreviousMove();
|
||||
Go.nextTurn = Promise.resolve(
|
||||
previousMove
|
||||
? { type: GoPlayType.move, x: previousMove[0], y: previousMove[1] }
|
||||
: { type: GoPlayType.pass, x: null, y: null },
|
||||
);
|
||||
}
|
||||
resetAI();
|
||||
handleNextTurn(currentGame).catch((error) => {
|
||||
showError(new Error(`Error while initializing first IPvGO move: ${error}`, { cause: error }));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,15 +106,19 @@ function loadCurrentGame(currentGame: unknown): BoardState | string {
|
||||
const board = loadSimpleBoard(currentGame.board, requiredSize);
|
||||
if (typeof board === "string") return board;
|
||||
const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null;
|
||||
if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0)
|
||||
return "invalid number for currentGame.cheatCount";
|
||||
const normalizedCheatCount = isInteger(currentGame.cheatCount) ? Math.max(0, currentGame.cheatCount || 0) : 0;
|
||||
const normalizedCheatCountForWhite = isInteger(currentGame.cheatCountForWhite)
|
||||
? Math.max(0, currentGame.cheatCountForWhite || 0)
|
||||
: 0;
|
||||
if (!isInteger(currentGame.passCount) || currentGame.passCount < 0) return "invalid number for currentGame.passCount";
|
||||
const previousBoards = typeof currentGame.previousBoard === "string" ? [currentGame.previousBoard] : [];
|
||||
|
||||
const boardState = boardStateFromSimpleBoard(board, ai);
|
||||
boardState.previousPlayer = previousPlayer;
|
||||
boardState.cheatCount = currentGame.cheatCount;
|
||||
boardState.cheatCount = normalizedCheatCount;
|
||||
boardState.cheatCountForWhite = normalizedCheatCountForWhite;
|
||||
boardState.passCount = currentGame.passCount;
|
||||
boardState.previousBoards = [];
|
||||
boardState.previousBoards = previousBoards;
|
||||
return boardState;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export type BoardState = {
|
||||
ai: GoOpponent;
|
||||
passCount: number;
|
||||
cheatCount: number;
|
||||
cheatCountForWhite: number;
|
||||
};
|
||||
|
||||
export type PointState = {
|
||||
|
||||
@@ -691,7 +691,7 @@ export function getColorOnBoardString(boardString: string, x: number, y: number)
|
||||
|
||||
/** Find a move made by the previous player, if present. */
|
||||
export function getPreviousMove(): [number, number] | null {
|
||||
const priorBoard = Go.currentGame?.previousBoards[0];
|
||||
const priorBoard = Go.currentGame.previousBoards[0];
|
||||
if (Go.currentGame.passCount || !priorBoard) {
|
||||
return null;
|
||||
}
|
||||
@@ -725,7 +725,7 @@ export function getPreviousMoveDetails(): Play {
|
||||
}
|
||||
|
||||
return {
|
||||
type: !priorMove && Go.currentGame?.passCount ? GoPlayType.pass : GoPlayType.gameOver,
|
||||
type: Go.currentGame.previousPlayer ? GoPlayType.pass : GoPlayType.gameOver,
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
@@ -14,51 +14,75 @@ import {
|
||||
getAllEyes,
|
||||
getAllEyesByChainId,
|
||||
getAllNeighboringChains,
|
||||
getAllValidMoves,
|
||||
getPreviousMoveDetails,
|
||||
} from "./boardAnalysis";
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
|
||||
let isAiThinking: boolean = false;
|
||||
let currentTurnResolver: (() => void) | null = null;
|
||||
type PlayerPromise = {
|
||||
nextTurn: Promise<Play>;
|
||||
resolver: ((play?: Play) => void) | null;
|
||||
};
|
||||
|
||||
const gameOver = { type: GoPlayType.gameOver, x: null, y: null } as const;
|
||||
const playerPromises: Record<GoColor.black | GoColor.white, PlayerPromise> = {
|
||||
[GoColor.black]: { nextTurn: Promise.resolve(gameOver), resolver: null },
|
||||
[GoColor.white]: { nextTurn: Promise.resolve(gameOver), resolver: null },
|
||||
};
|
||||
|
||||
export function getNextTurn(color: GoColor.black | GoColor.white): Promise<Play> {
|
||||
return playerPromises[color].nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a move from the current faction in response to the player's move
|
||||
* Does common processing in response to a move being made.
|
||||
*
|
||||
* Due to asynchronous and/or timer-based functions, this function might be
|
||||
* called multiple times per turn. Therefore, it is (and must be) idempotent.
|
||||
* It is also used to handle the first turn of the game, and post-load
|
||||
* processing.
|
||||
* On the AI's turn, it starts AI processing. On all turns, it does promise
|
||||
* handling and dispatches common events.
|
||||
* @returns the nextTurn promise for the player who just moved
|
||||
*/
|
||||
export function makeAIMove(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
|
||||
// If AI is already taking their turn, return the existing turn.
|
||||
if (isAiThinking) {
|
||||
return Go.nextTurn;
|
||||
export function handleNextTurn(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
|
||||
const previousColor = boardState.previousPlayer;
|
||||
if (previousColor === null) {
|
||||
// The game is over. We shouldn't get here in most circumstances,
|
||||
// because when the game ends resetAI() will be called to resolve promises.
|
||||
// Return an already-resolved promise until a new game is started.
|
||||
return Promise.resolve(gameOver);
|
||||
}
|
||||
isAiThinking = true;
|
||||
let encounteredError = false;
|
||||
const currentColor = previousColor === GoColor.black ? GoColor.white : GoColor.black;
|
||||
// Promises are indexed by who wants to wait on them, not by who triggers them.
|
||||
// So the index color is reversed here.
|
||||
const previousPromise = playerPromises[currentColor];
|
||||
const currentPromise = playerPromises[currentColor === GoColor.black ? GoColor.white : GoColor.black];
|
||||
// If we've already handled this turn, return the existing promise.
|
||||
if (previousPromise.resolver === null) {
|
||||
return currentPromise.nextTurn;
|
||||
}
|
||||
previousPromise.resolver();
|
||||
previousPromise.resolver = null;
|
||||
GoEvents.emit();
|
||||
|
||||
// If the AI is disabled, simply make a promise to be resolved once the player makes a move as white
|
||||
if (boardState.ai === GoOpponent.none) {
|
||||
resetAI();
|
||||
}
|
||||
// If an AI is in use, find the faction's move in response, and resolve the Go.nextTurn promise once it is found and played.
|
||||
else {
|
||||
// If an AI is in use, find the faction's move in response, and recursively call handleNextTurn to resolve the nextTurn promise once it is found and played.
|
||||
if (boardState.ai !== GoOpponent.none && currentColor == GoColor.white) {
|
||||
const currentMoveCount = Go.currentGame.previousBoards.length;
|
||||
Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai, useOfflineCycles).then(
|
||||
async (play): Promise<Play> => {
|
||||
if (boardState !== Go.currentGame) {
|
||||
getMove(boardState, currentColor, Go.currentGame.ai, useOfflineCycles)
|
||||
.then(async (play) => {
|
||||
if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
|
||||
//Stale game
|
||||
encounteredError = true;
|
||||
return play;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle AI passing
|
||||
if (play.type === GoPlayType.pass) {
|
||||
passTurn(boardState, GoColor.white);
|
||||
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over
|
||||
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) {
|
||||
return { type: GoPlayType.gameOver, x: null, y: null };
|
||||
}
|
||||
return play;
|
||||
passTurn(boardState, currentColor);
|
||||
return handleNextTurn(boardState, useOfflineCycles);
|
||||
}
|
||||
|
||||
// Handle AI making a move
|
||||
@@ -66,50 +90,58 @@ export function makeAIMove(boardState: BoardState, useOfflineCycles = true): Pro
|
||||
|
||||
if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
|
||||
console.warn("AI move attempted, but the board state has changed.");
|
||||
encounteredError = true;
|
||||
return play;
|
||||
return;
|
||||
}
|
||||
|
||||
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, GoColor.white);
|
||||
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, currentColor);
|
||||
|
||||
// Handle the AI breaking. This shouldn't ever happen.
|
||||
if (!aiUpdatedBoard) {
|
||||
boardState.previousPlayer = GoColor.white;
|
||||
boardState.previousPlayer = currentColor;
|
||||
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
|
||||
}
|
||||
|
||||
return play;
|
||||
},
|
||||
);
|
||||
// Recursively update promises for the next turn. This can't create an
|
||||
// infinite loop because the recursion is happenning asynchronously from a
|
||||
// delayed promise.
|
||||
return handleNextTurn(boardState, useOfflineCycles);
|
||||
})
|
||||
.catch((error) => exceptionAlert(error));
|
||||
}
|
||||
|
||||
// Once the AI moves (or the player playing as white with No AI moves),
|
||||
// clear the isAiThinking semaphore and update the board UI.
|
||||
Go.nextTurn = Go.nextTurn.finally(() => {
|
||||
if (!encounteredError) {
|
||||
isAiThinking = false;
|
||||
}
|
||||
GoEvents.emit();
|
||||
});
|
||||
|
||||
return Go.nextTurn;
|
||||
}
|
||||
|
||||
export function resetAI(thinking = true) {
|
||||
isAiThinking = thinking;
|
||||
GoEvents.emit();
|
||||
// Update currentTurnResolver to call Go.nextTurn's resolve function with the last played move's details
|
||||
Go.nextTurn = new Promise((resolve) => (currentTurnResolver = () => resolve(getPreviousMoveDetails())));
|
||||
// If we haven't resolved currentPromise yet (for instance, at game start),
|
||||
// we should continue to use it instead of resolving it and creating a new one.
|
||||
if (!currentPromise.resolver) {
|
||||
createPromise(currentPromise);
|
||||
}
|
||||
return currentPromise.nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current turn.
|
||||
* This is used for players manually playing against their script on the no-ai board.
|
||||
* Reset the promises for white and black turns.
|
||||
* This will notify scripts waiting on the old promises with gameOver,
|
||||
* potentially even when it is not their turn.
|
||||
* If the game has already ended, it won't re-notify (that was handled in
|
||||
* endGoGame()), which is why it is important to call this *before* resetting
|
||||
* the board state.
|
||||
*/
|
||||
export function resolveCurrentTurn() {
|
||||
// Call the resolve function on Go.nextTurn, if it exists
|
||||
currentTurnResolver?.();
|
||||
currentTurnResolver = null;
|
||||
export function resetAI(endOfGame = false): void {
|
||||
for (const playerPromise of Object.values(playerPromises)) {
|
||||
if (playerPromise.resolver) {
|
||||
playerPromise.resolver(gameOver);
|
||||
playerPromise.resolver = null;
|
||||
}
|
||||
if (!endOfGame && !playerPromise.resolver) {
|
||||
createPromise(playerPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves with the previous move details when the other player / script / AI makes a move
|
||||
function createPromise(promiseObj: PlayerPromise): void {
|
||||
promiseObj.resolver?.();
|
||||
promiseObj.nextTurn = new Promise((resolve) => {
|
||||
promiseObj.resolver = (play?: Play) => resolve(play ?? getPreviousMoveDetails());
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { newOpponentStats } from "../Constants";
|
||||
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
||||
import { getKomi } from "./goAI";
|
||||
import { getKomi, resetAI } from "./goAI";
|
||||
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { isNotNullish } from "../boardState/boardState";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import { Go } from "../Go";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
|
||||
/**
|
||||
* Returns the score of the current board.
|
||||
@@ -46,11 +46,6 @@ export function endGoGame(boardState: BoardState) {
|
||||
if (boardState.previousPlayer === null) {
|
||||
return;
|
||||
}
|
||||
Go.nextTurn = Promise.resolve({
|
||||
type: GoPlayType.gameOver,
|
||||
x: null,
|
||||
y: null,
|
||||
});
|
||||
|
||||
boardState.previousPlayer = null;
|
||||
const statusToUpdate = getOpponentStats(boardState.ai);
|
||||
@@ -59,7 +54,6 @@ export function endGoGame(boardState: BoardState) {
|
||||
|
||||
if (score[GoColor.black].sum < score[GoColor.white].sum) {
|
||||
resetWinstreak(boardState.ai, true);
|
||||
statusToUpdate.nodePower += Math.floor(score[GoColor.black].sum * 0.25);
|
||||
} else {
|
||||
statusToUpdate.wins++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
@@ -89,6 +83,8 @@ export function endGoGame(boardState: BoardState) {
|
||||
statusToUpdate.nodes += score[GoColor.black].sum;
|
||||
Go.currentGame = boardState;
|
||||
Go.previousGame = boardState;
|
||||
resetAI(true);
|
||||
GoEvents.emit();
|
||||
|
||||
// Update multipliers with new bonuses, once at the end of the game
|
||||
Player.applyEntropy(Player.entropy);
|
||||
@@ -123,7 +119,9 @@ function getColoredPieceCount(boardState: BoardState, color: GoColor) {
|
||||
* Finds all empty spaces fully surrounded by a single player's stones
|
||||
*/
|
||||
function getTerritoryScores(board: Board) {
|
||||
const emptyTerritoryChains = getAllChains(board).filter((chain) => chain?.[0]?.color === GoColor.empty);
|
||||
const emptyTerritoryChains = getAllChains(board).filter(
|
||||
(chain) => chain?.[0]?.color === GoColor.empty && chain.length <= board.length * 2,
|
||||
);
|
||||
|
||||
return emptyTerritoryChains.reduce(
|
||||
(scores, currentChain) => {
|
||||
|
||||
@@ -34,6 +34,7 @@ export function getNewBoardState(
|
||||
ai: ai,
|
||||
passCount: 0,
|
||||
cheatCount: 0,
|
||||
cheatCountForWhite: 0,
|
||||
board: Array.from({ length: boardSize }, (_, x) =>
|
||||
Array.from({ length: boardSize }, (_, y) =>
|
||||
!boardToCopy || boardToCopy?.[x]?.[y]
|
||||
@@ -151,7 +152,18 @@ export function passTurn(boardState: BoardState, player: GoColor, allowEndGame =
|
||||
* Modifies the board in place.
|
||||
*/
|
||||
export function applyHandicap(board: Board, handicap: number): void {
|
||||
const availableMoves = getEmptySpaces(board);
|
||||
const availableMoves = [];
|
||||
for (const column of board) {
|
||||
for (const point of column) {
|
||||
if (point) {
|
||||
if (point.color !== GoColor.empty) {
|
||||
// Game is in progress, don't apply handicap
|
||||
return;
|
||||
}
|
||||
availableMoves.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
const handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
|
||||
const handicapMoves: Move[] = [];
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Board, BoardState, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
import { Board, BoardState, OpponentStats, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { Go } from "../Go";
|
||||
import {
|
||||
getNewBoardState,
|
||||
getNewBoardStateFromSimpleBoard,
|
||||
makeMove,
|
||||
passTurn,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
import { makeAIMove, resetAI } from "../boardAnalysis/goAI";
|
||||
import { getNextTurn, handleNextTurn, resetAI } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
getControlledSpace,
|
||||
@@ -23,11 +22,13 @@ import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardA
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
import { CalculateEffect, getEffectTypeForFaction } from "./effect";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { newOpponentStats } from "../Constants";
|
||||
|
||||
/**
|
||||
* Check the move based on the current settings
|
||||
*/
|
||||
export function validateMove(error: (s: string) => void, x: number, y: number, methodName = "", settings = {}) {
|
||||
export function validateMove(error: (s: string) => never, x: number, y: number, methodName = "", settings = {}): void {
|
||||
const check = {
|
||||
emptyNode: true,
|
||||
requireNonEmptyNode: false,
|
||||
@@ -35,9 +36,23 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
onlineNode: true,
|
||||
requireOfflineNode: false,
|
||||
suicide: true,
|
||||
playAsWhite: false,
|
||||
pass: false,
|
||||
...settings,
|
||||
};
|
||||
|
||||
const moveString = methodName + (check.pass ? "" : ` ${x},${y}`) + (check.playAsWhite ? " (White)" : "") + ": ";
|
||||
const moveColor = check.playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
if (check.playAsWhite) {
|
||||
validatePlayAsWhite(error);
|
||||
}
|
||||
validateTurn(error, moveString, moveColor);
|
||||
|
||||
if (check.pass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const boardSize = Go.currentGame.board.length;
|
||||
if (x < 0 || x >= boardSize) {
|
||||
error(`Invalid column number (x = ${x}), column must be a number 0 through ${boardSize - 1}`);
|
||||
@@ -46,10 +61,7 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
error(`Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`);
|
||||
}
|
||||
|
||||
const moveString = `${methodName} ${x},${y}: `;
|
||||
validateTurn(error, moveString);
|
||||
|
||||
const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, GoColor.black);
|
||||
const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, moveColor);
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point && check.onlineNode) {
|
||||
error(
|
||||
@@ -88,8 +100,18 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
}
|
||||
}
|
||||
|
||||
export function validateTurn(error: (s: string) => void, moveString = "") {
|
||||
if (Go.currentGame.previousPlayer === GoColor.black) {
|
||||
function validatePlayAsWhite(error: (s: string) => never) {
|
||||
if (Go.currentGame.ai !== GoOpponent.none) {
|
||||
error(`${GoValidity.invalid}. You can only play as white when playing against 'No AI'`);
|
||||
}
|
||||
|
||||
if (Go.currentGame.previousPlayer === GoColor.white) {
|
||||
error(`${GoValidity.notYourTurn}. You cannot play or pass as white until the opponent has played.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateTurn(error: (s: string) => never, moveString = "", color = GoColor.black) {
|
||||
if (Go.currentGame.previousPlayer === color) {
|
||||
error(
|
||||
`${moveString} ${GoValidity.notYourTurn}. Do you have multiple scripts running, or did you forget to await makeMove() or opponentNextTurn()`,
|
||||
);
|
||||
@@ -104,42 +126,48 @@ export function validateTurn(error: (s: string) => void, moveString = "") {
|
||||
/**
|
||||
* Pass player's turn and await the opponent's response (or logs the end of the game if both players pass)
|
||||
*/
|
||||
export async function handlePassTurn(logger: (s: string) => void) {
|
||||
passTurn(Go.currentGame, GoColor.black);
|
||||
export function handlePassTurn(logger: (s: string) => void, passAsWhite = false) {
|
||||
const color = passAsWhite ? GoColor.white : GoColor.black;
|
||||
passTurn(Go.currentGame, color);
|
||||
logger("Go turn passed.");
|
||||
|
||||
if (Go.currentGame.previousPlayer === null) {
|
||||
logEndGame(logger);
|
||||
return getOpponentNextMove(false, logger);
|
||||
} else {
|
||||
return makeAIMove(Go.currentGame);
|
||||
}
|
||||
return handleNextTurn(Go.currentGame, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and applies the player's router placement
|
||||
*/
|
||||
export async function makePlayerMove(logger: (s: string) => void, error: (s: string) => void, x: number, y: number) {
|
||||
export function makePlayerMove(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
) {
|
||||
const boardState = Go.currentGame;
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black);
|
||||
const moveWasMade = makeMove(boardState, x, y, GoColor.black);
|
||||
const color = playAsWhite ? GoColor.white : GoColor.black;
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, color);
|
||||
const moveWasMade = makeMove(boardState, x, y, color);
|
||||
|
||||
if (validity !== GoValidity.valid || !moveWasMade) {
|
||||
error(`Invalid move: ${x} ${y}. ${validity}.`);
|
||||
}
|
||||
|
||||
GoEvents.emit();
|
||||
logger(`Go move played: ${x}, ${y}`);
|
||||
return makeAIMove(boardState);
|
||||
logger(`Go move played: ${x}, ${y}${playAsWhite ? " (White)" : ""}`);
|
||||
return handleNextTurn(boardState, true);
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the promise that provides the opponent's move, once it finishes thinking.
|
||||
*/
|
||||
export async function getOpponentNextMove(logOpponentMove = true, logger: (s: string) => void) {
|
||||
export function getOpponentNextMove(logger: (s: string) => void, logOpponentMove = true, playAsWhite = false) {
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
const nextTurn = getNextTurn(playerColor);
|
||||
// Only asynchronously log the opponent move if not disabled by the player
|
||||
if (logOpponentMove) {
|
||||
return Go.nextTurn.then((move) => {
|
||||
return nextTurn.then((move) => {
|
||||
if (move.type === GoPlayType.gameOver) {
|
||||
logEndGame(logger);
|
||||
} else if (move.type === GoPlayType.pass) {
|
||||
@@ -151,18 +179,25 @@ export async function getOpponentNextMove(logOpponentMove = true, logger: (s: st
|
||||
});
|
||||
}
|
||||
|
||||
return Go.nextTurn;
|
||||
return nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player
|
||||
*/
|
||||
export function getValidMoves(_boardState?: BoardState) {
|
||||
export function getValidMoves(_boardState?: BoardState, playAsWhite = false) {
|
||||
const boardState = _boardState || Go.currentGame;
|
||||
const color = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
// If the game is over, or if it is not your turn, there are no valid moves
|
||||
if (!boardState.previousPlayer || boardState.previousPlayer === color) {
|
||||
return boardState.board.map((): boolean[] => Array(boardState.board.length).fill(false) as boolean[]);
|
||||
}
|
||||
|
||||
// Map the board matrix into true/false values
|
||||
return boardState.board.map((column, x) =>
|
||||
column.reduce((validityArray: boolean[], point, y) => {
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, GoColor.black) === GoValidity.valid;
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, color) === GoValidity.valid;
|
||||
validityArray.push(isValid);
|
||||
return validityArray;
|
||||
}, []),
|
||||
@@ -229,6 +264,13 @@ export function getControlledEmptyNodes(_board?: Board) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all previous board states as SimpleBoards
|
||||
*/
|
||||
export function getHistory(): string[][] {
|
||||
return Go.currentGame.previousBoards.map((boardString): string[] => simpleBoardFromBoardString(boardString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of the current game.
|
||||
* Shows the current player, current score, and the previous move coordinates.
|
||||
@@ -300,9 +342,9 @@ export function resetBoardState(
|
||||
resetWinstreak(oldBoardState.ai, false);
|
||||
}
|
||||
|
||||
resetAI();
|
||||
Go.currentGame = getNewBoardState(boardSize, opponent, true);
|
||||
resetAI(false);
|
||||
GoEvents.emit(); // Trigger a Go UI rerender
|
||||
handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
|
||||
logger(`New game started: ${opponent}, ${boardSize}x${boardSize}`);
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
}
|
||||
@@ -331,6 +373,27 @@ export function getStats() {
|
||||
return statDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all win/loss numbers for the No AI opponent.
|
||||
* @param resetAll if true, reset win/loss records for all opponents. This leaves node power and bonuses unchanged.
|
||||
*/
|
||||
export function resetStats(resetAll = false) {
|
||||
if (resetAll) {
|
||||
for (const opponent of getRecordKeys(Go.stats)) {
|
||||
Go.stats[opponent] = {
|
||||
...(Go.stats[opponent] as OpponentStats),
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
Go.stats[GoOpponent.none] = newOpponentStats();
|
||||
}
|
||||
}
|
||||
|
||||
const boardValidity = {
|
||||
valid: "",
|
||||
badShape: "Invalid boardState: Board must be a square",
|
||||
@@ -345,7 +408,7 @@ const boardValidity = {
|
||||
* Validate the given SimpleBoard and prior board state (if present) and turn it into a full BoardState with updated analytics
|
||||
*/
|
||||
export function validateBoardState(
|
||||
error: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
_boardState?: unknown,
|
||||
_priorBoardState?: unknown,
|
||||
): BoardState | undefined {
|
||||
@@ -366,7 +429,7 @@ export function validateBoardState(
|
||||
/**
|
||||
* Check that the given boardState is a valid SimpleBoard, and return it if it is.
|
||||
*/
|
||||
function getSimpleBoardFromUnknown(error: (arg0: string) => void, _boardState: unknown): SimpleBoard | undefined {
|
||||
function getSimpleBoardFromUnknown(error: (arg0: string) => never, _boardState: unknown): SimpleBoard | undefined {
|
||||
if (!_boardState) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -392,7 +455,7 @@ function getSimpleBoardFromUnknown(error: (arg0: string) => void, _boardState: u
|
||||
}
|
||||
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
export function checkCheatApiAccess(error: (s: string) => never): void {
|
||||
const hasSourceFile = Player.activeSourceFileLvl(14) > 1;
|
||||
const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14;
|
||||
if (!hasSourceFile && !isBitnodeFourteenTwo) {
|
||||
@@ -408,35 +471,43 @@ export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
*
|
||||
* If it fails, determines if the player's turn is skipped, or if the player is ejected from the subnet.
|
||||
*/
|
||||
export async function determineCheatSuccess(
|
||||
export function determineCheatSuccess(
|
||||
logger: (s: string) => void,
|
||||
callback: () => void,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const state = Go.currentGame;
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
state.passCount = 0;
|
||||
const priorCheatCount = playAsWhite ? state.cheatCountForWhite : state.cheatCount;
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
// If cheat is successful, run callback
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount, playAsWhite)) {
|
||||
callback();
|
||||
GoEvents.emit();
|
||||
}
|
||||
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
||||
else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) {
|
||||
else if (priorCheatCount && (ejectRngOverride ?? rng.random()) < 0.1 && state.ai !== GoOpponent.none) {
|
||||
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||
endGoGame(state);
|
||||
return Go.nextTurn;
|
||||
}
|
||||
// If the cheat fails, your turn is skipped
|
||||
else {
|
||||
return handleNextTurn(state, true);
|
||||
} else {
|
||||
// If the cheat fails, your turn is skipped
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, GoColor.black, false);
|
||||
passTurn(state, playerColor, false);
|
||||
}
|
||||
|
||||
state.cheatCount++;
|
||||
return makeAIMove(state);
|
||||
if (playAsWhite) {
|
||||
state.cheatCountForWhite++;
|
||||
} else {
|
||||
state.cheatCount++;
|
||||
}
|
||||
Go.currentGame.previousPlayer = playerColor;
|
||||
updateCaptures(Go.currentGame.board, playerColor, true);
|
||||
|
||||
return handleNextTurn(state, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,7 +527,9 @@ export async function determineCheatSuccess(
|
||||
* 12: +534,704%
|
||||
* 15: +31,358,645%
|
||||
*/
|
||||
export function cheatSuccessChance(cheatCount: number) {
|
||||
export function cheatSuccessChance(cheatCountOverride: number, playAsWhite = false) {
|
||||
const cheatCount =
|
||||
cheatCountOverride ?? (playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount);
|
||||
const sourceFileBonus = Player.activeSourceFileLvl(14) === 3 ? 0.25 : 0;
|
||||
const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount;
|
||||
return Math.max(Math.min(0.6 * cheatCountScalar * Player.mults.crime_success + sourceFileBonus, 1), 0);
|
||||
@@ -467,26 +540,26 @@ export function cheatSuccessChance(cheatCount: number) {
|
||||
*/
|
||||
export function cheatRemoveRouter(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x: number,
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point) {
|
||||
logger(`Cheat failed. The point ${x},${y} is already offline.`);
|
||||
return Go.nextTurn;
|
||||
error(`Cheat failed. The point ${x},${y} is already offline.`);
|
||||
}
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point.color = GoColor.empty;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -495,33 +568,34 @@ export function cheatRemoveRouter(
|
||||
*/
|
||||
export function cheatPlayTwoMoves(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const point1 = Go.currentGame.board[x1][y1];
|
||||
const point2 = Go.currentGame.board[x2][y2];
|
||||
|
||||
if (!point1 || !point2) {
|
||||
logger(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`);
|
||||
return Go.nextTurn;
|
||||
error(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`);
|
||||
}
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point1.color = GoColor.black;
|
||||
point2.color = GoColor.black;
|
||||
updateCaptures(Go.currentGame.board, GoColor.black);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
point1.color = playerColor;
|
||||
point2.color = playerColor;
|
||||
|
||||
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -531,6 +605,7 @@ export function cheatRepairOfflineNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
@@ -542,12 +617,11 @@ export function cheatRepairOfflineNode(
|
||||
color: GoColor.empty,
|
||||
x,
|
||||
};
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -557,16 +631,16 @@ export function cheatDestroyNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Go.currentGame.board[x][y] = null;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was destroyed.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { makeAIMove, resetAI, resolveCurrentTurn } from "../boardAnalysis/goAI";
|
||||
import { handleNextTurn, resetAI } from "../boardAnalysis/goAI";
|
||||
import { GoScoreExplanation } from "./GoScoreExplanation";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
|
||||
@@ -94,48 +94,29 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
|
||||
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
|
||||
if (didUpdateBoard) {
|
||||
rerender();
|
||||
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
|
||||
}
|
||||
}
|
||||
|
||||
function passPlayerTurn() {
|
||||
if (boardState.previousPlayer === GoColor.white) {
|
||||
passTurn(boardState, GoColor.black);
|
||||
rerender();
|
||||
}
|
||||
if (boardState.previousPlayer === GoColor.black && boardState.ai === GoOpponent.none) {
|
||||
passTurn(boardState, GoColor.white);
|
||||
rerender();
|
||||
}
|
||||
if (boardState.previousPlayer === null) {
|
||||
setScoreOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
|
||||
}, 100);
|
||||
passTurn(boardState, boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black);
|
||||
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
|
||||
}
|
||||
|
||||
async function takeAiTurn(boardState: BoardState) {
|
||||
// If white is being played manually, halt and notify any scripts playing as black if present, instead of making an AI move
|
||||
if (Go.currentGame.ai === GoOpponent.none) {
|
||||
Go.currentGame.previousPlayer && resolveCurrentTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
const move = await makeAIMove(boardState, false);
|
||||
const move = await handleNextTurn(boardState, false);
|
||||
|
||||
if (move.type === GoPlayType.pass) {
|
||||
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
|
||||
rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
if (move.type === GoPlayType.gameOver || move.x === null || move.y === null) {
|
||||
if (boardState.previousPlayer === null) {
|
||||
setScoreOpen(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +133,9 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
resetWinstreak(boardState.ai, false);
|
||||
}
|
||||
|
||||
resetAI();
|
||||
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true);
|
||||
resetAI(false);
|
||||
rerender();
|
||||
resolveCurrentTurn();
|
||||
handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
|
||||
}
|
||||
|
||||
function getPriorMove() {
|
||||
|
||||
@@ -63,18 +63,22 @@ export const GoHistoryPage = (): React.ReactElement => {
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Wins:</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
Wins:{faction === GoOpponent.none ? " (Black / White)" : ""}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
{data.wins} / {data.losses + data.wins}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Current winstreak:</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
Current winstreak{faction === GoOpponent.none ? " for black" : ""}:
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>{data.winStreak}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
Highest winstreak:
|
||||
Highest winstreak{faction === GoOpponent.none ? " for black" : ""}:
|
||||
</TableCell>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
{data.highestWinStreak}
|
||||
|
||||
@@ -270,6 +270,7 @@ const go = {
|
||||
getLiberties: 16,
|
||||
getControlledEmptyNodes: 16,
|
||||
getStats: 0,
|
||||
resetStats: 0,
|
||||
},
|
||||
cheat: {
|
||||
getCheatSuccessChance: 1,
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
handlePassTurn,
|
||||
makePlayerMove,
|
||||
resetBoardState,
|
||||
resetStats,
|
||||
validateBoardState,
|
||||
validateMove,
|
||||
validateTurn,
|
||||
} from "../Go/effects/netscriptGoImplementation";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
import { errorMessage } from "../Netscript/ErrorMessages";
|
||||
@@ -43,19 +43,20 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
return {
|
||||
makeMove:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_x, _y): Promise<Play> => {
|
||||
(_x, _y, playAsWhite): Promise<Play> => {
|
||||
const x = helpers.number(ctx, "x", _x);
|
||||
const y = helpers.number(ctx, "y", _y);
|
||||
validateMove(error(ctx), x, y, "makeMove");
|
||||
return makePlayerMove(logger(ctx), error(ctx), x, y);
|
||||
validateMove(error(ctx), x, y, "makeMove", { playAsWhite });
|
||||
return makePlayerMove(logger(ctx), error(ctx), x, y, !!playAsWhite);
|
||||
},
|
||||
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
|
||||
validateTurn(error(ctx), "passTurn()");
|
||||
return handlePassTurn(logger(ctx));
|
||||
},
|
||||
opponentNextTurn: (ctx: NetscriptContext) => async (_logOpponentMove) => {
|
||||
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true;
|
||||
return getOpponentNextMove(logOpponentMove, logger(ctx));
|
||||
passTurn:
|
||||
(ctx: NetscriptContext) =>
|
||||
(playAsWhite): Promise<Play> => {
|
||||
validateMove(error(ctx), -1, -1, "passTurn", { playAsWhite, pass: true });
|
||||
return handlePassTurn(logger(ctx), !!playAsWhite);
|
||||
},
|
||||
opponentNextTurn: (ctx: NetscriptContext) => async (logOpponentMove, playAsWhite) => {
|
||||
return getOpponentNextMove(logger(ctx), !!logOpponentMove, !!playAsWhite);
|
||||
},
|
||||
getBoardState: () => () => {
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
@@ -79,9 +80,9 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
return resetBoardState(logger(ctx), error(ctx), opponent, boardSize);
|
||||
},
|
||||
analysis: {
|
||||
getValidMoves: (ctx) => (_boardState, _priorBoardState) => {
|
||||
getValidMoves: (ctx) => (_boardState, _priorBoardState, playAsWhite) => {
|
||||
const State = validateBoardState(error(ctx), _boardState, _priorBoardState);
|
||||
return getValidMoves(State);
|
||||
return getValidMoves(State, !!playAsWhite);
|
||||
},
|
||||
getChains: (ctx) => (_boardState) => {
|
||||
const State = validateBoardState(error(ctx), _boardState);
|
||||
@@ -98,22 +99,27 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
getStats: () => () => {
|
||||
return getStats();
|
||||
},
|
||||
resetStats:
|
||||
() =>
|
||||
(resetAll = false) => {
|
||||
resetStats(!!resetAll);
|
||||
},
|
||||
},
|
||||
cheat: {
|
||||
getCheatSuccessChance:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_cheatCount = Go.currentGame.cheatCount) => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
const cheatCount = helpers.number(ctx, "cheatCount", _cheatCount);
|
||||
return cheatSuccessChance(cheatCount);
|
||||
},
|
||||
getCheatCount: (ctx: NetscriptContext) => () => {
|
||||
getCheatSuccessChance: (ctx: NetscriptContext) => (_cheatCount, playAsWhite) => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
return Go.currentGame.cheatCount;
|
||||
const normalizedCheatCount =
|
||||
_cheatCount ?? (playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount);
|
||||
const cheatCount = helpers.number(ctx, "cheatCount", normalizedCheatCount);
|
||||
return cheatSuccessChance(cheatCount, !!playAsWhite);
|
||||
},
|
||||
getCheatCount: (ctx: NetscriptContext) => (playAsWhite) => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
return playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount;
|
||||
},
|
||||
removeRouter:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_x, _y): Promise<Play> => {
|
||||
(_x, _y, playAsWhite): Promise<Play> => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
const x = helpers.number(ctx, "x", _x);
|
||||
const y = helpers.number(ctx, "y", _y);
|
||||
@@ -122,32 +128,35 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
requireNonEmptyNode: true,
|
||||
repeat: false,
|
||||
suicide: false,
|
||||
playAsWhite: playAsWhite,
|
||||
});
|
||||
|
||||
return cheatRemoveRouter(logger(ctx), x, y);
|
||||
return cheatRemoveRouter(logger(ctx), error(ctx), x, y, undefined, undefined, !!playAsWhite);
|
||||
},
|
||||
playTwoMoves:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_x1, _y1, _x2, _y2): Promise<Play> => {
|
||||
(_x1, _y1, _x2, _y2, playAsWhite): Promise<Play> => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
const x1 = helpers.number(ctx, "x", _x1);
|
||||
const y1 = helpers.number(ctx, "y", _y1);
|
||||
validateMove(error(ctx), x1, y1, "playTwoMoves", {
|
||||
repeat: false,
|
||||
suicide: false,
|
||||
playAsWhite,
|
||||
});
|
||||
const x2 = helpers.number(ctx, "x", _x2);
|
||||
const y2 = helpers.number(ctx, "y", _y2);
|
||||
validateMove(error(ctx), x2, y2, "playTwoMoves", {
|
||||
repeat: false,
|
||||
suicide: false,
|
||||
playAsWhite,
|
||||
});
|
||||
|
||||
return cheatPlayTwoMoves(logger(ctx), x1, y1, x2, y2);
|
||||
return cheatPlayTwoMoves(logger(ctx), error(ctx), x1, y1, x2, y2, undefined, undefined, !!playAsWhite);
|
||||
},
|
||||
repairOfflineNode:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_x, _y): Promise<Play> => {
|
||||
(_x, _y, playAsWhite): Promise<Play> => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
const x = helpers.number(ctx, "x", _x);
|
||||
const y = helpers.number(ctx, "y", _y);
|
||||
@@ -157,13 +166,14 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
onlineNode: false,
|
||||
requireOfflineNode: true,
|
||||
suicide: false,
|
||||
playAsWhite,
|
||||
});
|
||||
|
||||
return cheatRepairOfflineNode(logger(ctx), x, y);
|
||||
return cheatRepairOfflineNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
|
||||
},
|
||||
destroyNode:
|
||||
(ctx: NetscriptContext) =>
|
||||
(_x, _y): Promise<Play> => {
|
||||
(_x, _y, playAsWhite): Promise<Play> => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
const x = helpers.number(ctx, "x", _x);
|
||||
const y = helpers.number(ctx, "y", _y);
|
||||
@@ -171,9 +181,10 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
repeat: false,
|
||||
onlineNode: true,
|
||||
suicide: false,
|
||||
playAsWhite,
|
||||
});
|
||||
|
||||
return cheatDestroyNode(logger(ctx), x, y);
|
||||
return cheatDestroyNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
53
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
53
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@@ -4360,11 +4360,13 @@ export interface GoAnalysis {
|
||||
* Also note that, when given a custom board state, only one prior move can be analyzed. This means that the superko rules
|
||||
* (no duplicate board states in the full game history) is not supported; you will have to implement your own analysis for that.
|
||||
*
|
||||
* playAsWhite is optional, and gets the current valid moves for the white player. Intended to be used when playing as white when the opponent is set to "No AI"
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* (This is intentionally expensive; you can derive this info from just getBoardState() )
|
||||
*/
|
||||
getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
|
||||
getValidMoves(boardState?: string[], priorBoardState?: string[], playAsWhite = false): boolean[][];
|
||||
|
||||
/**
|
||||
* Returns an ID for each point. All points that share an ID are part of the same network (or "chain"). Empty points
|
||||
@@ -4463,6 +4465,12 @@ export interface GoAnalysis {
|
||||
* </pre>
|
||||
*/
|
||||
getStats(): Partial<Record<GoOpponent, SimpleOpponentStats>>;
|
||||
|
||||
/**
|
||||
* Reset all win/loss and winstreak records for the No AI opponent.
|
||||
* @param resetAll if true, reset win/loss records for all opponents. Leaves node power and bonuses unchanged.
|
||||
*/
|
||||
resetStats(resetAll = false): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4480,20 +4488,22 @@ export interface GoCheat {
|
||||
* small (~10%) chance you will instantly be ejected from the subnet.
|
||||
*
|
||||
* @param cheatCount - Optional override for the number of cheats already attempted. Defaults to the number of cheats attempted in the current game.
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 1 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
*/
|
||||
getCheatSuccessChance(cheatCount?: number): number;
|
||||
getCheatSuccessChance(cheatCount?: number, playAsWhite = false): number;
|
||||
/**
|
||||
* Returns the number of times you've attempted to cheat in the current game.
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 1 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
*/
|
||||
getCheatCount(): number;
|
||||
getCheatCount(playAsWhite = false): number;
|
||||
/**
|
||||
* Attempts to remove an existing router, leaving an empty node behind.
|
||||
*
|
||||
@@ -4502,6 +4512,11 @@ export interface GoCheat {
|
||||
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
|
||||
* small (~10%) chance you will instantly be ejected from the subnet.
|
||||
*
|
||||
*
|
||||
* @param x - x coordinate of router to remove
|
||||
* @param y - y coordinate of router to remove
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
@@ -4511,6 +4526,7 @@ export interface GoCheat {
|
||||
removeRouter(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -4525,6 +4541,13 @@ export interface GoCheat {
|
||||
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
|
||||
* small (~10%) chance you will instantly be ejected from the subnet.
|
||||
*
|
||||
*
|
||||
* @param x1 - x coordinate of first move to make
|
||||
* @param y1 - y coordinate of first move to make
|
||||
* @param x2 - x coordinate of second move to make
|
||||
* @param y2 - y coordinate of second move to make
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
@@ -4536,6 +4559,7 @@ export interface GoCheat {
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -4550,6 +4574,10 @@ export interface GoCheat {
|
||||
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
|
||||
* small (~10%) chance you will instantly be ejected from the subnet.
|
||||
*
|
||||
* @param x - x coordinate of offline node to repair
|
||||
* @param y - y coordinate of offline node to repair
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
@@ -4559,6 +4587,7 @@ export interface GoCheat {
|
||||
repairOfflineNode(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -4574,6 +4603,10 @@ export interface GoCheat {
|
||||
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
|
||||
* small (~10%) chance you will instantly be ejected from the subnet.
|
||||
*
|
||||
* @param x - x coordinate of empty node to destroy
|
||||
* @param y - y coordinate of empty node to destroy
|
||||
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* Requires BitNode 14.2 to use
|
||||
@@ -4583,6 +4616,7 @@ export interface GoCheat {
|
||||
destroyNode(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -4599,6 +4633,8 @@ export interface Go {
|
||||
* Make a move on the IPvGO subnet game board, and await the opponent's response.
|
||||
* x:0 y:0 represents the bottom-left corner of the board in the UI.
|
||||
*
|
||||
* playAsWhite is optional, and attempts to make a move as the white player. Only can be used when playing against "No AI".
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 4 GB
|
||||
*
|
||||
@@ -4607,6 +4643,7 @@ export interface Go {
|
||||
makeMove(
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
@@ -4620,13 +4657,15 @@ export interface Go {
|
||||
* This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was
|
||||
* closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.
|
||||
*
|
||||
* passAsWhite is optional, and attempts to pass while playing as the white player. Only can be used when playing against "No AI".
|
||||
*
|
||||
* @returns a promise that contains the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
*/
|
||||
passTurn(): Promise<{
|
||||
passTurn(passAsWhite = false): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
@@ -4637,13 +4676,17 @@ export interface Go {
|
||||
* x:0 y:0 represents the bottom-left corner of the board in the UI.
|
||||
*
|
||||
* @param logOpponentMove - optional, defaults to true. if false prevents logging opponent move
|
||||
* @param playAsWhite - optional. If true, waits to get the next move the black player makes. Intended to be used when playing as white when the opponent is set to "No AI"
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* @returns a promise that contains if your last move was valid and successful, the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended
|
||||
*/
|
||||
opponentNextTurn(logOpponentMove?: boolean): Promise<{
|
||||
opponentNextTurn(
|
||||
logOpponentMove?: boolean,
|
||||
playAsWhite = false,
|
||||
): Promise<{
|
||||
type: "move" | "pass" | "gameOver";
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
simpleBoardFromBoard,
|
||||
updatedBoardFromSimpleBoard,
|
||||
} from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { resetAI } from "../../../src/Go/boardAnalysis/goAI";
|
||||
import {
|
||||
cheatPlayTwoMoves,
|
||||
cheatRemoveRouter,
|
||||
@@ -39,6 +40,9 @@ jest.mock("../../../src/ui/GameRoot", () => ({
|
||||
toPage: () => ({}),
|
||||
},
|
||||
}));
|
||||
const errFun = (x) => {
|
||||
throw x;
|
||||
};
|
||||
|
||||
setPlayer(new PlayerObject());
|
||||
AddToAllServers(new Server({ hostname: "home" }));
|
||||
@@ -48,20 +52,18 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should handle invalid moves", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
const mockError = jest.fn(() => {
|
||||
throw new Error("Invalid");
|
||||
});
|
||||
resetAI();
|
||||
|
||||
await makePlayerMove(mockLogger, mockError, 0, 0).catch(() => {});
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith("Invalid move: 0 0. That node is already occupied by a piece.");
|
||||
expect(() => makePlayerMove(jest.fn(), errFun, 0, 0)).toThrow(
|
||||
"Invalid move: 0 0. That node is already occupied by a piece.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should update the board with valid player moves", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....."];
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
Go.currentGame = boardState;
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
const mockError = jest.fn();
|
||||
|
||||
@@ -75,6 +77,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("passTurn() tests", () => {
|
||||
it("should handle pass attempts", async () => {
|
||||
Go.currentGame = getNewBoardState(7);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await handlePassTurn(mockLogger);
|
||||
@@ -84,7 +87,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
});
|
||||
|
||||
describe("getBoardState() tests", () => {
|
||||
it("should correctly return a string version of the bard state", () => {
|
||||
it("should correctly return a string version of the board state", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
|
||||
@@ -100,6 +103,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black);
|
||||
boardState.previousBoards = ["OX.........#.....XX...X."];
|
||||
Go.currentGame = boardState;
|
||||
resetAI();
|
||||
|
||||
const result = getGameState();
|
||||
|
||||
@@ -118,6 +122,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should set the player's board to the requested size and opponent", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
const mockError = jest.fn();
|
||||
|
||||
@@ -132,6 +137,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should throw an error if an invalid opponent is requested", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
const mockError = jest.fn();
|
||||
|
||||
@@ -144,6 +150,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should throw an error if an invalid size is requested", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
const mockError = jest.fn();
|
||||
|
||||
@@ -157,6 +164,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should return all valid and invalid moves on the board", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const result = getValidMoves();
|
||||
|
||||
@@ -172,6 +180,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should return all valid and invalid moves on the board, if a board is provided", () => {
|
||||
const currentBoard = [".....", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const board = getNewBoardStateFromSimpleBoard(
|
||||
["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"],
|
||||
@@ -193,6 +202,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should assign an ID to all contiguous chains on the board", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const result = getChains();
|
||||
|
||||
@@ -207,6 +217,7 @@ describe("Netscript Go API 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"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const result = getLiberties();
|
||||
|
||||
@@ -223,6 +234,7 @@ describe("Netscript Go API 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"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const result = getControlledEmptyNodes();
|
||||
|
||||
@@ -232,6 +244,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should show the details for the given board, if provided", () => {
|
||||
const currentBoard = [".....", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
|
||||
const board = updatedBoardFromSimpleBoard(["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]);
|
||||
const result = getControlledEmptyNodes(board);
|
||||
@@ -243,6 +256,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should handle invalid moves", () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockError = jest.fn();
|
||||
validateMove(mockError, 0, 0, "playTwoMoves", {
|
||||
repeat: false,
|
||||
@@ -256,9 +270,10 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0);
|
||||
await cheatPlayTwoMoves(mockLogger, errFun, 4, 3, 3, 4, 0, 0);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4");
|
||||
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.black);
|
||||
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black);
|
||||
@@ -268,9 +283,10 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1);
|
||||
await cheatPlayTwoMoves(mockLogger, errFun, 4, 3, 3, 4, 2, 1);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
|
||||
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty);
|
||||
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty);
|
||||
@@ -280,10 +296,11 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
Go.currentGame.cheatCount = 1;
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
|
||||
await cheatPlayTwoMoves(mockLogger, errFun, 4, 3, 3, 4, 1, 0);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||
expect(Go.currentGame.previousBoards).toEqual([]);
|
||||
});
|
||||
@@ -292,6 +309,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should handle invalid moves", () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockError = jest.fn();
|
||||
validateMove(mockError, 1, 0, "removeRouter", {
|
||||
emptyNode: false,
|
||||
@@ -307,6 +325,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should remove the router if the move is valid", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatRemoveRouter(mockLogger, 0, 0, 0, 0);
|
||||
@@ -318,10 +337,11 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
Go.currentGame.cheatCount = 1;
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
|
||||
await cheatRemoveRouter(mockLogger, errFun, 0, 0, 1, 0);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||
expect(Go.currentGame.previousBoards).toEqual([]);
|
||||
});
|
||||
@@ -330,6 +350,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should handle invalid moves", () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....#"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockError = jest.fn();
|
||||
validateMove(mockError, 0, 0, "repairOfflineNode", {
|
||||
emptyNode: false,
|
||||
@@ -345,6 +366,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
it("should update the board with the repaired node if the cheat is successful", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....#"];
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
resetAI();
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("Board analysis utility tests", () => {
|
||||
ai: GoOpponent.Illuminati,
|
||||
passCount: 0,
|
||||
cheatCount: 0,
|
||||
cheatCountForWhite: 0,
|
||||
});
|
||||
expect(result.board?.length).toEqual(5);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,9 @@ exports[`Check Save File Continuity GoSave continuity 1`] = `
|
||||
".......",
|
||||
],
|
||||
"cheatCount": 0,
|
||||
"cheatCountForWhite": 0,
|
||||
"passCount": 0,
|
||||
"previousBoard": "",
|
||||
"previousPlayer": "White",
|
||||
},
|
||||
"previousGame": null,
|
||||
|
||||
Reference in New Issue
Block a user