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:
Michael Ficocelli
2025-02-02 23:47:16 -05:00
committed by GitHub
parent de6b202341
commit c8d2c9f769
30 changed files with 502 additions and 263 deletions
+4
View File
@@ -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. 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:** **Signature:**
```typescript ```typescript
makeMove( makeMove(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
@@ -25,6 +28,7 @@ makeMove(
| --- | --- | --- | | --- | --- | --- |
| x | number | | | x | number | |
| y | number | | | y | number | |
| playAsWhite | (not declared) | _(Optional)_ |
**Returns:** **Returns:**
+3 -3
View File
@@ -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. | | [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> | | [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. | | [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. | | [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)](./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. | | [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()](./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> | | [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> | | [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> |
+5 -1
View File
@@ -9,7 +9,10 @@ Returns a promise that resolves with the success or failure state of your last m
**Signature:** **Signature:**
```typescript ```typescript
opponentNextTurn(logOpponentMove?: boolean): Promise<{ opponentNextTurn(
logOpponentMove?: boolean,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
y: number | null; y: number | null;
@@ -21,6 +24,7 @@ opponentNextTurn(logOpponentMove?: boolean): Promise<{
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| logOpponentMove | boolean | _(Optional)_ optional, defaults to true. if false prevents logging opponent move | | 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:** **Returns:**
+10 -1
View File
@@ -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. 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:** **Signature:**
```typescript ```typescript
passTurn(): Promise<{ passTurn(passAsWhite = false): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
y: number | null; y: number | null;
}>; }>;
``` ```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| passAsWhite | (not declared) | _(Optional)_ |
**Returns:** **Returns:**
Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt; Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;
@@ -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. 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:** **Signature:**
```typescript ```typescript
getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][]; getValidMoves(boardState?: string[], priorBoardState?: string[], playAsWhite = false): boolean[][];
``` ```
## Parameters ## Parameters
@@ -26,6 +28,7 @@ getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
| --- | --- | --- | | --- | --- | --- |
| boardState | string\[\] | _(Optional)_ | | boardState | string\[\] | _(Optional)_ |
| priorBoardState | string\[\] | _(Optional)_ | | priorBoardState | string\[\] | _(Optional)_ |
| playAsWhite | (not declared) | _(Optional)_ |
**Returns:** **Returns:**
+2 -1
View File
@@ -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> | | [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> | | [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> | | [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. |
@@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [GoAnalysis](./bitburner.goanalysis.md) &gt; [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
+4 -2
View File
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
destroyNode( destroyNode(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
@@ -27,8 +28,9 @@ destroyNode(
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| x | number | | | x | number | x coordinate of empty node to destroy |
| y | number | | | 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:** **Returns:**
+8 -1
View File
@@ -9,8 +9,15 @@ Returns the number of times you've attempted to cheat in the current game.
**Signature:** **Signature:**
```typescript ```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:** **Returns:**
number number
@@ -11,7 +11,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
**Signature:** **Signature:**
```typescript ```typescript
getCheatSuccessChance(cheatCount?: number): number; getCheatSuccessChance(cheatCount?: number, playAsWhite = false): number;
``` ```
## Parameters ## Parameters
@@ -19,6 +19,7 @@ getCheatSuccessChance(cheatCount?: number): number;
| Parameter | Type | Description | | 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. | | 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:** **Returns:**
+6 -6
View File
@@ -16,10 +16,10 @@ export interface GoCheat
| Method | Description | | 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> | | [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()](./bitburner.gocheat.getcheatcount.md) | Returns the number of times you've attempted to cheat in the current game. | | [getCheatCount(playAsWhite)](./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> | | [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)](./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> | | [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)](./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> | | [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)](./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> | | [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> |
+6 -4
View File
@@ -18,6 +18,7 @@ playTwoMoves(
y1: number, y1: number,
x2: number, x2: number,
y2: number, y2: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
@@ -29,10 +30,11 @@ playTwoMoves(
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| x1 | number | | | x1 | number | x coordinate of first move to make |
| y1 | number | | | y1 | number | y coordinate of first move to make |
| x2 | number | | | x2 | number | x coordinate of second move to make |
| y2 | number | | | 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:** **Returns:**
+4 -2
View File
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
removeRouter( removeRouter(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
@@ -27,8 +28,9 @@ removeRouter(
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| x | number | | | x | number | x coordinate of router to remove |
| y | number | | | 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:** **Returns:**
@@ -16,6 +16,7 @@ Warning: if you fail to play a cheat move, your turn will be skipped. After your
repairOfflineNode( repairOfflineNode(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
@@ -27,8 +28,9 @@ repairOfflineNode(
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| x | number | | | x | number | x coordinate of offline node to repair |
| y | number | | | 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:** **Returns:**
+8 -8
View File
@@ -1,26 +1,26 @@
import type { BoardState, OpponentStats, Play } from "./Types"; import type { BoardState, OpponentStats } from "./Types";
import { GoPlayType, type GoOpponent } from "@enums"; import type { GoOpponent } from "@enums";
import { getRecordValues, PartialRecord } from "../Types/Record"; import { getRecordKeys, PartialRecord } from "../Types/Record";
import { resetAI } from "./boardAnalysis/goAI";
import { getNewBoardState } from "./boardState/boardState"; import { getNewBoardState } from "./boardState/boardState";
import { EventEmitter } from "../utils/EventEmitter"; import { EventEmitter } from "../utils/EventEmitter";
import { newOpponentStats } from "./Constants";
export class GoObject { export class GoObject {
// Todo: Make previous game a slimmer interface // Todo: Make previous game a slimmer interface
previousGame: BoardState | null = null; previousGame: BoardState | null = null;
currentGame: BoardState = getNewBoardState(7); currentGame: BoardState = getNewBoardState(7);
stats: PartialRecord<GoOpponent, OpponentStats> = {}; stats: PartialRecord<GoOpponent, OpponentStats> = {};
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
storedCycles: number = 0; storedCycles: number = 0;
prestigeAugmentation() { prestigeAugmentation() {
for (const stats of getRecordValues(this.stats)) { for (const opponent of getRecordKeys(Go.stats)) {
stats.nodePower = 0; Go.stats[opponent] = newOpponentStats();
stats.nodes = 0;
stats.winStreak = 0;
} }
} }
prestigeSourceFile() { prestigeSourceFile() {
resetAI();
this.previousGame = null; this.previousGame = null;
this.currentGame = getNewBoardState(7); this.currentGame = getNewBoardState(7);
this.stats = {}; this.stats = {};
+19 -22
View File
@@ -2,18 +2,20 @@ import type { BoardState, OpponentStats, SimpleBoard } from "./Types";
import type { PartialRecord } from "../Types/Record"; import type { PartialRecord } from "../Types/Record";
import { Truthy } from "lodash"; import { Truthy } from "lodash";
import { GoColor, GoOpponent, GoPlayType } from "@enums"; import { GoColor, GoOpponent } from "@enums";
import { Go } from "./Go"; import { Go } from "./Go";
import { boardStateFromSimpleBoard, getPreviousMove, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis"; import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
import { assertLoadingType } from "../utils/TypeAssertion"; import { assertLoadingType } from "../utils/TypeAssertion";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
import { boardSizes } from "./Constants"; import { boardSizes } from "./Constants";
import { isInteger, isNumber } from "../types"; 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 PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
type CurrentGameSaveData = PreviousGameSaveData & { type CurrentGameSaveData = PreviousGameSaveData & {
previousBoard?: string;
cheatCount: number; cheatCount: number;
cheatCountForWhite: number;
passCount: number; passCount: number;
}; };
@@ -36,8 +38,10 @@ export function getGoSave(): SaveFormat {
currentGame: { currentGame: {
ai: Go.currentGame.ai, ai: Go.currentGame.ai,
board: simpleBoardFromBoard(Go.currentGame.board), board: simpleBoardFromBoard(Go.currentGame.board),
previousBoard: Go.currentGame.previousBoards[0] ?? "",
previousPlayer: Go.currentGame.previousPlayer, previousPlayer: Go.currentGame.previousPlayer,
cheatCount: Go.currentGame.cheatCount, cheatCount: Go.currentGame.cheatCount,
cheatCountForWhite: Go.currentGame.cheatCount,
passCount: Go.currentGame.passCount, passCount: Go.currentGame.passCount,
}, },
stats: Go.stats, stats: Go.stats,
@@ -82,21 +86,10 @@ export function loadGo(data: unknown): boolean {
Go.stats = stats; Go.stats = stats;
Go.storeCycles(loadStoredCycles(parsedData.storedCycles)); Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
// If it's the AI's turn, initiate their turn, which will populate nextTurn resetAI();
if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) { handleNextTurn(currentGame).catch((error) => {
makeAIMove(currentGame).catch((error) => { showError(new Error(`Error while initializing first IPvGO move: ${error}`, { cause: 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 },
);
}
return true; return true;
} }
@@ -113,15 +106,19 @@ function loadCurrentGame(currentGame: unknown): BoardState | string {
const board = loadSimpleBoard(currentGame.board, requiredSize); const board = loadSimpleBoard(currentGame.board, requiredSize);
if (typeof board === "string") return board; if (typeof board === "string") return board;
const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null; const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null;
if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0) const normalizedCheatCount = isInteger(currentGame.cheatCount) ? Math.max(0, currentGame.cheatCount || 0) : 0;
return "invalid number for currentGame.cheatCount"; 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"; 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); const boardState = boardStateFromSimpleBoard(board, ai);
boardState.previousPlayer = previousPlayer; boardState.previousPlayer = previousPlayer;
boardState.cheatCount = currentGame.cheatCount; boardState.cheatCount = normalizedCheatCount;
boardState.cheatCountForWhite = normalizedCheatCountForWhite;
boardState.passCount = currentGame.passCount; boardState.passCount = currentGame.passCount;
boardState.previousBoards = []; boardState.previousBoards = previousBoards;
return boardState; return boardState;
} }
+1
View File
@@ -53,6 +53,7 @@ export type BoardState = {
ai: GoOpponent; ai: GoOpponent;
passCount: number; passCount: number;
cheatCount: number; cheatCount: number;
cheatCountForWhite: number;
}; };
export type PointState = { export type PointState = {
+2 -2
View File
@@ -691,7 +691,7 @@ export function getColorOnBoardString(boardString: string, x: number, y: number)
/** Find a move made by the previous player, if present. */ /** Find a move made by the previous player, if present. */
export function getPreviousMove(): [number, number] | null { export function getPreviousMove(): [number, number] | null {
const priorBoard = Go.currentGame?.previousBoards[0]; const priorBoard = Go.currentGame.previousBoards[0];
if (Go.currentGame.passCount || !priorBoard) { if (Go.currentGame.passCount || !priorBoard) {
return null; return null;
} }
@@ -725,7 +725,7 @@ export function getPreviousMoveDetails(): Play {
} }
return { return {
type: !priorMove && Go.currentGame?.passCount ? GoPlayType.pass : GoPlayType.gameOver, type: Go.currentGame.previousPlayer ? GoPlayType.pass : GoPlayType.gameOver,
x: null, x: null,
y: null, y: null,
}; };
+90 -58
View File
@@ -14,51 +14,75 @@ import {
getAllEyes, getAllEyes,
getAllEyesByChainId, getAllEyesByChainId,
getAllNeighboringChains, getAllNeighboringChains,
getAllValidMoves,
getPreviousMoveDetails, getPreviousMoveDetails,
} from "./boardAnalysis"; } from "./boardAnalysis";
import { findDisputedTerritory } from "./controlledTerritory"; import { findDisputedTerritory } from "./controlledTerritory";
import { findAnyMatchedPatterns } from "./patternMatching"; import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import { Go, GoEvents } from "../Go"; import { Go, GoEvents } from "../Go";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
let isAiThinking: boolean = false; type PlayerPromise = {
let currentTurnResolver: (() => void) | null = null; 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> { export function handleNextTurn(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
// If AI is already taking their turn, return the existing turn. const previousColor = boardState.previousPlayer;
if (isAiThinking) { if (previousColor === null) {
return Go.nextTurn; // 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; const currentColor = previousColor === GoColor.black ? GoColor.white : GoColor.black;
let encounteredError = false; // 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 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) { if (boardState.ai !== GoOpponent.none && currentColor == GoColor.white) {
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 {
const currentMoveCount = Go.currentGame.previousBoards.length; const currentMoveCount = Go.currentGame.previousBoards.length;
Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai, useOfflineCycles).then( getMove(boardState, currentColor, Go.currentGame.ai, useOfflineCycles)
async (play): Promise<Play> => { .then(async (play) => {
if (boardState !== Go.currentGame) { if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
//Stale game //Stale game
encounteredError = true; return;
return play;
} }
// Handle AI passing // Handle AI passing
if (play.type === GoPlayType.pass) { if (play.type === GoPlayType.pass) {
passTurn(boardState, GoColor.white); passTurn(boardState, currentColor);
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over return handleNextTurn(boardState, useOfflineCycles);
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) {
return { type: GoPlayType.gameOver, x: null, y: null };
}
return play;
} }
// Handle AI making a move // 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) { if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
console.warn("AI move attempted, but the board state has changed."); console.warn("AI move attempted, but the board state has changed.");
encounteredError = true; return;
return play;
} }
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. // Handle the AI breaking. This shouldn't ever happen.
if (!aiUpdatedBoard) { if (!aiUpdatedBoard) {
boardState.previousPlayer = GoColor.white; boardState.previousPlayer = currentColor;
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`); console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
} }
// Recursively update promises for the next turn. This can't create an
return play; // 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), // If we haven't resolved currentPromise yet (for instance, at game start),
// clear the isAiThinking semaphore and update the board UI. // we should continue to use it instead of resolving it and creating a new one.
Go.nextTurn = Go.nextTurn.finally(() => { if (!currentPromise.resolver) {
if (!encounteredError) { createPromise(currentPromise);
isAiThinking = false; }
} return currentPromise.nextTurn;
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())));
} }
/** /**
* Resolves the current turn. * Reset the promises for white and black turns.
* This is used for players manually playing against their script on the no-ai board. * 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() { export function resetAI(endOfGame = false): void {
// Call the resolve function on Go.nextTurn, if it exists for (const playerPromise of Object.values(playerPromises)) {
currentTurnResolver?.(); if (playerPromise.resolver) {
currentTurnResolver = null; 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());
});
} }
/* /*
+8 -10
View File
@@ -1,15 +1,15 @@
import type { Board, BoardState, PointState } from "../Types"; import type { Board, BoardState, PointState } from "../Types";
import { Player } from "@player"; import { Player } from "@player";
import { GoOpponent, GoColor, GoPlayType } from "@enums"; import { GoOpponent, GoColor } from "@enums";
import { newOpponentStats } from "../Constants"; import { newOpponentStats } from "../Constants";
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis"; import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
import { getKomi } from "./goAI"; import { getKomi, resetAI } from "./goAI";
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect"; import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
import { isNotNullish } from "../boardState/boardState"; import { isNotNullish } from "../boardState/boardState";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { getEnumHelper } from "../../utils/EnumHelper"; import { getEnumHelper } from "../../utils/EnumHelper";
import { Go } from "../Go"; import { Go, GoEvents } from "../Go";
/** /**
* Returns the score of the current board. * Returns the score of the current board.
@@ -46,11 +46,6 @@ export function endGoGame(boardState: BoardState) {
if (boardState.previousPlayer === null) { if (boardState.previousPlayer === null) {
return; return;
} }
Go.nextTurn = Promise.resolve({
type: GoPlayType.gameOver,
x: null,
y: null,
});
boardState.previousPlayer = null; boardState.previousPlayer = null;
const statusToUpdate = getOpponentStats(boardState.ai); const statusToUpdate = getOpponentStats(boardState.ai);
@@ -59,7 +54,6 @@ export function endGoGame(boardState: BoardState) {
if (score[GoColor.black].sum < score[GoColor.white].sum) { if (score[GoColor.black].sum < score[GoColor.white].sum) {
resetWinstreak(boardState.ai, true); resetWinstreak(boardState.ai, true);
statusToUpdate.nodePower += Math.floor(score[GoColor.black].sum * 0.25);
} else { } else {
statusToUpdate.wins++; statusToUpdate.wins++;
statusToUpdate.oldWinStreak = statusToUpdate.winStreak; statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
@@ -89,6 +83,8 @@ export function endGoGame(boardState: BoardState) {
statusToUpdate.nodes += score[GoColor.black].sum; statusToUpdate.nodes += score[GoColor.black].sum;
Go.currentGame = boardState; Go.currentGame = boardState;
Go.previousGame = boardState; Go.previousGame = boardState;
resetAI(true);
GoEvents.emit();
// Update multipliers with new bonuses, once at the end of the game // Update multipliers with new bonuses, once at the end of the game
Player.applyEntropy(Player.entropy); 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 * Finds all empty spaces fully surrounded by a single player's stones
*/ */
function getTerritoryScores(board: Board) { 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( return emptyTerritoryChains.reduce(
(scores, currentChain) => { (scores, currentChain) => {
+13 -1
View File
@@ -34,6 +34,7 @@ export function getNewBoardState(
ai: ai, ai: ai,
passCount: 0, passCount: 0,
cheatCount: 0, cheatCount: 0,
cheatCountForWhite: 0,
board: Array.from({ length: boardSize }, (_, x) => board: Array.from({ length: boardSize }, (_, x) =>
Array.from({ length: boardSize }, (_, y) => Array.from({ length: boardSize }, (_, y) =>
!boardToCopy || boardToCopy?.[x]?.[y] !boardToCopy || boardToCopy?.[x]?.[y]
@@ -151,7 +152,18 @@ export function passTurn(boardState: BoardState, player: GoColor, allowEndGame =
* Modifies the board in place. * Modifies the board in place.
*/ */
export function applyHandicap(board: Board, handicap: number): void { 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 handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
const handicapMoves: Move[] = []; const handicapMoves: Move[] = [];
+134 -60
View File
@@ -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 { Player } from "@player";
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums"; import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
import { Go, GoEvents } from "../Go"; import { Go } from "../Go";
import { import {
getNewBoardState, getNewBoardState,
getNewBoardStateFromSimpleBoard, getNewBoardStateFromSimpleBoard,
makeMove, makeMove,
passTurn, passTurn,
updateCaptures, updateCaptures,
updateChains,
} from "../boardState/boardState"; } from "../boardState/boardState";
import { makeAIMove, resetAI } from "../boardAnalysis/goAI"; import { getNextTurn, handleNextTurn, resetAI } from "../boardAnalysis/goAI";
import { import {
evaluateIfMoveIsValid, evaluateIfMoveIsValid,
getControlledSpace, getControlledSpace,
@@ -23,11 +22,13 @@ import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardA
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import { getRecordKeys } from "../../Types/Record"; import { getRecordKeys } from "../../Types/Record";
import { CalculateEffect, getEffectTypeForFaction } from "./effect"; import { CalculateEffect, getEffectTypeForFaction } from "./effect";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { newOpponentStats } from "../Constants";
/** /**
* Check the move based on the current settings * 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 = { const check = {
emptyNode: true, emptyNode: true,
requireNonEmptyNode: false, requireNonEmptyNode: false,
@@ -35,9 +36,23 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
onlineNode: true, onlineNode: true,
requireOfflineNode: false, requireOfflineNode: false,
suicide: true, suicide: true,
playAsWhite: false,
pass: false,
...settings, ...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; const boardSize = Go.currentGame.board.length;
if (x < 0 || x >= boardSize) { if (x < 0 || x >= boardSize) {
error(`Invalid column number (x = ${x}), column must be a number 0 through ${boardSize - 1}`); 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}`); error(`Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`);
} }
const moveString = `${methodName} ${x},${y}: `; const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, moveColor);
validateTurn(error, moveString);
const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, GoColor.black);
const point = Go.currentGame.board[x][y]; const point = Go.currentGame.board[x][y];
if (!point && check.onlineNode) { if (!point && check.onlineNode) {
error( 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 = "") { function validatePlayAsWhite(error: (s: string) => never) {
if (Go.currentGame.previousPlayer === GoColor.black) { 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( error(
`${moveString} ${GoValidity.notYourTurn}. Do you have multiple scripts running, or did you forget to await makeMove() or opponentNextTurn()`, `${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) * 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) { export function handlePassTurn(logger: (s: string) => void, passAsWhite = false) {
passTurn(Go.currentGame, GoColor.black); const color = passAsWhite ? GoColor.white : GoColor.black;
passTurn(Go.currentGame, color);
logger("Go turn passed."); logger("Go turn passed.");
if (Go.currentGame.previousPlayer === null) { if (Go.currentGame.previousPlayer === null) {
logEndGame(logger); logEndGame(logger);
return getOpponentNextMove(false, logger);
} else {
return makeAIMove(Go.currentGame);
} }
return handleNextTurn(Go.currentGame, true);
} }
/** /**
* Validates and applies the player's router placement * 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 boardState = Go.currentGame;
const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black); const color = playAsWhite ? GoColor.white : GoColor.black;
const moveWasMade = makeMove(boardState, x, y, GoColor.black); const validity = evaluateIfMoveIsValid(boardState, x, y, color);
const moveWasMade = makeMove(boardState, x, y, color);
if (validity !== GoValidity.valid || !moveWasMade) { if (validity !== GoValidity.valid || !moveWasMade) {
error(`Invalid move: ${x} ${y}. ${validity}.`); error(`Invalid move: ${x} ${y}. ${validity}.`);
} }
GoEvents.emit(); logger(`Go move played: ${x}, ${y}${playAsWhite ? " (White)" : ""}`);
logger(`Go move played: ${x}, ${y}`); return handleNextTurn(boardState, true);
return makeAIMove(boardState);
} }
/** /**
Returns the promise that provides the opponent's move, once it finishes thinking. 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 // Only asynchronously log the opponent move if not disabled by the player
if (logOpponentMove) { if (logOpponentMove) {
return Go.nextTurn.then((move) => { return nextTurn.then((move) => {
if (move.type === GoPlayType.gameOver) { if (move.type === GoPlayType.gameOver) {
logEndGame(logger); logEndGame(logger);
} else if (move.type === GoPlayType.pass) { } 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 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 // Map the board matrix into true/false values
return boardState.board.map((column, x) => return boardState.board.map((column, x) =>
column.reduce((validityArray: boolean[], point, y) => { 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); validityArray.push(isValid);
return validityArray; 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. * Gets the status of the current game.
* Shows the current player, current score, and the previous move coordinates. * Shows the current player, current score, and the previous move coordinates.
@@ -300,9 +342,9 @@ export function resetBoardState(
resetWinstreak(oldBoardState.ai, false); resetWinstreak(oldBoardState.ai, false);
} }
resetAI();
Go.currentGame = getNewBoardState(boardSize, opponent, true); Go.currentGame = getNewBoardState(boardSize, opponent, true);
resetAI(false); handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
GoEvents.emit(); // Trigger a Go UI rerender
logger(`New game started: ${opponent}, ${boardSize}x${boardSize}`); logger(`New game started: ${opponent}, ${boardSize}x${boardSize}`);
return simpleBoardFromBoard(Go.currentGame.board); return simpleBoardFromBoard(Go.currentGame.board);
} }
@@ -331,6 +373,27 @@ export function getStats() {
return statDetails; 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 = { const boardValidity = {
valid: "", valid: "",
badShape: "Invalid boardState: Board must be a square", 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 * Validate the given SimpleBoard and prior board state (if present) and turn it into a full BoardState with updated analytics
*/ */
export function validateBoardState( export function validateBoardState(
error: (s: string) => void, error: (s: string) => never,
_boardState?: unknown, _boardState?: unknown,
_priorBoardState?: unknown, _priorBoardState?: unknown,
): BoardState | undefined { ): BoardState | undefined {
@@ -366,7 +429,7 @@ export function validateBoardState(
/** /**
* Check that the given boardState is a valid SimpleBoard, and return it if it is. * 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) { if (!_boardState) {
return undefined; 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. */ /** 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 hasSourceFile = Player.activeSourceFileLvl(14) > 1;
const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14; const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14;
if (!hasSourceFile && !isBitnodeFourteenTwo) { 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. * 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, logger: (s: string) => void,
callback: () => void, callback: () => void,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
playAsWhite = false,
): Promise<Play> { ): Promise<Play> {
const state = Go.currentGame; const state = Go.currentGame;
const rng = new WHRNG(Player.totalPlaytime); const rng = new WHRNG(Player.totalPlaytime);
state.passCount = 0; state.passCount = 0;
const priorCheatCount = playAsWhite ? state.cheatCountForWhite : state.cheatCount;
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
// If cheat is successful, run callback // If cheat is successful, run callback
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) { if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount, playAsWhite)) {
callback(); callback();
GoEvents.emit();
} }
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing // 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.`); logger(`Cheat failed! You have been ejected from the subnet.`);
endGoGame(state); endGoGame(state);
return Go.nextTurn; return handleNextTurn(state, true);
} } else {
// If the cheat fails, your turn is skipped // If the cheat fails, your turn is skipped
else {
logger(`Cheat failed. Your turn has been skipped.`); logger(`Cheat failed. Your turn has been skipped.`);
passTurn(state, GoColor.black, false); passTurn(state, playerColor, false);
} }
state.cheatCount++; if (playAsWhite) {
return makeAIMove(state); 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% * 12: +534,704%
* 15: +31,358,645% * 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 sourceFileBonus = Player.activeSourceFileLvl(14) === 3 ? 0.25 : 0;
const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount; const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount;
return Math.max(Math.min(0.6 * cheatCountScalar * Player.mults.crime_success + sourceFileBonus, 1), 0); 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( export function cheatRemoveRouter(
logger: (s: string) => void, logger: (s: string) => void,
error: (s: string) => never,
x: number, x: number,
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
playAsWhite = false,
): Promise<Play> { ): Promise<Play> {
const point = Go.currentGame.board[x][y]; const point = Go.currentGame.board[x][y];
if (!point) { if (!point) {
logger(`Cheat failed. The point ${x},${y} is already offline.`); error(`Cheat failed. The point ${x},${y} is already offline.`);
return Go.nextTurn;
} }
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {
point.color = GoColor.empty; point.color = GoColor.empty;
updateChains(Go.currentGame.board);
Go.currentGame.previousPlayer = GoColor.black;
logger(`Cheat successful. The point ${x},${y} was cleared.`); logger(`Cheat successful. The point ${x},${y} was cleared.`);
}, },
successRngOverride, successRngOverride,
ejectRngOverride, ejectRngOverride,
playAsWhite,
); );
} }
@@ -495,33 +568,34 @@ export function cheatRemoveRouter(
*/ */
export function cheatPlayTwoMoves( export function cheatPlayTwoMoves(
logger: (s: string) => void, logger: (s: string) => void,
error: (s: string) => never,
x1: number, x1: number,
y1: number, y1: number,
x2: number, x2: number,
y2: number, y2: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
playAsWhite = false,
): Promise<Play> { ): Promise<Play> {
const point1 = Go.currentGame.board[x1][y1]; const point1 = Go.currentGame.board[x1][y1];
const point2 = Go.currentGame.board[x2][y2]; const point2 = Go.currentGame.board[x2][y2];
if (!point1 || !point2) { if (!point1 || !point2) {
logger(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`); error(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`);
return Go.nextTurn;
} }
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {
point1.color = GoColor.black; point1.color = playerColor;
point2.color = GoColor.black; point2.color = playerColor;
updateCaptures(Go.currentGame.board, GoColor.black);
Go.currentGame.previousPlayer = GoColor.black;
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`); logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
}, },
successRngOverride, successRngOverride,
ejectRngOverride, ejectRngOverride,
playAsWhite,
); );
} }
@@ -531,6 +605,7 @@ export function cheatRepairOfflineNode(
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
playAsWhite = false,
): Promise<Play> { ): Promise<Play> {
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
@@ -542,12 +617,11 @@ export function cheatRepairOfflineNode(
color: GoColor.empty, color: GoColor.empty,
x, x,
}; };
updateChains(Go.currentGame.board);
Go.currentGame.previousPlayer = GoColor.black;
logger(`Cheat successful. The point ${x},${y} was repaired.`); logger(`Cheat successful. The point ${x},${y} was repaired.`);
}, },
successRngOverride, successRngOverride,
ejectRngOverride, ejectRngOverride,
playAsWhite,
); );
} }
@@ -557,16 +631,16 @@ export function cheatDestroyNode(
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
playAsWhite = false,
): Promise<Play> { ): Promise<Play> {
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {
Go.currentGame.board[x][y] = null; Go.currentGame.board[x][y] = null;
updateChains(Go.currentGame.board);
Go.currentGame.previousPlayer = GoColor.black;
logger(`Cheat successful. The point ${x},${y} was destroyed.`); logger(`Cheat successful. The point ${x},${y} was destroyed.`);
}, },
successRngOverride, successRngOverride,
ejectRngOverride, ejectRngOverride,
playAsWhite,
); );
} }
+7 -27
View File
@@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard"; import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch"; import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { makeAIMove, resetAI, resolveCurrentTurn } from "../boardAnalysis/goAI"; import { handleNextTurn, resetAI } from "../boardAnalysis/goAI";
import { GoScoreExplanation } from "./GoScoreExplanation"; import { GoScoreExplanation } from "./GoScoreExplanation";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
@@ -94,48 +94,29 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer); const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
if (didUpdateBoard) { if (didUpdateBoard) {
rerender();
takeAiTurn(boardState).catch((error) => exceptionAlert(error)); takeAiTurn(boardState).catch((error) => exceptionAlert(error));
} }
} }
function passPlayerTurn() { 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) { if (boardState.previousPlayer === null) {
setScoreOpen(true); setScoreOpen(true);
return; return;
} }
passTurn(boardState, boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black);
setTimeout(() => { takeAiTurn(boardState).catch((error) => exceptionAlert(error));
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
}, 100);
} }
async function takeAiTurn(boardState: BoardState) { 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 const move = await handleNextTurn(boardState, false);
if (Go.currentGame.ai === GoOpponent.none) {
Go.currentGame.previousPlayer && resolveCurrentTurn();
return;
}
const move = await makeAIMove(boardState, false);
if (move.type === GoPlayType.pass) { if (move.type === GoPlayType.pass) {
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000); SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
rerender();
return; return;
} }
if (move.type === GoPlayType.gameOver || move.x === null || move.y === null) { if (boardState.previousPlayer === null) {
setScoreOpen(true); setScoreOpen(true);
return;
} }
} }
@@ -152,10 +133,9 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
resetWinstreak(boardState.ai, false); resetWinstreak(boardState.ai, false);
} }
resetAI();
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true); Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true);
resetAI(false); handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
rerender();
resolveCurrentTurn();
} }
function getPriorMove() { function getPriorMove() {
+7 -3
View File
@@ -63,18 +63,22 @@ export const GoHistoryPage = (): React.ReactElement => {
<Table sx={{ display: "table", mb: 1, width: "100%" }}> <Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody> <TableBody>
<TableRow> <TableRow>
<TableCell className={classes.cellNone}>Wins:</TableCell> <TableCell className={classes.cellNone}>
Wins:{faction === GoOpponent.none ? " (Black / White)" : ""}
</TableCell>
<TableCell className={classes.cellNone}> <TableCell className={classes.cellNone}>
{data.wins} / {data.losses + data.wins} {data.wins} / {data.losses + data.wins}
</TableCell> </TableCell>
</TableRow> </TableRow>
<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> <TableCell className={classes.cellNone}>{data.winStreak}</TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}> <TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
Highest winstreak: Highest winstreak{faction === GoOpponent.none ? " for black" : ""}:
</TableCell> </TableCell>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}> <TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{data.highestWinStreak} {data.highestWinStreak}
+1
View File
@@ -270,6 +270,7 @@ const go = {
getLiberties: 16, getLiberties: 16,
getControlledEmptyNodes: 16, getControlledEmptyNodes: 16,
getStats: 0, getStats: 0,
resetStats: 0,
}, },
cheat: { cheat: {
getCheatSuccessChance: 1, getCheatSuccessChance: 1,
+41 -30
View File
@@ -24,9 +24,9 @@ import {
handlePassTurn, handlePassTurn,
makePlayerMove, makePlayerMove,
resetBoardState, resetBoardState,
resetStats,
validateBoardState, validateBoardState,
validateMove, validateMove,
validateTurn,
} from "../Go/effects/netscriptGoImplementation"; } from "../Go/effects/netscriptGoImplementation";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
import { errorMessage } from "../Netscript/ErrorMessages"; import { errorMessage } from "../Netscript/ErrorMessages";
@@ -43,19 +43,20 @@ export function NetscriptGo(): InternalAPI<NSGo> {
return { return {
makeMove: makeMove:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => { (_x, _y, playAsWhite): Promise<Play> => {
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
validateMove(error(ctx), x, y, "makeMove"); validateMove(error(ctx), x, y, "makeMove", { playAsWhite });
return makePlayerMove(logger(ctx), error(ctx), x, y); return makePlayerMove(logger(ctx), error(ctx), x, y, !!playAsWhite);
}, },
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => { passTurn:
validateTurn(error(ctx), "passTurn()"); (ctx: NetscriptContext) =>
return handlePassTurn(logger(ctx)); (playAsWhite): Promise<Play> => {
}, validateMove(error(ctx), -1, -1, "passTurn", { playAsWhite, pass: true });
opponentNextTurn: (ctx: NetscriptContext) => async (_logOpponentMove) => { return handlePassTurn(logger(ctx), !!playAsWhite);
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true; },
return getOpponentNextMove(logOpponentMove, logger(ctx)); opponentNextTurn: (ctx: NetscriptContext) => async (logOpponentMove, playAsWhite) => {
return getOpponentNextMove(logger(ctx), !!logOpponentMove, !!playAsWhite);
}, },
getBoardState: () => () => { getBoardState: () => () => {
return simpleBoardFromBoard(Go.currentGame.board); return simpleBoardFromBoard(Go.currentGame.board);
@@ -79,9 +80,9 @@ export function NetscriptGo(): InternalAPI<NSGo> {
return resetBoardState(logger(ctx), error(ctx), opponent, boardSize); return resetBoardState(logger(ctx), error(ctx), opponent, boardSize);
}, },
analysis: { analysis: {
getValidMoves: (ctx) => (_boardState, _priorBoardState) => { getValidMoves: (ctx) => (_boardState, _priorBoardState, playAsWhite) => {
const State = validateBoardState(error(ctx), _boardState, _priorBoardState); const State = validateBoardState(error(ctx), _boardState, _priorBoardState);
return getValidMoves(State); return getValidMoves(State, !!playAsWhite);
}, },
getChains: (ctx) => (_boardState) => { getChains: (ctx) => (_boardState) => {
const State = validateBoardState(error(ctx), _boardState); const State = validateBoardState(error(ctx), _boardState);
@@ -98,22 +99,27 @@ export function NetscriptGo(): InternalAPI<NSGo> {
getStats: () => () => { getStats: () => () => {
return getStats(); return getStats();
}, },
resetStats:
() =>
(resetAll = false) => {
resetStats(!!resetAll);
},
}, },
cheat: { cheat: {
getCheatSuccessChance: getCheatSuccessChance: (ctx: NetscriptContext) => (_cheatCount, playAsWhite) => {
(ctx: NetscriptContext) =>
(_cheatCount = Go.currentGame.cheatCount) => {
checkCheatApiAccess(error(ctx));
const cheatCount = helpers.number(ctx, "cheatCount", _cheatCount);
return cheatSuccessChance(cheatCount);
},
getCheatCount: (ctx: NetscriptContext) => () => {
checkCheatApiAccess(error(ctx)); 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: removeRouter:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => { (_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
@@ -122,32 +128,35 @@ export function NetscriptGo(): InternalAPI<NSGo> {
requireNonEmptyNode: true, requireNonEmptyNode: true,
repeat: false, repeat: false,
suicide: false, suicide: false,
playAsWhite: playAsWhite,
}); });
return cheatRemoveRouter(logger(ctx), x, y); return cheatRemoveRouter(logger(ctx), error(ctx), x, y, undefined, undefined, !!playAsWhite);
}, },
playTwoMoves: playTwoMoves:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
(_x1, _y1, _x2, _y2): Promise<Play> => { (_x1, _y1, _x2, _y2, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x1 = helpers.number(ctx, "x", _x1); const x1 = helpers.number(ctx, "x", _x1);
const y1 = helpers.number(ctx, "y", _y1); const y1 = helpers.number(ctx, "y", _y1);
validateMove(error(ctx), x1, y1, "playTwoMoves", { validateMove(error(ctx), x1, y1, "playTwoMoves", {
repeat: false, repeat: false,
suicide: false, suicide: false,
playAsWhite,
}); });
const x2 = helpers.number(ctx, "x", _x2); const x2 = helpers.number(ctx, "x", _x2);
const y2 = helpers.number(ctx, "y", _y2); const y2 = helpers.number(ctx, "y", _y2);
validateMove(error(ctx), x2, y2, "playTwoMoves", { validateMove(error(ctx), x2, y2, "playTwoMoves", {
repeat: false, repeat: false,
suicide: 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: repairOfflineNode:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => { (_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
@@ -157,13 +166,14 @@ export function NetscriptGo(): InternalAPI<NSGo> {
onlineNode: false, onlineNode: false,
requireOfflineNode: true, requireOfflineNode: true,
suicide: false, suicide: false,
playAsWhite,
}); });
return cheatRepairOfflineNode(logger(ctx), x, y); return cheatRepairOfflineNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
}, },
destroyNode: destroyNode:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => { (_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
@@ -171,9 +181,10 @@ export function NetscriptGo(): InternalAPI<NSGo> {
repeat: false, repeat: false,
onlineNode: true, onlineNode: true,
suicide: false, suicide: false,
playAsWhite,
}); });
return cheatDestroyNode(logger(ctx), x, y); return cheatDestroyNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
}, },
}, },
}; };
+48 -5
View File
@@ -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 * 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. * (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 * @remarks
* RAM cost: 8 GB * RAM cost: 8 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() ) * (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 * 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> * </pre>
*/ */
getStats(): Partial<Record<GoOpponent, SimpleOpponentStats>>; 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. * 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 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 * @remarks
* RAM cost: 1 GB * RAM cost: 1 GB
* Requires BitNode 14.2 to use * 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. * 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 * @remarks
* RAM cost: 1 GB * RAM cost: 1 GB
* Requires BitNode 14.2 to use * Requires BitNode 14.2 to use
*/ */
getCheatCount(): number; getCheatCount(playAsWhite = false): number;
/** /**
* Attempts to remove an existing router, leaving an empty node behind. * 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 * 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. * 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 * @remarks
* RAM cost: 8 GB * RAM cost: 8 GB
* Requires BitNode 14.2 to use * Requires BitNode 14.2 to use
@@ -4511,6 +4526,7 @@ export interface GoCheat {
removeRouter( removeRouter(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; 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 * 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. * 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 * @remarks
* RAM cost: 8 GB * RAM cost: 8 GB
* Requires BitNode 14.2 to use * Requires BitNode 14.2 to use
@@ -4536,6 +4559,7 @@ export interface GoCheat {
y1: number, y1: number,
x2: number, x2: number,
y2: number, y2: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; 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 * 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. * 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 * @remarks
* RAM cost: 8 GB * RAM cost: 8 GB
* Requires BitNode 14.2 to use * Requires BitNode 14.2 to use
@@ -4559,6 +4587,7 @@ export interface GoCheat {
repairOfflineNode( repairOfflineNode(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; 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 * 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. * 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 * @remarks
* RAM cost: 8 GB * RAM cost: 8 GB
* Requires BitNode 14.2 to use * Requires BitNode 14.2 to use
@@ -4583,6 +4616,7 @@ export interface GoCheat {
destroyNode( destroyNode(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; 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. * 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. * 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 * @remarks
* RAM cost: 4 GB * RAM cost: 4 GB
* *
@@ -4607,6 +4643,7 @@ export interface Go {
makeMove( makeMove(
x: number, x: number,
y: number, y: number,
playAsWhite = false,
): Promise<{ ): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; 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 * 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. * 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 * @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 * @remarks
* RAM cost: 0 GB * RAM cost: 0 GB
* *
*/ */
passTurn(): Promise<{ passTurn(passAsWhite = false): Promise<{
type: "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
y: 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. * 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 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 * @remarks
* RAM cost: 0 GB * 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 * @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"; type: "move" | "pass" | "gameOver";
x: number | null; x: number | null;
y: number | null; y: number | null;
+34 -12
View File
@@ -6,6 +6,7 @@ import {
simpleBoardFromBoard, simpleBoardFromBoard,
updatedBoardFromSimpleBoard, updatedBoardFromSimpleBoard,
} from "../../../src/Go/boardAnalysis/boardAnalysis"; } from "../../../src/Go/boardAnalysis/boardAnalysis";
import { resetAI } from "../../../src/Go/boardAnalysis/goAI";
import { import {
cheatPlayTwoMoves, cheatPlayTwoMoves,
cheatRemoveRouter, cheatRemoveRouter,
@@ -39,6 +40,9 @@ jest.mock("../../../src/ui/GameRoot", () => ({
toPage: () => ({}), toPage: () => ({}),
}, },
})); }));
const errFun = (x) => {
throw x;
};
setPlayer(new PlayerObject()); setPlayer(new PlayerObject());
AddToAllServers(new Server({ hostname: "home" })); AddToAllServers(new Server({ hostname: "home" }));
@@ -48,20 +52,18 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", async () => { it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); resetAI();
const mockError = jest.fn(() => {
throw new Error("Invalid");
});
await makePlayerMove(mockLogger, mockError, 0, 0).catch(() => {}); expect(() => makePlayerMove(jest.fn(), errFun, 0, 0)).toThrow(
"Invalid move: 0 0. That node is already occupied by a piece.",
expect(mockError).toHaveBeenCalledWith("Invalid move: 0 0. That node is already occupied by a piece."); );
}); });
it("should update the board with valid player moves", async () => { it("should update the board with valid player moves", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....."]; const board = ["OXX..", ".....", ".....", ".....", "....."];
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
Go.currentGame = boardState; Go.currentGame = boardState;
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
const mockError = jest.fn(); const mockError = jest.fn();
@@ -75,6 +77,7 @@ describe("Netscript Go API unit tests", () => {
describe("passTurn() tests", () => { describe("passTurn() tests", () => {
it("should handle pass attempts", async () => { it("should handle pass attempts", async () => {
Go.currentGame = getNewBoardState(7); Go.currentGame = getNewBoardState(7);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
const result = await handlePassTurn(mockLogger); const result = await handlePassTurn(mockLogger);
@@ -84,7 +87,7 @@ describe("Netscript Go API unit tests", () => {
}); });
describe("getBoardState() 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 board = ["OXX..", ".....", ".....", ".....", "..###"];
const boardState = boardStateFromSimpleBoard(board); const boardState = boardStateFromSimpleBoard(board);
@@ -100,6 +103,7 @@ describe("Netscript Go API unit tests", () => {
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black); const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black);
boardState.previousBoards = ["OX.........#.....XX...X."]; boardState.previousBoards = ["OX.........#.....XX...X."];
Go.currentGame = boardState; Go.currentGame = boardState;
resetAI();
const result = getGameState(); 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", () => { it("should set the player's board to the requested size and opponent", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"]; const board = ["OXX..", ".....", ".....", ".....", "..###"];
Go.currentGame = boardStateFromSimpleBoard(board); Go.currentGame = boardStateFromSimpleBoard(board);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
const mockError = 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", () => { it("should throw an error if an invalid opponent is requested", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"]; const board = ["OXX..", ".....", ".....", ".....", "..###"];
Go.currentGame = boardStateFromSimpleBoard(board); Go.currentGame = boardStateFromSimpleBoard(board);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
const mockError = 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", () => { it("should throw an error if an invalid size is requested", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"]; const board = ["OXX..", ".....", ".....", ".....", "..###"];
Go.currentGame = boardStateFromSimpleBoard(board); Go.currentGame = boardStateFromSimpleBoard(board);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
const mockError = 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", () => { it("should return all valid and invalid moves on the board", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const result = getValidMoves(); 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", () => { it("should return all valid and invalid moves on the board, if a board is provided", () => {
const currentBoard = [".....", ".....", ".....", ".....", "....."]; const currentBoard = [".....", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
resetAI();
const board = getNewBoardStateFromSimpleBoard( const board = getNewBoardStateFromSimpleBoard(
["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"], ["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", () => { it("should assign an ID to all contiguous chains on the board", () => {
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const result = getChains(); 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", () => { 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"]; const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const result = getLiberties(); 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", () => { 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"]; const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const result = getControlledEmptyNodes(); const result = getControlledEmptyNodes();
@@ -232,6 +244,7 @@ describe("Netscript Go API unit tests", () => {
it("should show the details for the given board, if provided", () => { it("should show the details for the given board, if provided", () => {
const currentBoard = [".....", ".....", ".....", ".....", "....."]; const currentBoard = [".....", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
resetAI();
const board = updatedBoardFromSimpleBoard(["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]); const board = updatedBoardFromSimpleBoard(["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]);
const result = getControlledEmptyNodes(board); const result = getControlledEmptyNodes(board);
@@ -243,6 +256,7 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", () => { it("should handle invalid moves", () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockError = jest.fn(); const mockError = jest.fn();
validateMove(mockError, 0, 0, "playTwoMoves", { validateMove(mockError, 0, 0, "playTwoMoves", {
repeat: false, 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 () => { it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"]; const board = ["OXX..", ".....", ".....", ".....", "....O"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockLogger = jest.fn(); 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(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[4]?.[3]?.color).toEqual(GoColor.black);
expect(Go.currentGame.board[3]?.[4]?.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 () => { it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"]; const board = ["OXX..", ".....", ".....", ".....", "....O"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty); expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty);
expect(Go.currentGame.board[3]?.[4]?.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 () => { it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"]; const board = ["OXX..", ".....", ".....", ".....", "....O"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
Go.currentGame.cheatCount = 1; Go.currentGame.cheatCount = 1;
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(Go.currentGame.previousBoards).toEqual([]); expect(Go.currentGame.previousBoards).toEqual([]);
}); });
@@ -292,6 +309,7 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", () => { it("should handle invalid moves", () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockError = jest.fn(); const mockError = jest.fn();
validateMove(mockError, 1, 0, "removeRouter", { validateMove(mockError, 1, 0, "removeRouter", {
emptyNode: false, emptyNode: false,
@@ -307,6 +325,7 @@ describe("Netscript Go API unit tests", () => {
it("should remove the router if the move is valid", async () => { it("should remove the router if the move is valid", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
await cheatRemoveRouter(mockLogger, 0, 0, 0, 0); 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 () => { it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....O"]; const board = ["OXX..", ".....", ".....", ".....", "....O"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
Go.currentGame.cheatCount = 1; Go.currentGame.cheatCount = 1;
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(Go.currentGame.previousBoards).toEqual([]); expect(Go.currentGame.previousBoards).toEqual([]);
}); });
@@ -330,6 +350,7 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", () => { it("should handle invalid moves", () => {
const board = ["XOO..", ".....", ".....", ".....", "....#"]; const board = ["XOO..", ".....", ".....", ".....", "....#"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockError = jest.fn(); const mockError = jest.fn();
validateMove(mockError, 0, 0, "repairOfflineNode", { validateMove(mockError, 0, 0, "repairOfflineNode", {
emptyNode: false, 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 () => { it("should update the board with the repaired node if the cheat is successful", async () => {
const board = ["OXX..", ".....", ".....", ".....", "....#"]; const board = ["OXX..", ".....", ".....", ".....", "....#"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
resetAI();
const mockLogger = jest.fn(); const mockLogger = jest.fn();
await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0); await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
+1
View File
@@ -19,6 +19,7 @@ describe("Board analysis utility tests", () => {
ai: GoOpponent.Illuminati, ai: GoOpponent.Illuminati,
passCount: 0, passCount: 0,
cheatCount: 0, cheatCount: 0,
cheatCountForWhite: 0,
}); });
expect(result.board?.length).toEqual(5); expect(result.board?.length).toEqual(5);
}); });
@@ -40,7 +40,9 @@ exports[`Check Save File Continuity GoSave continuity 1`] = `
".......", ".......",
], ],
"cheatCount": 0, "cheatCount": 0,
"cheatCountForWhite": 0,
"passCount": 0, "passCount": 0,
"previousBoard": "",
"previousPlayer": "White", "previousPlayer": "White",
}, },
"previousGame": null, "previousGame": null,