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