BITNODE: IPvGO territory control strategy game (#934)

This commit is contained in:
Michael Ficocelli 2023-12-26 11:45:27 -05:00 committed by GitHub
parent c6141f2adf
commit 7ef12a0323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 7833 additions and 17 deletions

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [analysis](./bitburner.go.analysis.md)
## Go.analysis property
Tools to analyze the IPvGO subnet.
**Signature:**
```typescript
analysis: {
getValidMoves(): boolean[][];
getChains(): (number | null)[][];
getLiberties(): number[][];
getControlledEmptyNodes(): string[];
};
```

@ -0,0 +1,55 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [cheat](./bitburner.go.cheat.md)
## Go.cheat property
Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use.
**Signature:**
```typescript
cheat: {
getCheatSuccessChance(): number;
removeRouter(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
playTwoMoves(
x1: number,
y1: number,
x2: number,
x2: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
repairOfflineNode(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
destroyNode(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
};
```

@ -0,0 +1,38 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [getBoardState](./bitburner.go.getboardstate.md)
## Go.getBoardState() method
Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)
For example, a 5x5 board might look like this:
```
[
"XX.O.",
"X..OO",
".XO..",
"XXO.#",
".XO.#",
]
```
Each string represents a vertical column on the board, and each character in the string represents a point.
Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].
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.
**Signature:**
```typescript
getBoardState(): string[];
```
**Returns:**
string\[\]
## Remarks
RAM cost: 4 GB

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [getOpponent](./bitburner.go.getopponent.md)
## Go.getOpponent() method
Returns the name of the opponent faction in the current subnet. "Netburners" \| "Slum Snakes" \| "The Black Hand" \| "Tetrads" \| "Daedalus" \| "Illuminati"
**Signature:**
```typescript
getOpponent(): opponents;
```
**Returns:**
opponents

@ -0,0 +1,39 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [makeMove](./bitburner.go.makemove.md)
## Go.makeMove() method
Make a move on the IPvGO subnet gameboard, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI.
**Signature:**
```typescript
makeMove(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| x | number | |
| y | number | |
**Returns:**
Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;
a promise that contains if your 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
## Remarks
RAM cost: 4 GB

41
markdown/bitburner.go.md Normal file

@ -0,0 +1,41 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md)
## Go interface
IPvGO api
**Signature:**
```typescript
export interface Go
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [analysis](./bitburner.go.analysis.md) | | { getValidMoves(): boolean\[\]\[\]; getChains(): (number \| null)\[\]\[\]; getLiberties(): number\[\]\[\]; getControlledEmptyNodes(): string\[\]; } | Tools to analyze the IPvGO subnet. |
| [cheat](./bitburner.go.cheat.md) | | { getCheatSuccessChance(): number; removeRouter( x: number, y: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; playTwoMoves( x1: number, y1: number, x2: number, x2: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; repairOfflineNode( x: number, y: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; destroyNode( x: number, y: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; } | Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use. |
## Methods
| Method | Description |
| --- | --- |
| [getBoardState()](./bitburner.go.getboardstate.md) | <p>Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)</p><p>For example, a 5x5 board might look like this:</p>
```
[
"XX.O.",
"X..OO",
".XO..",
"XXO.#",
".XO.#",
]
```
<p>Each string represents a vertical column on the board, and each character in the string represents a point.</p><p>Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].</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> |
| [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. "Netburners" \| "Slum Snakes" \| "The Black Hand" \| "Tetrads" \| "Daedalus" \| "Illuminati" |
| [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet gameboard, and await the opponent's response. 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> |
| [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 on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",</p> |

@ -0,0 +1,30 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [passTurn](./bitburner.go.passturn.md)
## Go.passTurn() method
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.
**Signature:**
```typescript
passTurn(): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
```
**Returns:**
Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;
a promise that contains if your 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
## Remarks
RAM cost: 0 GB

@ -0,0 +1,38 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [resetBoardState](./bitburner.go.resetboardstate.md)
## Go.resetBoardState() method
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 on the subnet at this state.
opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",
**Signature:**
```typescript
resetBoardState(
opponent: "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati",
boardSize: 5 | 7 | 9 | 13,
): string[] | undefined;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| opponent | "Netburners" \| "Slum Snakes" \| "The Black Hand" \| "Tetrads" \| "Daedalus" \| "Illuminati" | |
| boardSize | 5 \| 7 \| 9 \| 13 | |
**Returns:**
string\[\] \| undefined
a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details
## Remarks
RAM cost: 0 GB

@ -64,6 +64,7 @@
| [GangOtherInfoObject](./bitburner.gangotherinfoobject.md) | |
| [GangTaskStats](./bitburner.gangtaskstats.md) | Object representing data representing a gang member task. |
| [GangTerritory](./bitburner.gangterritory.md) | |
| [Go](./bitburner.go.md) | IPvGO api |
| [Grafting](./bitburner.grafting.md) | Grafting API |
| [HackingFormulas](./bitburner.hackingformulas.md) | Hacking formulas |
| [HackingMultipliers](./bitburner.hackingmultipliers.md) | Hack related multipliers. |

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [go](./bitburner.ns.go.md)
## NS.go property
Namespace for Go functions.
**Signature:**
```typescript
readonly go: Go;
```
## Remarks
RAM cost: 0 GB

@ -38,6 +38,7 @@ export async function main(ns) {
| [enums](./bitburner.ns.enums.md) | | [NSEnums](./bitburner.nsenums.md) | |
| [formulas](./bitburner.ns.formulas.md) | <code>readonly</code> | [Formulas](./bitburner.formulas.md) | Namespace for formulas functions. |
| [gang](./bitburner.ns.gang.md) | <code>readonly</code> | [Gang](./bitburner.gang.md) | Namespace for gang functions. Contains spoilers. |
| [go](./bitburner.ns.go.md) | <code>readonly</code> | [Go](./bitburner.go.md) | Namespace for Go functions. |
| [grafting](./bitburner.ns.grafting.md) | <code>readonly</code> | [Grafting](./bitburner.grafting.md) | Namespace for grafting functions. Contains spoilers. |
| [hacknet](./bitburner.ns.hacknet.md) | <code>readonly</code> | [Hacknet](./bitburner.hacknet.md) | Namespace for hacknet functions. Some of this API contains spoilers. |
| [infiltration](./bitburner.ns.infiltration.md) | <code>readonly</code> | [Infiltration](./bitburner.infiltration.md) | Namespace for infiltration functions. |

@ -96,6 +96,7 @@ export function PlayerMultipliers(): React.ReactElement {
mult: "Hacking Speed",
current: Player.mults.hacking_speed,
augmented: Player.mults.hacking_speed * mults.hacking_speed,
bnMult: currentNodeMults.HackingSpeedMultiplier,
},
{
mult: "Hacking Money",
@ -220,6 +221,7 @@ export function PlayerMultipliers(): React.ReactElement {
mult: "Company Reputation Gain",
current: Player.mults.company_rep,
augmented: Player.mults.company_rep * mults.company_rep,
bnMult: currentNodeMults.CompanyWorkRepGain,
color: Settings.theme.combat,
},
{
@ -240,6 +242,7 @@ export function PlayerMultipliers(): React.ReactElement {
mult: "Crime Success Chance",
current: Player.mults.crime_success,
augmented: Player.mults.crime_success * mults.crime_success,
bnMult: currentNodeMults.CrimeSuccessRate,
color: Settings.theme.combat,
},
{

@ -445,6 +445,41 @@ export function initBitNodes() {
</>
),
);
BitNodes.BitNode14 = new BitNode(
14,
1,
"IPvGO Subnet Takeover",
"Territory exists only in the 'net",
(
<>
In late 2070, the .org bubble burst, and most of the newly-implemented IPvGO 'net collapsed overnight. Since
then, various factions have been fighting over small subnets to control their computational power. These subnets
are very valuable in the right hands, if you can wrest them from their current owners. You will be opposed by
the other factions, but you can overcome them with careful choices. Prevent their attempts to destroy your
networks by controlling the open space in the 'net!
<br />
<br />
Destroying this BitNode will give you Source-File 14, or if you already have this Source-File it will upgrade
its level up to a maximum of 3. This Source-File grants the following benefits:
<br />
<br />
Level 1: 25% increased stat multipliers from Node Power
<br />
Level 2: Permanently unlocks the go.cheat API
<br />
Level 3: 25% increased success rate for the go.cheat API
<br />
<br />
This Source-File also increases the maximum favor you can gain for each faction from IPvGO by:
<br />
Level 1: +10
<br />
Level 2: +20
<br />
Level 3: +40
</>
),
);
}
export const defaultMultipliers = new BitNodeMultipliers();
@ -909,6 +944,49 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
WorldDaemonDifficulty: 3,
});
}
case 14: {
return new BitNodeMultipliers({
GoPower: 4,
HackingLevelMultiplier: 0.4,
HackingSpeedMultiplier: 0.3,
ServerMaxMoney: 0.7,
ServerStartingMoney: 0.5,
ServerStartingSecurity: 1.5,
CrimeMoney: 0.75,
CrimeSuccessRate: 0.4,
HacknetNodeMoney: 0.25,
ScriptHackMoney: 0.3,
StrengthLevelMultiplier: 0.5,
DexterityLevelMultiplier: 0.5,
AgilityLevelMultiplier: 0.5,
AugmentationMoneyCost: 1.5,
InfiltrationMoney: 0.75,
FactionWorkRepGain: 0.2,
CompanyWorkRepGain: 0.2,
CorporationValuation: 0.4,
CorporationSoftcap: 0.9,
CorporationDivisions: 0.8,
BladeburnerRank: 0.6,
BladeburnerSkillCost: 2,
GangSoftcap: 0.7,
GangUniqueAugs: 0.4,
StaneksGiftPowerMultiplier: 0.5,
StaneksGiftExtraSize: -1,
WorldDaemonDifficulty: 5,
});
}
default: {
throw new Error("Invalid BitNodeN");
}

@ -36,6 +36,9 @@ export class BitNodeMultipliers {
/** Influences how much money the player earns when completing working their job. */
CompanyWorkMoney = 1;
/** Influences how much rep the player gains when performing work for a company. */
CompanyWorkRepGain = 1;
/** Influences the valuation of corporations created by the player. */
CorporationValuation = 1;
@ -45,6 +48,9 @@ export class BitNodeMultipliers {
/** Influences the base money gained when the player commits a crime. */
CrimeMoney = 1;
/** influences the success chance of committing crimes */
CrimeSuccessRate = 1;
/** Influences how many Augmentations you need in order to get invited to the Daedalus faction */
DaedalusAugsRequirement = 30;
@ -75,12 +81,18 @@ export class BitNodeMultipliers {
/** Percentage of unique augs that the gang has. */
GangUniqueAugs = 1;
/** Percentage multiplier on the effect of the IPvGO rewards **/
GoPower = 1;
/** Influences the experienced gained when hacking a server. */
HackExpGain = 1;
/** Influences how quickly the player's hacking level (not experience) scales */
HackingLevelMultiplier = 1;
/** Influences how quickly the player's hack(), grow() and weaken() calls run */
HackingSpeedMultiplier = 1;
/**
* Influences how much money is produced by Hacknet Nodes.
* Influences the hash rate of Hacknet Servers (unlocked in BitNode-9)

@ -58,6 +58,7 @@ export const BitNodeMultipliersDisplay = ({ n, level }: IProps): React.ReactElem
<CorporationMults n={n} mults={mults} />
<BladeburnerMults n={n} mults={mults} />
<StanekMults n={n} mults={mults} />
<GoMults n={n} mults={mults} />
</Box>
);
};
@ -351,3 +352,11 @@ function CorporationMults({ mults }: IMultsProps): React.ReactElement {
return <BNMultTable sectionName="Corporation" rowData={rows} mults={mults} />;
}
function GoMults({ mults }: IMultsProps): React.ReactElement {
const rows: IBNMultRows = {
GoPower: { name: "IPvGO Node Power bonus" },
};
return <BNMultTable sectionName="IPvGO Subnet Takeover" rowData={rows} mults={mults} />;
}

@ -241,7 +241,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | / __| \ | | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | O | | O / | O | | O | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | |_/ |/ | \_ \_| | | | | </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | | O | | O__/ | / \__ | | O | | | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | | <BitNodePortal n={14} level={n(4)} flume={props.flume} destroyedBitNode={destroyed} /> | | O__/ | / \__ | | O | | | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | / /| O / \| | | | | | | </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>O | | | \| | O / _/ | / O | |/ | | | O</Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | | |O / | | O / | O O | | \ O| | | |</Typography>

@ -5,6 +5,7 @@ import { WorkerScript } from "../Netscript/WorkerScript";
import { CrimeType } from "@enums";
import { CrimeWork } from "../Work/CrimeWork";
import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
interface IConstructorParams {
hacking_success_weight?: number;
@ -128,6 +129,7 @@ export class Crime {
chance /= CONSTANTS.MaxSkillLevel;
chance /= this.difficulty;
chance *= p.mults.crime_success;
chance *= currentNodeMults.CrimeSuccessRate;
chance *= calculateIntelligenceBonus(p.skills.intelligence, 1);
return Math.min(chance, 1);

@ -26,6 +26,8 @@
## Advanced Mechanics
- [Hacking Algorithms](programming/hackingalgorithms.md)
- [IPvGO](programming/go_algorithms.md)
- [BitNodes](advanced/bitnodes.md)
- [BladeBurners](advanced/bladeburners.md)
- [Corporations](advanced/corporations.md)

@ -0,0 +1,303 @@
## Automating IPvGO
IPvGO is a strategic territory control minigame accessible from DefComm in New Tokyo, or the CIA in Sector-12. Form networks of routers on a grid to control open space and gain stat multipliers and favor, but make sure the opposing faction does not surround and destroy your network!
For basic instructions, go to DefComm or CIA to access the current subnet, and look through the "How to Play" section. This document is specifically focused on building scripts to automate subnet takeover, which will be more applicable you have played a few subnets.
&nbsp;
#### Overview
The rules of Go, at their heart, are very simple. Because of this, they can be used to make a very simple script to automatically collect node power from IPvGO subnets.
This script can be iteratively improved upon, gradually giving it more tools and types of move to look for, and becoming more consistent at staking out territory on the current subnet.
This document starts out with a lot of detail and example code to get you started, but will transition to more high-level algorithm design and pseudocode as it progresses. If you get stuck implementing some of these ideas, feel free to discuss in the [official Discord server](https://discord.gg/TFc3hKD)
&nbsp;
### Script outline: Starting with the basics
&nbsp;
#### Testing Validity
The `ns.go` API provides a number of useful tools to understand the current subnet state.
`ns.go.analysis.getValidMoves()` returns a 2D array of true/false, indicating if you can place a router on the current square.
You can test if a given move `x,y` is valid with a test like this:
```js
const validMoves = ns.go.analysis.getValidMoves();
if (validMoves[x][y] === true) {
// Do something
}
```
&nbsp;
#### Choosing a random move
The simplest move type, and the fallback if no other move can be found, is to pick a random valid move.
The `validMoves` grid can be retrieved using `getValidMoves()` as mentioned above. `board` comes from `ns.go.getBoardState()`, more on that later.
It would be a problem to fill in every single node on the board, however. If networks ever lose contact with any empty nodes, they will be destroyed! So, leave some "airspace" by excluding certain moves from the random ones we select.
One way to do this is to exclude nodes with both even X coordinate and even y coordinate:
```js
/**
* Choose one of the empty points on the board at random to play
*/
const getRandomMove = (board, validMoves) => {
const moveOptions = [];
const size = board[0].length;
// Look through all the points on the board
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
// Make sure the point is a valid move
const isValidMove = validMoves[x][y] === true;
// Leave some spaces to make it harder to capture our pieces.
// We don't want to run out of empty node connections!
const isNotReservedSpace = x % 2 === 1 || y % 2 === 1;
if (isValidMove && isNotReservedSpace) {
moveOptions.push([x, y]);
}
}
}
// Choose one of the found moves at random
const randomIndex = Math.floor(Math.random() * moveOptions.length);
return moveOptions[randomIndex] ?? [];
};
```
This idea can also be improved to focus on a specific area or corner first, rather than spread across the whole board right away.
&nbsp;
#### Playing moves
Now that a simple move type is available, it can be used to play on the current subnet!
`await ns.go.makeMove(x, y)` can be used to play a chosen move `x,y`. Note that it returns a `Promise`, meaning it needs to be used with `await`.
`await ns.go.passTurn()` can be used if no moves are found. This will end the game if the AI also passes (or just passed previously).
Both `makeMove()` and `passTurn()` , when awaited, return an object that tells you if your move was valid and successfully played, and what the AI's response is.
```js
{
// If your move was successfully applied to the subnet
success: boolean;
// If the opponent moved or passed, or if the game is now over, or if your move was invalid
type: "invalid" | "move" | "pass" | "gameOver";
x: number; // Opponent move's x coord (if applicable)
y: number; // Opponent move's y coord (if applicable)
}
```
&nbsp;
When used together with the `getRandomMove()` implemented above, the framework of the script is ready. An example `main()` that implements this is below. Search for a new subnet using the UI, then launch the script you have been working on, and watch it play!
```js
/** @param {NS} ns */
export async function main(ns) {
let result, x, y;
do {
const board = ns.go.getBoardState();
const validMoves = ns.go.analysis.getValidMoves();
const [randX, randY] = getRandomMove(board, validMoves);
// TODO: more move options
// Choose a move from our options (currently just "random move")
x = randX;
y = randY;
if (x === undefined) {
// Pass turn if no moves are found
result = await ns.go.passTurn();
} else {
// Play the selected move
result = await ns.go.makeMove(x, y);
}
await ns.sleep(200);
// Keep looping as long as the opponent is playing moves
} while (result?.type !== "gameOver");
// After the opponent passes, end the game by passing as well
await ns.go.passTurn();
// TODO: add a loop to keep playing
// TODO: reset board, e.g. `ns.go.resetBoardState("Netburners", 7)`
}
```
&nbsp;
### Adding network expansion moves
Just playing random moves is not very effective, though. The next step is to use the board state to try and take over territory.
`ns.go.getBoardState()` returns a simple grid representing what the current board looks like. The player's routers are marked with `X`, and the opponents with `O`.
Example 5x5 board state, with a number of networks for each player:
```angularjs
[ "XX.O.",
"X..OO",
".XO..",
"XXO..",
".XOO.", ]
```
The board state can be used to look at all the nodes touching a given point, by looking at an adjacent pair of coordinates.
For example, the point to the 'north' of the current point `x, y` can be retrieved with `board[x + 1]?.[y]`. If it is a friendly router it will have value `"X"`. (It will be undefined if `x,y` is on the north edge of the subnet)
That info can be used to make decisions about where to place routers.
In order to expand the area that is controlled by the player's networks, connecting to friendly routers (when possible) is a strong move. This can be done with a very similar implementation to `getRandomMove()`, with the additional check of looking for a neighboring friendly router. For each point on the board:
```
Detect expansion moves:
For each point on the board:
* If the empty point is a valid move, and
* If the point is not an open space reserved to protect the network [see getRandomMove()], and
* If a point to the north, south, east, or west is a friendly router
Then, the move will expand an existing network
```
When possible, an expansion move like this should be used over a random move. When neither can be found, pass turn.
This idea can be improved: reserved spaces can be skipped if the nodes are in different networks. Se `ns.go.analysis.getChains()`
After implementing this, the script will consistently get points on the subnet against most opponents (at least on the larger boards), and will sometimes even get lucky and win against the easiest factions.
&nbsp;
#### Next Steps
There is a lot we can still do to improve the script. For one, it currently only plays one game, and must be restarted each time! Also, it does not re-set the subnet upon game completion yet.
In addition, the script only knows about a few types of moves, and does not yet know how to capture or defend networks.
&nbsp;
#### Killing duplicate scripts
Because there is only one subnet active at any time, you do not want multiple copies of your scripts running and competing with each other. It may be helpful to kill any other scripts with the same name as your IPvGO script on startup. This can be done using `ns.getRunningScript()` to get the script details and `ns.kill()` to remove old copies of the script.
&nbsp;
### Move option: Capturing the opponent's networks
If the opposing faction's network is down to its last open port, placing a router in that empty node will capture and destroy that entire network.
To find out what networks are in danger of capture, `ns.go.analysis.getLiberties()` shows how many empty nodes / open ports each network has. As with `getBoardState()` and `getValidMoves()` , the number of liberties (open ports) for a given point's network can be retrieved via its coordinates `[x][y]` on the grid returned by `getLiberties()`
```
Detect moves to capture the opponent's routers:
For each point on the board:
* If the empty point is a valid move, and
* If a point to the north, south, east, or west is a router with exactly 1 liberty [via its coordinates in getLiberties()], and
* That point is controlled by the opponent [it is a "O" via getBoardState()]
Then, playing that move will capture the opponent's network.
```
&nbsp;
### Move option: Defending your networks from capture
`getLiberties()` can also be used to detect your own networks that are in danger of being captured, and look for moves to try and save it.
```
Detect moves to defend a threatened network:
For each point on the board:
* If the empty point is a valid move, and
* If a point to the north, south, east, or west is a router with exactly 1 liberty [via its coordinates in getLiberties()], and
* That point is controlled by the player [it is a "X" via getBoardState()]
Then, that network is in danger of being captured.
To detect if that network can be saved:
* Ensure the new move will not immediately allow the opponent to capture:
* That empty point ALSO has two or more empty points adjacent to it [a "." via getBoardState()], OR
* That empty point has a friendly network adjacent to it, and that network has 3 or more liberties [via getLiberties()]
Then, playing that move will prevent your network from being captured (at least for a turn or two)
```
&nbsp;
### Move option: Smothering the opponent's networks
In some cases, an opponent's network cannot YET be captured, but by placing routers all around it, the network can be captured on a future move. (Or at least you force the opponent to spend moves defending their network.)
There are many ways to approach this, but the simplest is to look for any opposing network with the fewest liberties remaining (ideally 2), and find a safe point to place a router that touches it.
To make sure the move will not immediately get re-captured, make sure the point you play on has two adjacent empty nodes, or is touching a friendly network with three+ liberties. (This is the same as the check in the move to defend a friendly chain.)
&nbsp;
### Move option: Expanding your networks' connections to empty nodes
The more empty nodes a network touches, the stronger it is, and the more territory it influences. Thus, placing routers that touch a friendly network and also to as many open nodes as possible is often a strong move.
This is similar to the logic for defending your networks from immediate capture. Look for a friendly network with the fewest open ports, and find an empty node adjacent to it that touches multiple other empty nodes.
&nbsp;
### Move option: Encircling space to control empty nodes
&nbsp;
### Choosing a good move option
Having multiple plausible moves to select from is helpful, but choosing the right option is important to making a strong Go script. In some cases, if a move type is available, it is almost always worth playing (such as defending your network from immediate capture, or capturing a vulnerable enemy network)
Each of the IPvGO factions has a few moves they will almost always choose (The Black hand will always capture if possible, for example). Coming up with a simple prioritized list is a good start to compete with these scripts. Experiment to see what works best!
This idea can be improved, however, by including information such as the size of the network that is being threatened or that is vulnerable to capture. It is probably worth giving up one router in exchange for capturing a large enemy network, for example. Adding two new open ports to a large network is helpful, but limiting an opponent's network to one open port might be better.
&nbsp;
### Other types of move options
**Preparing to invade the opponent**
Empty areas that are completely surrounded and controlled by a single player can be seen via `ns.go.analysis.getControlledEmptyNodes()`. However, just because the area is currently controlled by the opponent does not mean it cannot be attacked! Start by surrounding an opponent's network from the outside, then it can be captured by attacking the space it surrounds and controls. (Note that this only works on networks that have a single interior empty space: if they have multiple inner empty points, the suicide rule prevents you from filling any of them)
**Wrapping empty space**
The starting script uses some very simple logic to leave open empty nodes inside its networks (simply excluding points with `x % 2 === 0 && y % 2 === 0`). However, it is very strong to look for ways to actively surround empty space.
Look for moves that connect a network to the edge of a board that touch an empty node, or look for moves that connect two networks and touch an empty node. Or, look for a move that touches a friendly network and splits apart a chain of empty nodes.
**Jumps and Knights' moves**
The factions currently only look at moves directly connected to friendly or enemy networks in most cases. however, especially on the larger board, playing a router a few spaces away from an existing line/network allows the player to influence more territory, compared to slower moves that connect one adjacent node at a time. Consider skipping a node or two, or playing diagonally, or combining them to make L shaped jumps (like a knight's move in chess)
**Pattern Matching**
There are a lot of strong shapes in Go, that are worth attempting to re-create. The factions look for ways to slip diagonally between the players' networks and cut them apart. They also look for ways to wrap around isolated opposing routers. Consider making a small library of strong shapes, then looking to match them on the board (or their rotations or mirrors). The exact shapes will require some research into Go, but there is a lot of good documentation online about this idea.
**Creating "Eyes"**
If a single network fully encloses two different disconnected empty nodes, it can never be taken. (If it only had one inner airspace, the opponent could eventually surround and then fill it to capture the network. If there is two, however, the suicide rule prevents them from filling either inner empty space.) Detecting moves that make figure-8 type shapes, or split an encircled empty node chain into two smaller ones, are very strong.
In addition, if the opponent has only a single such move, playing there first to block it is often extremely disruptive, and can even lead to their network being captured.

@ -36,9 +36,10 @@ import file33 from "!!raw-loader!./doc/index.md";
import file34 from "!!raw-loader!./doc/migrations/v1.md";
import file35 from "!!raw-loader!./doc/migrations/v2.md";
import file36 from "!!raw-loader!./doc/programming/game_frozen.md";
import file37 from "!!raw-loader!./doc/programming/hackingalgorithms.md";
import file38 from "!!raw-loader!./doc/programming/learn.md";
import file39 from "!!raw-loader!./doc/programming/remote_api.md";
import file37 from "!!raw-loader!./doc/programming/go_algorithms.md";
import file38 from "!!raw-loader!./doc/programming/hackingalgorithms.md";
import file39 from "!!raw-loader!./doc/programming/learn.md";
import file40 from "!!raw-loader!./doc/programming/remote_api.md";
interface Document {
default: string;
@ -81,6 +82,7 @@ AllPages["index.md"] = file33;
AllPages["migrations/v1.md"] = file34;
AllPages["migrations/v2.md"] = file35;
AllPages["programming/game_frozen.md"] = file36;
AllPages["programming/hackingalgorithms.md"] = file37;
AllPages["programming/learn.md"] = file38;
AllPages["programming/remote_api.md"] = file39;
AllPages["programming/go_algorithms.md"] = file37;
AllPages["programming/hackingalgorithms.md"] = file38;
AllPages["programming/learn.md"] = file39;
AllPages["programming/remote_api.md"] = file40;

36
src/Go/GoRoot.tsx Normal file

@ -0,0 +1,36 @@
import { Container, Tab, Tabs } from "@mui/material";
import React from "react";
import { GoInstructionsPage } from "./ui/GoInstructionsPage";
import { BorderInnerSharp, Help, ManageSearch, History } from "@mui/icons-material";
import { GoStatusPage } from "./ui/GoStatusPage";
import { GoHistoryPage } from "./ui/GoHistoryPage";
import { GoGameboardWrapper } from "./ui/GoGameboardWrapper";
import { boardStyles } from "./boardState/goStyles";
export function GoRoot(): React.ReactElement {
const classes = boardStyles();
const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void {
setValue(tab);
}
function showInstructions() {
setValue(3);
}
return (
<Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
<Tab label="IPvGO Subnet" icon={<BorderInnerSharp />} iconPosition={"start"} className={classes.tab} />
<Tab label="Status" icon={<ManageSearch />} iconPosition={"start"} className={classes.tab} />
<Tab label="History" icon={<History />} iconPosition={"start"} className={classes.tab} />
<Tab label="How to Play" icon={<Help />} iconPosition={"start"} className={classes.tab} />
</Tabs>
{value === 0 && <GoGameboardWrapper showInstructions={showInstructions} />}
{value === 1 && <GoStatusPage />}
{value === 2 && <GoHistoryPage />}
{value === 3 && <GoInstructionsPage />}
</Container>
);
}

@ -0,0 +1,683 @@
import {
Board,
BoardState,
Neighbor,
opponents,
PlayerColor,
playerColors,
PointState,
validityReason,
} from "../boardState/goConstants";
import {
findAdjacentPointsInChain,
findNeighbors,
getArrayFromNeighbor,
getBoardCopy,
getEmptySpaces,
getNewBoardState,
getStateCopy,
isDefined,
isNotNull,
updateCaptures,
updateChains,
} from "../boardState/boardState";
/**
* Determines if the given player can legally make a move at the specified coordinates.
*
* You cannot repeat previous board states, to prevent endless loops (superko rule)
*
* You cannot make a move that would remove all liberties of your own piece(s) unless it captures opponent's pieces
*
* You cannot make a move in an occupied space
*
* You cannot make a move if it is not your turn, or if the game is over
*
* @returns a validity explanation for if the move is legal or not
*/
export function evaluateIfMoveIsValid(
boardState: BoardState,
x: number,
y: number,
player: PlayerColor,
shortcut = true,
) {
const point = boardState.board?.[x]?.[y];
if (boardState.previousPlayer === null) {
return validityReason.gameOver;
}
if (boardState.previousPlayer === player) {
return validityReason.notYourTurn;
}
if (!point) {
return validityReason.pointBroken;
}
if (point.player !== playerColors.empty) {
return validityReason.pointNotEmpty;
}
// Detect if the current player has ever previously played this move. Used to detect potential repeated board states
const moveHasBeenPlayedBefore = !!boardState.history.find((board) => board[x]?.[y]?.player === player);
if (shortcut) {
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
const liberties = findAdjacentLibertiesForPoint(boardState, x, y);
const hasLiberty = liberties.north || liberties.east || liberties.south || liberties.west;
if (!moveHasBeenPlayedBefore && hasLiberty) {
return validityReason.valid;
}
// If a connected friendly chain has more than one liberty, the move is not suicide. If the move is not repeated, it is legal
const neighborChainLibertyCount = findMaxLibertyCountOfAdjacentChains(boardState, x, y, player);
if (!moveHasBeenPlayedBefore && neighborChainLibertyCount > 1) {
return validityReason.valid;
}
// If there is any neighboring enemy chain with only one liberty, and the move is not repeated, it is valid,
// because it would capture the enemy chain and free up some liberties for itself
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
boardState,
x,
y,
player === playerColors.black ? playerColors.white : playerColors.black,
);
if (!moveHasBeenPlayedBefore && potentialCaptureChainLibertyCount < 2) {
return validityReason.valid;
}
// If there is no direct liberties for the move, no captures, and no neighboring friendly chains with multiple liberties,
// the move is not valid because it would suicide the piece
if (!hasLiberty && potentialCaptureChainLibertyCount >= 2 && neighborChainLibertyCount <= 1) {
return validityReason.noSuicide;
}
}
// If the move has been played before and is not obviously illegal, we have to actually play it out to determine
// if it is a repeated move, or if it is a valid move
const evaluationBoard = evaluateMoveResult(boardState, x, y, player, true);
if (evaluationBoard.board[x]?.[y]?.player !== player) {
return validityReason.noSuicide;
}
if (moveHasBeenPlayedBefore && checkIfBoardStateIsRepeated(evaluationBoard)) {
return validityReason.boardRepeated;
}
return validityReason.valid;
}
/**
* Create a new evaluation board and play out the results of the given move on the new board
*/
export function evaluateMoveResult(
initialBoardState: BoardState,
x: number,
y: number,
player: playerColors,
resetChains = false,
) {
const boardState = getStateCopy(initialBoardState);
boardState.history.push(getBoardCopy(boardState).board);
const point = boardState.board[x]?.[y];
if (!point) {
return initialBoardState;
}
point.player = player;
boardState.previousPlayer = player;
const neighbors = getArrayFromNeighbor(findNeighbors(boardState, x, y));
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
resetChainsById(boardState, chainIdsToUpdate);
return updateCaptures(boardState, player, resetChains);
}
export function getControlledSpace(boardState: BoardState) {
const chains = getAllChains(boardState);
const length = boardState.board[0].length;
const whiteControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.white, length * 2)
.map((eye) => eye.chain)
.flat();
const blackControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.black, length * 2)
.map((eye) => eye.chain)
.flat();
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => playerColors.empty));
whiteControlledEmptyNodes.forEach((node) => {
ownedPointGrid[node.x][node.y] = playerColors.white;
});
blackControlledEmptyNodes.forEach((node) => {
ownedPointGrid[node.x][node.y] = playerColors.black;
});
return ownedPointGrid;
}
/**
Clear the chain and liberty data of all points in the given chains
*/
const resetChainsById = (boardState: BoardState, chainIds: string[]) => {
const pointsToUpdate = boardState.board
.flat()
.filter(isDefined)
.filter(isNotNull)
.filter((point) => chainIds.includes(point.chain));
pointsToUpdate.forEach((point) => {
point.chain = "";
point.liberties = [];
});
};
/**
* For a potential move, determine what the liberty of the point would be if played, by looking at adjacent empty nodes
* as well as the remaining liberties of neighboring friendly chains
*/
export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
.filter(isNotNull)
.filter(isDefined);
// Get all chains that the new move will connect to
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.player === player);
const allyNeighborChainLiberties = allyNeighbors
.map((neighbor) => {
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
return chain?.[0]?.liberties ?? null;
})
.flat()
.filter(isNotNull);
// Get all empty spaces that the new move connects to that aren't already part of friendly liberties
const directLiberties = neighborPoints.filter((neighbor) => neighbor.player === playerColors.empty);
const allLiberties = [...directLiberties, ...allyNeighborChainLiberties];
// filter out duplicates, and starting point
return allLiberties
.filter(
(liberty, index) =>
allLiberties.findIndex((neighbor) => liberty.x === neighbor.x && liberty.y === neighbor.y) === index,
)
.filter((liberty) => liberty.x !== x || liberty.y !== y);
}
/**
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the maximum
*/
export function findMaxLibertyCountOfAdjacentChains(
boardState: BoardState,
x: number,
y: number,
player: playerColors,
) {
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
.filter(isNotNull)
.filter(isDefined)
.filter((neighbor) => neighbor.player === player);
return friendlyNeighbors.reduce((max, neighbor) => Math.max(max, neighbor?.liberties?.length ?? 0), 0);
}
/**
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the minimum
*/
export function findMinLibertyCountOfAdjacentChains(
boardState: BoardState,
x: number,
y: number,
player: playerColors,
) {
const chain = findEnemyNeighborChainWithFewestLiberties(boardState, x, y, player);
return chain?.[0]?.liberties?.length ?? 99;
}
export function findEnemyNeighborChainWithFewestLiberties(
boardState: BoardState,
x: number,
y: number,
player: playerColors,
) {
const chains = getAllChains(boardState);
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
.filter(isNotNull)
.filter(isDefined)
.filter((neighbor) => neighbor.player === player);
const minimumLiberties = friendlyNeighbors.reduce(
(min, neighbor) => Math.min(min, neighbor?.liberties?.length ?? 0),
friendlyNeighbors?.[0]?.liberties?.length ?? 99,
);
const chainId = friendlyNeighbors.find((neighbor) => neighbor?.liberties?.length === minimumLiberties)?.chain;
return chains.find((chain) => chain[0].chain === chainId);
}
/**
* Returns a list of points that are valid moves for the given player
*/
export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
return getEmptySpaces(boardState).filter(
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === validityReason.valid,
);
}
/**
Find all empty point groups where either:
* all of its immediate surrounding player-controlled points are in the same continuous chain, or
* it is completely surrounded by some single larger chain and the edge of the board
Eyes are important, because a chain of pieces cannot be captured if it fully surrounds two or more eyes.
*/
export function getAllEyesByChainId(boardState: BoardState, player: playerColors) {
const allChains = getAllChains(boardState);
const eyeCandidates = getAllPotentialEyes(boardState, allChains, player);
const eyes: { [s: string]: PointState[][] } = {};
eyeCandidates.forEach((candidate) => {
if (candidate.neighbors.length === 0) {
return;
}
// If only one chain surrounds the empty space, it is a true eye
if (candidate.neighbors.length === 1) {
const neighborChainID = candidate.neighbors[0][0].chain;
eyes[neighborChainID] = eyes[neighborChainID] || [];
eyes[neighborChainID].push(candidate.chain);
return;
}
// If any chain fully encircles the empty space (even if there are other chains encircled as well), the eye is true
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
boardState,
candidate.chain,
candidate.neighbors,
allChains,
);
neighborsEncirclingEye.forEach((neighborChain) => {
const neighborChainID = neighborChain[0].chain;
eyes[neighborChainID] = eyes[neighborChainID] || [];
eyes[neighborChainID].push(candidate.chain);
});
});
return eyes;
}
/**
* Get a list of all eyes, grouped by the chain they are adjacent to
*/
export function getAllEyes(boardState: BoardState, player: playerColors, eyesObject?: { [s: string]: PointState[][] }) {
const eyes = eyesObject ?? getAllEyesByChainId(boardState, player);
return Object.keys(eyes).map((key) => eyes[key]);
}
/**
Find all empty spaces completely surrounded by a single player color.
For each player chain number, add any empty space chains that are completely surrounded by a single player's color to
an array at that chain number's index.
*/
export function getAllPotentialEyes(
boardState: BoardState,
allChains: PointState[][],
player: playerColors,
_maxSize?: number,
) {
const nodeCount = boardState.board.map((row) => row.filter((p) => p)).flat().length;
const maxSize = _maxSize ?? Math.min(nodeCount * 0.4, 11);
const emptyPointChains = allChains.filter((chain) => chain[0].player === playerColors.empty);
const eyeCandidates: { neighbors: PointState[][]; chain: PointState[]; id: string }[] = [];
emptyPointChains
.filter((chain) => chain.length <= maxSize)
.forEach((chain) => {
const neighboringChains = getAllNeighboringChains(boardState, chain, allChains);
const hasWhitePieceNeighbor = neighboringChains.find(
(neighborChain) => neighborChain[0]?.player === playerColors.white,
);
const hasBlackPieceNeighbor = neighboringChains.find(
(neighborChain) => neighborChain[0]?.player === playerColors.black,
);
// Record the neighbor chains of the eye candidate empty chain, if all of its neighbors are the same color piece
if (
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === playerColors.white) ||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === playerColors.black)
) {
eyeCandidates.push({
neighbors: neighboringChains,
chain: chain,
id: chain[0].chain,
});
}
});
return eyeCandidates;
}
/**
* For each chain bordering an eye candidate:
* remove all other neighboring chains. (replace with empty points)
* check if the eye candidate is a simple true eye now
* If so, the original candidate is a true eye.
*/
function findNeighboringChainsThatFullyEncircleEmptySpace(
boardState: BoardState,
candidateChain: PointState[],
neighborChainList: PointState[][],
allChains: PointState[][],
) {
const boardMax = boardState.board[0].length - 1;
const candidateSpread = findFurthestPointsOfChain(candidateChain);
return neighborChainList.filter((neighborChain, index) => {
// If the chain does not go far enough to surround the eye in question, don't bother building an eval board
const neighborSpread = findFurthestPointsOfChain(neighborChain);
const couldWrapNorth =
neighborSpread.north > candidateSpread.north ||
(candidateSpread.north === boardMax && neighborSpread.north === boardMax);
const couldWrapEast =
neighborSpread.east > candidateSpread.east ||
(candidateSpread.east === boardMax && neighborSpread.east === boardMax);
const couldWrapSouth =
neighborSpread.south < candidateSpread.south || (candidateSpread.south === 0 && neighborSpread.south === 0);
const couldWrapWest =
neighborSpread.west < candidateSpread.west || (candidateSpread.west === 0 && neighborSpread.west === 0);
if (!couldWrapNorth || !couldWrapEast || !couldWrapSouth || !couldWrapWest) {
return false;
}
const evaluationBoard = getStateCopy(boardState);
const examplePoint = candidateChain[0];
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
.flat()
.filter(isNotNull)
.filter(isDefined);
otherChainNeighborPoints.forEach((point) => {
const pointToEdit = evaluationBoard.board[point.x]?.[point.y];
if (pointToEdit) {
pointToEdit.player = playerColors.empty;
}
});
const updatedBoard = updateChains(evaluationBoard);
const newChains = getAllChains(updatedBoard);
const newChainID = updatedBoard.board[examplePoint.x]?.[examplePoint.y]?.chain;
const chain = newChains.find((chain) => chain[0].chain === newChainID) || [];
const newNeighborChains = getAllNeighboringChains(boardState, chain, allChains);
return newNeighborChains.length === 1;
});
}
/**
* Determine the furthest that a chain extends in each of the cardinal directions
*/
function findFurthestPointsOfChain(chain: PointState[]) {
return chain.reduce(
(directions, point) => {
if (point.y > directions.north) {
directions.north = point.y;
}
if (point.y < directions.south) {
directions.south = point.y;
}
if (point.x > directions.east) {
directions.east = point.x;
}
if (point.x < directions.west) {
directions.west = point.x;
}
return directions;
},
{
north: chain[0].y,
east: chain[0].x,
south: chain[0].y,
west: chain[0].x,
},
);
}
/**
* Removes an element from an array at the given index
*/
function removePointAtIndex(arr: PointState[][], index: number) {
const newArr = [...arr];
newArr.splice(index, 1);
return newArr;
}
/**
* Get all player chains that are adjacent / touching the current chain
*/
export function getAllNeighboringChains(boardState: BoardState, chain: PointState[], allChains: PointState[][]) {
const playerNeighbors = getPlayerNeighbors(boardState, chain);
const neighboringChains = playerNeighbors.reduce(
(neighborChains, neighbor) =>
neighborChains.add(allChains.find((chain) => chain[0].chain === neighbor.chain) || []),
new Set<PointState[]>(),
);
return [...neighboringChains];
}
/**
* Gets all points that have player pieces adjacent to the given point
*/
export function getPlayerNeighbors(boardState: BoardState, chain: PointState[]) {
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player !== playerColors.empty);
}
/**
* Gets all points adjacent to the given point
*/
export function getAllNeighbors(boardState: BoardState, chain: PointState[]) {
const allNeighbors = chain.reduce((chainNeighbors: Set<PointState>, point: PointState) => {
getArrayFromNeighbor(findNeighbors(boardState, point.x, point.y))
.filter((neighborPoint) => !isPointInChain(neighborPoint, chain))
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
return chainNeighbors;
}, new Set<PointState>());
return [...allNeighbors];
}
/**
* Determines if chain has a point that matches the given coordinates
*/
export function isPointInChain(point: PointState, chain: PointState[]) {
return !!chain.find((chainPoint) => chainPoint.x === point.x && chainPoint.y === point.y);
}
/**
* Looks through the board history to see if the current state is identical to any previous state
* Capped at 5 for calculation speed, because loops of size 6 are essentially impossible
*/
function checkIfBoardStateIsRepeated(boardState: BoardState) {
const currentBoard = boardState.board;
return boardState.history.slice(-5).find((state) => {
for (let x = 0; x < state.length; x++) {
for (let y = 0; y < state[x].length; y++) {
if (currentBoard[x]?.[y]?.player && currentBoard[x]?.[y]?.player !== state[x]?.[y]?.player) {
return false;
}
}
}
return true;
});
}
/**
* Finds all groups of connected pieces, or empty space groups
*/
export function getAllChains(boardState: BoardState): PointState[][] {
const chains: { [s: string]: PointState[] } = {};
for (let x = 0; x < boardState.board.length; x++) {
for (let y = 0; y < boardState.board[x].length; y++) {
const point = boardState.board[x]?.[y];
// If the current chain is already analyzed, skip it
if (!point || point.chain === "") {
continue;
}
chains[point.chain] = chains[point.chain] || [];
chains[point.chain].push(point);
}
}
return Object.keys(chains).map((key) => chains[key]);
}
/**
* Find any group of stones with no liberties (who therefore are to be removed from the board)
*/
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: PlayerColor) {
const opposingPlayer = playerWhoMoved === playerColors.white ? playerColors.black : playerColors.white;
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
if (enemyChainsToCapture) {
return enemyChainsToCapture;
}
const friendlyChainsToCapture = findCapturedChainOfColor(chainList, playerWhoMoved);
if (friendlyChainsToCapture) {
return friendlyChainsToCapture;
}
}
function findCapturedChainOfColor(chainList: PointState[][], playerColor: PlayerColor) {
return chainList.filter((chain) => chain?.[0].player === playerColor && chain?.[0].liberties?.length === 0);
}
/**
* Find all empty points adjacent to any piece in a given chain
*/
export function findLibertiesForChain(boardState: BoardState, chain: PointState[]): PointState[] {
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player === playerColors.empty);
}
/**
* Find all empty points adjacent to any piece in the chain that a given point belongs to
*/
export function findChainLibertiesForPoint(boardState: BoardState, x: number, y: number): PointState[] {
const chain = findAdjacentPointsInChain(boardState, x, y);
return findLibertiesForChain(boardState, chain);
}
/**
* Returns an object that includes which of the cardinal neighbors are empty
* (adjacent 'liberties' of the current piece )
*/
export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number, y: number): Neighbor {
const neighbors = findNeighbors(boardState, x, y);
const hasNorthLiberty = neighbors.north && neighbors.north.player === playerColors.empty;
const hasEastLiberty = neighbors.east && neighbors.east.player === playerColors.empty;
const hasSouthLiberty = neighbors.south && neighbors.south.player === playerColors.empty;
const hasWestLiberty = neighbors.west && neighbors.west.player === playerColors.empty;
return {
north: hasNorthLiberty ? neighbors.north : null,
east: hasEastLiberty ? neighbors.east : null,
south: hasSouthLiberty ? neighbors.south : null,
west: hasWestLiberty ? neighbors.west : null,
};
}
/**
* Returns an object that includes which of the cardinal neighbors are either empty or contain the
* current player's pieces. Used for making the connection map on the board
*/
export function findAdjacentLibertiesAndAlliesForPoint(
boardState: BoardState,
x: number,
y: number,
_player?: PlayerColor,
): Neighbor {
const currentPoint = boardState.board[x]?.[y];
const player =
_player || (!currentPoint || currentPoint.player === playerColors.empty ? undefined : currentPoint.player);
const adjacentLiberties = findAdjacentLibertiesForPoint(boardState, x, y);
const neighbors = findNeighbors(boardState, x, y);
return {
north: adjacentLiberties.north || neighbors.north?.player === player ? neighbors.north : null,
east: adjacentLiberties.east || neighbors.east?.player === player ? neighbors.east : null,
south: adjacentLiberties.south || neighbors.south?.player === player ? neighbors.south : null,
west: adjacentLiberties.west || neighbors.west?.player === player ? neighbors.west : null,
};
}
/**
* Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points.
*
* For example, a 5x5 board might look like this:
* ```
* [
* "XX.O.",
* "X..OO",
* ".XO..",
* "XXO..",
* ".XOO.",
* ]
* ```
*
* Each string represents a vertical column on the board, and each character in the string represents a point.
*
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index [1][0].
*
* 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 game.
*
*/
export function getSimplifiedBoardState(board: Board): string[] {
return board.map((column) =>
column.reduce((str, point) => {
if (!point) {
return str + "#";
}
if (point.player === playerColors.black) {
return str + "X";
}
if (point.player === playerColors.white) {
return str + "O";
}
return str + ".";
}, ""),
);
}
export function getBoardFromSimplifiedBoardState(
boardStrings: string[],
ai = opponents.Daedalus,
lastPlayer = playerColors.black,
) {
const newBoardState = getNewBoardState(boardStrings[0].length, ai);
newBoardState.previousPlayer = lastPlayer;
for (let x = 0; x < boardStrings[0].length; x++) {
for (let y = 0; y < boardStrings[0].length; y++) {
const boardStringPoint = boardStrings[x]?.[y];
const newBoardPoint = newBoardState.board[x]?.[y];
if (boardStringPoint === "#") {
newBoardState.board[x][y] = null;
}
if (boardStringPoint === "X" && newBoardPoint?.player) {
newBoardPoint.player = playerColors.black;
}
if (boardStringPoint === "O" && newBoardPoint?.player) {
newBoardPoint.player = playerColors.white;
}
}
}
return updateCaptures(newBoardState, lastPlayer);
}

@ -0,0 +1,98 @@
import { BoardState, playerColors, type PointState } from "../boardState/goConstants";
import {
getAllChains,
getAllEyes,
getAllNeighboringChains,
getAllPotentialEyes,
getAllValidMoves,
} from "./boardAnalysis";
import { contains, isNotNull } from "../boardState/boardState";
/**
* Any empty space fully encircled by the opponent is not worth playing in, unless one of its borders explicitly has a weakness
*
* Specifically, ignore any empty space encircled by the opponent, unless one of the chains that is on the exterior:
* * does not have too many more liberties
* * has been fully encircled on the outside by the current player
* * Only has liberties remaining inside the abovementioned empty space
*
* In which case, only the liberties of that one weak chain are worth considering. Other parts of that fully-encircled
* enemy space, and other similar spaces, should be ignored, otherwise the game drags on too long
*/
export function findDisputedTerritory(boardState: BoardState, player: playerColors, excludeFriendlyEyes?: boolean) {
let validMoves = getAllValidMoves(boardState, player);
if (excludeFriendlyEyes) {
const friendlyEyes = getAllEyes(boardState, player)
.filter((eye) => eye.length >= 2)
.flat()
.flat();
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
}
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
const chains = getAllChains(boardState);
const emptySpacesToAnalyze = getAllPotentialEyes(boardState, chains, opponent);
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
const playableNodesInsideOfEnemySpace = emptySpacesToAnalyze.reduce((playableNodes: PointState[], space) => {
// Look for any opponent chains on the border of the empty space, to see if it has a weakness
const attackableLiberties = space.neighbors
.map((neighborChain) => {
const liberties = neighborChain[0].liberties ?? [];
// Ignore border chains with too many liberties, they can't effectively be attacked
if (liberties.length > 4) {
return [];
}
// Get all opponent chains that make up the border of the opponent-controlled space
const neighborChains = getAllNeighboringChains(boardState, neighborChain, chains);
// Ignore border chains that do not touch the current player's pieces somewhere, as they are likely fully interior
// to the empty space in question, or only share a border with the edge of the board and the space, or are not yet
// surrounded on the exterior and ready to be attacked within
if (!neighborChains.find((chain) => chain?.[0]?.player === player)) {
return [];
}
const libertiesInsideOfSpaceToAnalyze = liberties
.filter(isNotNull)
.filter((point) => contains(space.chain, point));
// If the chain has any liberties outside the empty space being analyzed, it is not yet fully surrounded,
// and should not be attacked yet
if (libertiesInsideOfSpaceToAnalyze.length !== liberties.length) {
return [];
}
// If the enemy chain is fully surrounded on the outside of the space by the current player, then its liberties
// inside the empty space is worth considering for an attack
return libertiesInsideOfSpaceToAnalyze;
})
.flat();
return [...playableNodes, ...attackableLiberties];
}, []);
// Return only valid moves that are not inside enemy surrounded empty spaces, or ones that are explicitly next to an enemy chain that can be attacked
return validMoves.filter(
(move) => !contains(nodesInsideEyeSpacesToAnalyze, move) || contains(playableNodesInsideOfEnemySpace, move),
);
}
/**
If a group of stones has more than one empty holes that it completely surrounds, it cannot be captured, because white can
only play one stone at a time.
Thus, the empty space of those holes is firmly claimed by the player surrounding them, and it can be ignored as a play area
Once all points are either stones or claimed territory in this way, the game is over
Note that this does not detect mutual eyes formed by two chains making an eye together, or eyes via seki, or some other edge cases.
*/
export function findClaimedTerritory(boardState: BoardState) {
const whiteClaimedTerritory = getAllEyes(boardState, playerColors.white).filter(
(eyesForChainN) => eyesForChainN.length >= 2,
);
const blackClaimedTerritory = getAllEyes(boardState, playerColors.black).filter(
(eyesForChainN) => eyesForChainN.length >= 2,
);
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
}

@ -0,0 +1,96 @@
/** @param {NS} ns */
export async function main(ns) {
let result;
do {
const board = ns.go.getBoardState();
const validMoves = ns.go.analysis.getValidMoves();
const [growX, growY] = getGrowMove(board, validMoves);
const [randX, randY] = getRandomMove(board, validMoves);
// Try to pick a grow move, otherwise choose a random move
const x = growX ?? randX;
const y = growY ?? randY;
if (x === undefined) {
// Pass turn if no moves are found
result = await ns.go.passTurn();
} else {
// Play the selected move
result = await ns.go.makeMove(x, y);
}
await ns.sleep(100);
} while (result?.type !== "gameOver" && result?.type !== "pass");
// After the opponent passes, end the game by passing as well
await ns.go.passTurn();
}
/**
* Choose one of the empty points on the board at random to play
*/
const getRandomMove = (board, validMoves) => {
const moveOptions = [];
const size = board[0].length;
// Look through all the points on the board
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
// Make sure the point is a valid move
const isValidMove = validMoves[x][y];
// Leave some spaces to make it harder to capture our pieces
const isNotReservedSpace = x % 2 || y % 2;
if (isValidMove && isNotReservedSpace) {
moveOptions.push([x, y]);
}
}
}
// Choose one of the found moves at random
const randomIndex = Math.floor(Math.random() * moveOptions.length);
return moveOptions[randomIndex] ?? [];
};
/**
* Choose a point connected to a friendly stone to play
*/
const getGrowMove = (board, validMoves) => {
const moveOptions = [];
const size = board[0].length;
// Look through all the points on the board
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
// make sure the move is valid
const isValidMove = validMoves[x][y];
// Leave some open spaces to make it harder to capture our pieces
const isNotReservedSpace = x % 2 || y % 2;
// Make sure we are connected to a friendly piece
const neighbors = getNeighbors(board, x, y);
const hasFriendlyNeighbor = neighbors.includes("X");
if (isValidMove && isNotReservedSpace && hasFriendlyNeighbor) {
moveOptions.push([x, y]);
}
}
}
// Choose one of the found moves at random
const randomIndex = Math.floor(Math.random() * moveOptions.length);
return moveOptions[randomIndex] ?? [];
};
/**
* Find all adjacent points in the four connected directions
*/
const getNeighbors = (board, x, y) => {
const north = board[x + 1]?.[y];
const east = board[x][y + 1];
const south = board[x - 1]?.[y];
const west = board[x]?.[y - 1];
return [north, east, south, west];
};

@ -0,0 +1,811 @@
import {
BoardState,
EyeMove,
Move,
MoveOptions,
opponentDetails,
opponents,
PlayerColor,
playerColors,
playTypes,
PointState,
} from "../boardState/goConstants";
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
import {
evaluateIfMoveIsValid,
evaluateMoveResult,
findEffectiveLibertiesOfNewMove,
findEnemyNeighborChainWithFewestLiberties,
findMinLibertyCountOfAdjacentChains,
getAllChains,
getAllEyes,
getAllEyesByChainId,
getAllNeighboringChains,
getAllValidMoves,
} from "./boardAnalysis";
import { findDisputedTerritory } from "./controlledTerritory";
import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG";
import { Player } from "@player";
import { AugmentationName } from "@enums";
/*
Basic GO AIs, each with some personality and weaknesses
The AIs are aware of chains of connected pieces, their liberties, and their eyes.
They know how to lok for moves that capture or threaten capture, moves that create eyes, and moves that take
away liberties from their opponent, as well as some pattern matching on strong move ideas.
They do not know about larger jump moves, nor about frameworks on the board. Also, they each have a tendancy to
over-focus on a different type of move, giving each AI a different playstyle and weakness to exploit.
*/
/**
* Finds an array of potential moves based on the current board state, then chooses one
* based on the given opponent's personality and preferences. If no preference is given by the AI,
* will choose one from the reasonable moves at random.
*
* @returns a promise that will resolve with a move (or pass) from the designated AI opponent.
*/
export async function getMove(boardState: BoardState, player: PlayerColor, opponent: opponents, rngOverride?: number) {
await sleep(200);
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
const smart = isSmart(opponent, rng.random());
const moves = await getMoveOptions(boardState, player, rng.random(), smart);
const priorityMove = getFactionMove(moves, opponent, rng.random());
if (priorityMove) {
return {
type: playTypes.move,
x: priorityMove.x,
y: priorityMove.y,
};
}
// If no priority move is chosen, pick one of the reasonable moves
const moveOptions = [
moves.growth?.point,
moves.surround?.point,
moves.defend?.point,
moves.expansion?.point,
moves.pattern,
moves.eyeMove?.point,
moves.eyeBlock?.point,
]
.filter(isNotNull)
.filter(isDefined)
.filter((point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player, false));
const chosenMove = moveOptions[floor(rng.random() * moveOptions.length)];
if (chosenMove) {
await sleep(200);
console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
return {
type: playTypes.move,
x: chosenMove.x,
y: chosenMove.y,
};
} else {
console.debug("No valid moves found");
return handleNoMoveFound(boardState, player);
}
}
/**
* Detects if the AI is merely passing their turn, or if the game should end.
*
* Ends the game if the player passed on the previous turn before the AI passes,
* or if the player will be forced to pass their next turn after the AI passes.
*/
function handleNoMoveFound(boardState: BoardState, player: playerColors) {
passTurn(boardState, player);
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
if (remainingTerritory > 0 && boardState.passCount < 2) {
return {
type: playTypes.pass,
x: -1,
y: -1,
};
} else {
return {
type: playTypes.gameOver,
x: -1,
y: -1,
};
}
}
/**
* Given a group of move options, chooses one based on the given opponent's personality (if any fit their priorities)
*/
function getFactionMove(moves: MoveOptions, faction: opponents, rng: number): PointState | null {
if (faction === opponents.Netburners) {
return getNetburnersPriorityMove(moves, rng);
}
if (faction === opponents.SlumSnakes) {
return getSlumSnakesPriorityMove(moves, rng);
}
if (faction === opponents.TheBlackHand) {
return getBlackHandPriorityMove(moves, rng);
}
if (faction === opponents.Tetrads) {
return getTetradPriorityMove(moves, rng);
}
if (faction === opponents.Daedalus) {
return getDaedalusPriorityMove(moves, rng);
}
return getIlluminatiPriorityMove(moves, rng);
}
/**
* Determines if certain failsafes and mistake avoidance are enabled for the given move
*/
function isSmart(faction: opponents, rng: number) {
if (faction === opponents.Netburners) {
return false;
}
if (faction === opponents.SlumSnakes) {
return rng < 0.3;
}
if (faction === opponents.TheBlackHand) {
return rng < 0.8;
}
return true;
}
/**
* Netburners mostly just put random points around the board, but occasionally have a smart move
*/
function getNetburnersPriorityMove(moves: MoveOptions, rng: number): PointState | null {
if (rng < 0.2) {
return getIlluminatiPriorityMove(moves, rng);
} else if (rng < 0.4 && moves.expansion) {
return moves.expansion.point;
} else if (rng < 0.6 && moves.growth) {
return moves.growth.point;
} else if (rng < 0.75) {
return moves.random;
}
return null;
}
/**
* Slum snakes prioritize defending their pieces and building chains that snake around as much of the bord as possible.
*/
function getSlumSnakesPriorityMove(moves: MoveOptions, rng: number): PointState | null {
if (moves.defendCapture) {
console.debug("defend capture: defend move chosen");
return moves.defendCapture.point;
}
if (rng < 0.2) {
return getIlluminatiPriorityMove(moves, rng);
} else if (rng < 0.6 && moves.growth) {
return moves.growth.point;
} else if (rng < 0.65) {
return moves.random;
}
return null;
}
/**
* Black hand just wants to smOrk. They always capture or smother the opponent if possible.
*/
function getBlackHandPriorityMove(moves: MoveOptions, rng: number): PointState | null {
if (moves.capture) {
console.debug("capture: capture move chosen");
return moves.capture.point;
}
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 999) <= 1) {
console.debug("surround move chosen");
return moves.surround.point;
}
if (moves.defendCapture) {
console.debug("defend capture: defend move chosen");
return moves.defendCapture.point;
}
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 999) <= 2) {
console.debug("surround move chosen");
return moves.surround.point;
}
if (rng < 0.3) {
return getIlluminatiPriorityMove(moves, rng);
} else if (rng < 0.75 && moves.surround) {
return moves.surround.point;
} else if (rng < 0.8) {
return moves.random;
}
return null;
}
/**
* Tetrads really like to be up close and personal, cutting and circling their opponent
*/
function getTetradPriorityMove(moves: MoveOptions, rng: number) {
if (moves.capture) {
console.debug("capture: capture move chosen");
return moves.capture.point;
}
if (moves.defendCapture) {
console.debug("defend capture: defend move chosen");
return moves.defendCapture.point;
}
if (moves.pattern) {
console.debug("pattern match move chosen");
return moves.pattern;
}
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 1) {
console.debug("surround move chosen");
return moves.surround.point;
}
if (rng < 0.4) {
return getIlluminatiPriorityMove(moves, rng);
}
return null;
}
/**
* Daedalus almost always picks the Illuminati move, but very occasionally gets distracted.
*/
function getDaedalusPriorityMove(moves: MoveOptions, rng: number): PointState | null {
if (rng < 0.9) {
return getIlluminatiPriorityMove(moves, rng);
}
return null;
}
/**
* First prioritizes capturing of opponent pieces.
* Then, preventing capture of their own pieces.
* Then, creating "eyes" to solidify their control over the board
* Then, finding opportunities to capture on their next move
* Then, blocking the opponent's attempts to create eyes
* Finally, will match any of the predefined local patterns indicating a strong move.
*/
function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): PointState | null {
if (moves.capture) {
console.debug("capture: capture move chosen");
return moves.capture.point;
}
if (moves.defendCapture) {
console.debug("defend capture: defend move chosen");
return moves.defendCapture.point;
}
if (moves.eyeMove) {
console.debug("Create eye move chosen");
return moves.eyeMove.point;
}
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 1) {
console.debug("surround move chosen");
return moves.surround.point;
}
if (moves.eyeBlock) {
console.debug("Block eye move chosen");
return moves.eyeBlock.point;
}
if (moves.corner) {
console.debug("Corner move chosen");
return moves.corner;
}
const hasMoves = [moves.eyeMove, moves.eyeBlock, moves.growth, moves.defend, moves.surround].filter((m) => m).length;
const usePattern = rng > 0.25 || !hasMoves;
if (moves.pattern && usePattern) {
console.debug("pattern match move chosen");
return moves.pattern;
}
if (rng > 0.4 && moves.jump) {
console.debug("Jump move chosen");
return moves.jump.point;
}
if (rng < 0.6 && moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 2) {
console.debug("surround move chosen");
return moves.surround.point;
}
return null;
}
/**
* Get a move that places a piece to influence (and later control) a corner
*/
function getCornerMove(boardState: BoardState) {
const boardEdge = boardState.board[0].length - 1;
const cornerMax = boardEdge - 2;
if (isCornerAvailableForMove(boardState, cornerMax, cornerMax, boardEdge, boardEdge)) {
return boardState.board[cornerMax][cornerMax];
}
if (isCornerAvailableForMove(boardState, 0, cornerMax, cornerMax, boardEdge)) {
return boardState.board[2][cornerMax];
}
if (isCornerAvailableForMove(boardState, 0, 0, 2, 2)) {
return boardState.board[2][2];
}
if (isCornerAvailableForMove(boardState, cornerMax, 0, boardEdge, 2)) {
return boardState.board[cornerMax][2];
}
return null;
}
/**
* Find all non-offline nodes in a given area
*/
function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
const foundPoints: PointState[] = [];
boardState.board.forEach((column) =>
column.forEach(
(point) => point && point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2 && foundPoints.push(point),
),
);
return foundPoints;
}
/**
* Determine if a corner is largely intact and currently empty, and thus a good target for corner takeover moves
*/
function isCornerAvailableForMove(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
const foundPoints = findLiveNodesInArea(boardState, x1, y1, x2, y2);
const foundPieces = foundPoints.filter((point) => point.player !== playerColors.empty);
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
}
/**
* Select a move from the list of open-area moves
*/
function getExpansionMove(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
rng: number,
moveArray?: Move[],
) {
const moveOptions = moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces);
const randomIndex = floor(rng * moveOptions.length);
return moveOptions[randomIndex];
}
/**
* Get a move in open space that is nearby a friendly piece
*/
function getJumpMove(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
rng: number,
moveArray?: Move[],
) {
const board = boardState.board;
const moveOptions = (moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces)).filter(({ point }) =>
[
board[point.x]?.[point.y + 2],
board[point.x + 2]?.[point.y],
board[point.x]?.[point.y - 2],
board[point.x - 2]?.[point.y],
].some((point) => point?.player === player),
);
const randomIndex = floor(rng * moveOptions.length);
return moveOptions[randomIndex];
}
/**
* Finds a move in an open area to expand influence and later build on
*/
export function getExpansionMoveArray(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
): Move[] {
// Look for any empty spaces fully surrounded by empty spaces to expand into
const emptySpaces = availableSpaces.filter((space) => {
const neighbors = findNeighbors(boardState, space.x, space.y);
return (
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
(point) => point && point.player === playerColors.empty,
).length === 4
);
});
// Once no such empty areas exist anymore, instead expand into any disputed territory
// to gain a few more points in endgame
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(boardState, player, availableSpaces, 1);
const moveOptions = [...emptySpaces, ...disputedSpaces];
return moveOptions.map((point) => {
return {
point: point,
newLibertyCount: -1,
oldLibertyCount: -1,
};
});
}
function getDisputedTerritoryMoves(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
maxChainSize = 99,
) {
const chains = getAllChains(boardState).filter((chain) => chain.length <= maxChainSize);
return availableSpaces.filter((space) => {
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
const playerNeighbors = getAllNeighboringChains(boardState, chain, chains);
const hasWhitePieceNeighbor = playerNeighbors.find(
(neighborChain) => neighborChain[0]?.player === playerColors.white,
);
const hasBlackPieceNeighbor = playerNeighbors.find(
(neighborChain) => neighborChain[0]?.player === playerColors.black,
);
return hasWhitePieceNeighbor && hasBlackPieceNeighbor;
});
}
/**
* Finds all moves that increases the liberties of the player's pieces, making them harder to capture and occupy more space on the board.
*/
async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
if (!friendlyChains.length) {
return [];
}
// Get all liberties of friendly chains as potential growth move options
const liberties = friendlyChains
.map((chain) =>
chain[0].liberties?.filter(isNotNull).map((liberty) => ({
libertyPoint: liberty,
oldLibertyCount: chain[0].liberties?.length,
})),
)
.flat()
.filter(isNotNull)
.filter(isDefined)
.filter((liberty) =>
availableSpaces.find((point) => liberty.libertyPoint.x === point.x && liberty.libertyPoint.y === point.y),
);
// Find a liberty where playing a piece increases the liberty of the chain (aka expands or defends the chain)
return liberties
.map((liberty) => {
const move = liberty.libertyPoint;
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
// Get the smallest liberty count of connected chains to represent the old state
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(boardState, move.x, move.y, player);
return {
point: move,
oldLibertyCount: oldLibertyCount,
newLibertyCount: newLibertyCount,
};
})
.filter((move) => move.newLibertyCount > 1 && move.newLibertyCount >= move.oldLibertyCount);
}
/**
* Find a move that increases the player's liberties by the maximum amount
*/
async function getGrowthMove(
initialState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
rng: number,
) {
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
const maxLibertyCount = Math.max(...growthMoves.map((l) => l.newLibertyCount - l.oldLibertyCount));
const moveCandidates = growthMoves.filter((l) => l.newLibertyCount - l.oldLibertyCount === maxLibertyCount);
return moveCandidates[floor(rng * moveCandidates.length)];
}
/**
* Find a move that specifically increases a chain's liberties from 1 to more than 1, preventing capture
*/
async function getDefendMove(initialState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
const libertyIncreases =
growthMoves?.filter((move) => move.oldLibertyCount <= 1 && move.newLibertyCount > move.oldLibertyCount) ?? [];
const maxLibertyCount = Math.max(...libertyIncreases.map((l) => l.newLibertyCount - l.oldLibertyCount));
if (maxLibertyCount < 1) {
return null;
}
const moveCandidates = libertyIncreases.filter((l) => l.newLibertyCount - l.oldLibertyCount === maxLibertyCount);
return moveCandidates[floor(Math.random() * moveCandidates.length)];
}
/**
* Find a move that reduces the opponent's liberties as much as possible,
* capturing (or making it easier to capture) their pieces
*/
async function getSurroundMove(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
smart = true,
) {
const opposingPlayer = player === playerColors.black ? playerColors.white : playerColors.black;
const enemyChains = getAllChains(boardState).filter((chain) => chain[0].player === opposingPlayer);
if (!enemyChains.length || !availableSpaces.length) {
return null;
}
const enemyLiberties = enemyChains
.map((chain) => chain[0].liberties)
.flat()
.filter((liberty) => availableSpaces.find((point) => liberty?.x === point.x && liberty?.y === point.y))
.filter(isNotNull);
const captureMoves: Move[] = [];
const atariMoves: Move[] = [];
const surroundMoves: Move[] = [];
enemyLiberties.forEach((move) => {
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
const weakestEnemyChain = findEnemyNeighborChainWithFewestLiberties(
boardState,
move.x,
move.y,
player === playerColors.black ? playerColors.white : playerColors.black,
);
const weakestEnemyChainLength = weakestEnemyChain?.length ?? 99;
const enemyChainLibertyCount = weakestEnemyChain?.[0]?.liberties?.length ?? 99;
const enemyLibertyGroups = [
...(weakestEnemyChain?.[0]?.liberties ?? []).reduce(
(chainIDs, point) => chainIDs.add(point?.chain ?? ""),
new Set<string>(),
),
];
// Do not suggest moves that do not capture anything and let your opponent immediately capture
if (newLibertyCount <= 2 && enemyChainLibertyCount > 2) {
return;
}
// If a neighboring enemy chain has only one liberty, the current move suggestion will capture
if (enemyChainLibertyCount <= 1) {
captureMoves.push({
point: move,
oldLibertyCount: enemyChainLibertyCount,
newLibertyCount: enemyChainLibertyCount - 1,
});
}
// If the move puts the enemy chain in threat of capture, it forces the opponent to respond.
// Only do this if your piece cannot be captured, or if the enemy group is surrounded and vulnerable to losing its only interior space
else if (
enemyChainLibertyCount === 2 &&
(newLibertyCount >= 2 || (enemyLibertyGroups.length === 1 && weakestEnemyChainLength > 3) || !smart)
) {
atariMoves.push({
point: move,
oldLibertyCount: enemyChainLibertyCount,
newLibertyCount: enemyChainLibertyCount - 1,
});
}
// If the move will not immediately get re-captured, and limit's the opponent's liberties
else if (newLibertyCount >= 2) {
surroundMoves.push({
point: move,
oldLibertyCount: enemyChainLibertyCount,
newLibertyCount: enemyChainLibertyCount - 1,
});
}
});
return [...captureMoves, ...atariMoves, ...surroundMoves][0];
}
/**
* Finds all moves that would create an eye for the given player.
*
* An "eye" is empty point(s) completely surrounded by a single player's connected pieces.
* If a chain has multiple eyes, it cannot be captured by the opponent (since they can only fill one eye at a time,
* and suiciding your own pieces is not legal unless it captures the opponents' first)
*/
function getEyeCreationMoves(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
maxLiberties = 99,
) {
const allEyes = getAllEyesByChainId(boardState, player);
const currentEyes = getAllEyes(boardState, player, allEyes);
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
const currentLivingGroupsCount = currentLivingGroupIDs.length;
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
const chains = getAllChains(boardState);
const friendlyLiberties = chains
.filter((chain) => chain[0].player === player)
.filter((chain) => chain.length > 1)
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
.map((chain) => chain[0].liberties)
.flat()
.filter(isNotNull)
.filter((point) =>
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
)
.filter((point: PointState) => {
console.warn("eye check ", point.x, point.y);
const neighbors = findNeighbors(boardState, point.x, point.y);
const neighborhood = [neighbors.north, neighbors.east, neighbors.south, neighbors.west];
return (
neighborhood.filter((point) => !point || point?.player === player).length >= 2 &&
neighborhood.some((point) => point?.player === playerColors.empty)
);
});
const eyeCreationMoves = friendlyLiberties.reduce((moveOptions: EyeMove[], point: PointState) => {
const evaluationBoard = evaluateMoveResult(boardState, point.x, point.y, player);
const newEyes = getAllEyes(evaluationBoard, player);
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
const newEyeCount = newEyes.filter((eye) => eye.length).length;
if (
newLivingGroupsCount > currentLivingGroupsCount ||
(newEyeCount > currentEyeCount && newLivingGroupsCount === currentLivingGroupsCount)
) {
moveOptions.push({
point: point,
createsLife: newLivingGroupsCount > currentLivingGroupsCount,
});
}
return moveOptions;
}, []);
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
}
function getEyeCreationMove(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
return getEyeCreationMoves(boardState, player, availableSpaces)[0];
}
/**
* If there is only one move that would create two eyes for the opponent, it should be blocked if possible
*/
function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availablePoints: PointState[]) {
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
const opponentEyeMoves = getEyeCreationMoves(boardState, opposingPlayer, availablePoints, 5);
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
const oneEyeMoves = opponentEyeMoves.filter((move) => !move.createsLife);
if (twoEyeMoves.length === 1) {
return twoEyeMoves[0];
}
if (!twoEyeMoves.length && oneEyeMoves.length === 1) {
return oneEyeMoves[0];
}
return null;
}
/**
* Gets a group of reasonable moves based on the current board state, to be passed to the factions' AI to decide on
*/
async function getMoveOptions(
boardState: BoardState,
player: PlayerColor,
rng: number,
smart = true,
): Promise<MoveOptions> {
const availableSpaces = findDisputedTerritory(boardState, player, smart);
const contestedPoints = getDisputedTerritoryMoves(boardState, player, availableSpaces);
const expansionMoves = getExpansionMoveArray(boardState, player, availableSpaces);
// If the player is passing, and all territory is surrounded by a single color: do not suggest moves that
// needlessly extend the game, unless they actually can change the score
const endGameAvailable = !contestedPoints.length && boardState.passCount;
const growthMove = endGameAvailable ? null : await getGrowthMove(boardState, player, availableSpaces, rng);
await sleep(80);
const expansionMove = await getExpansionMove(boardState, player, availableSpaces, rng, expansionMoves);
await sleep(80);
const jumpMove = await getJumpMove(boardState, player, availableSpaces, rng, expansionMoves);
await sleep(80);
const defendMove = await getDefendMove(boardState, player, availableSpaces);
await sleep(80);
const surroundMove = await getSurroundMove(boardState, player, availableSpaces, smart);
await sleep(80);
const eyeMove = endGameAvailable ? null : getEyeCreationMove(boardState, player, availableSpaces);
await sleep(80);
const eyeBlock = endGameAvailable ? null : getEyeBlockingMove(boardState, player, availableSpaces);
await sleep(80);
const cornerMove = getCornerMove(boardState);
const pattern = endGameAvailable
? null
: await findAnyMatchedPatterns(boardState, player, availableSpaces, smart, rng);
// Only offer a random move if there are some contested spaces on the board.
// (Random move should not be picked if the AI would otherwise pass turn.)
const random = contestedPoints.length ? availableSpaces[floor(rng * availableSpaces.length)] : null;
const captureMove = surroundMove && surroundMove?.newLibertyCount === 0 ? surroundMove : null;
const defendCaptureMove =
defendMove && defendMove.oldLibertyCount == 1 && defendMove?.newLibertyCount > 1 ? defendMove : null;
console.debug("---------------------");
console.debug("capture: ", captureMove?.point?.x, captureMove?.point?.y);
console.debug("defendCapture: ", defendCaptureMove?.point?.x, defendCaptureMove?.point?.y);
console.debug("eyeMove: ", eyeMove?.point?.x, eyeMove?.point?.y);
console.debug("eyeBlock: ", eyeBlock?.point?.x, eyeBlock?.point?.y);
console.debug("pattern: ", pattern?.x, pattern?.y);
console.debug("surround: ", surroundMove?.point?.x, surroundMove?.point?.y);
console.debug("defend: ", defendMove?.point?.x, defendMove?.point?.y);
console.debug("Growth: ", growthMove?.point?.x, growthMove?.point?.y);
console.debug("Expansion: ", expansionMove?.point?.x, expansionMove?.point?.y);
console.debug("Jump: ", jumpMove?.point?.x, jumpMove?.point?.y);
console.debug("Corner: ", cornerMove?.x, cornerMove?.y);
console.debug("Random: ", random?.x, random?.y);
return {
capture: captureMove,
defendCapture: defendCaptureMove,
eyeMove: eyeMove,
eyeBlock: eyeBlock,
pattern: pattern,
growth: growthMove,
expansion: expansionMove,
jump: jumpMove,
defend: defendMove,
surround: surroundMove,
corner: cornerMove,
random: random,
};
}
/**
* Gets the starting score for white.
*/
export function getKomi(opponent: opponents) {
return opponentDetails[opponent].komi;
}
/**
* Allows time to pass
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function showWorldDemon() {
return Player.augmentations.some((a) => a.name === AugmentationName.TheRedPill) && Player.sourceFileLvl(1);
}

@ -0,0 +1,198 @@
// Inspired by https://github.com/pasky/michi/blob/master/michi.py
import { BoardState, PlayerColor, playerColors, PointState } from "../boardState/goConstants";
import { sleep } from "./goAI";
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
import { floor } from "../boardState/boardState";
export const threeByThreePatterns = [
// 3x3 piece patterns; X,O are color pieces; x,o are any state except the opposite color piece;
// " " is off the edge of the board; "?" is any state (even off the board)
[
"XOX", // hane pattern - enclosing hane
"...",
"???",
],
[
"XO.", // hane pattern - non-cutting hane
"...",
"?.?",
],
[
"XO?", // hane pattern - magari
"X..",
"o.?",
],
[
".O.", // generic pattern - katatsuke or diagonal attachment; similar to magari
"X..",
"...",
],
[
"XO?", // cut1 pattern (kiri] - unprotected cut
"O.x",
"?x?",
],
[
"XO?", // cut1 pattern (kiri] - peeped cut
"O.X",
"???",
],
[
"?X?", // cut2 pattern (de]
"O.O",
"xxx",
],
[
"OX?", // cut keima
"x.O",
"???",
],
[
"X.?", // side pattern - chase
"O.?",
" ",
],
[
"OX?", // side pattern - block side cut
"X.O",
" ",
],
[
"?X?", // side pattern - block side connection
"o.O",
" ",
],
[
"?XO", // side pattern - sagari
"o.o",
" ",
],
[
"?OX", // side pattern - cut
"X.O",
" ",
],
];
/**
* Searches the board for any point that matches the expanded pattern set
*/
export async function findAnyMatchedPatterns(
boardState: BoardState,
player: PlayerColor,
availableSpaces: PointState[],
smart = true,
rng: number,
) {
const board = boardState.board;
const boardSize = board[0].length;
const patterns = expandAllThreeByThreePatterns();
const moves = [];
for (let x = 0; x < boardSize; x++) {
for (let y = 0; y < boardSize; y++) {
const neighborhood = getNeighborhood(boardState, x, y);
const matchedPattern = patterns.find((pattern) => checkMatch(neighborhood, pattern, player));
if (
matchedPattern &&
availableSpaces.find((availablePoint) => availablePoint.x === x && availablePoint.y === y) &&
(!smart || findEffectiveLibertiesOfNewMove(boardState, x, y, player).length > 1)
) {
moves.push(board[x][y]);
}
}
await sleep(10);
}
return moves[floor(rng * moves.length)] || null;
}
/**
Returns false if any point does not match the pattern, and true if it matches fully.
*/
function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], player: PlayerColor) {
const patternArr = pattern.join("").split("");
const neighborhoodArray = neighborhood.flat();
return patternArr.every((str, index) => matches(str, neighborhoodArray[index], player));
}
/**
* Gets the 8 points adjacent and diagonally adjacent to the given point
*/
function getNeighborhood(boardState: BoardState, x: number, y: number) {
const board = boardState.board;
return [
[board[x - 1]?.[y - 1], board[x - 1]?.[y], board[x - 1]?.[y + 1]],
[board[x]?.[y - 1], board[x]?.[y], board[x]?.[y + 1]],
[board[x + 1]?.[y - 1], board[x + 1]?.[y], board[x + 1]?.[y + 1]],
];
}
/**
* @returns true if the given point matches the given string representation, false otherwise
*
* Capital X and O only match stones of that color
* lowercase x and o match stones of that color, or empty space, or the edge of the board
* a period "." only matches empty nodes
* A space " " only matches the edge of the board
* question mark "?" matches anything
*/
function matches(stringPoint: string, point: PointState | null, player: PlayerColor) {
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
switch (stringPoint) {
case "X": {
return point?.player === player;
}
case "O": {
return point?.player === opponent;
}
case "x": {
return point?.player !== opponent;
}
case "o": {
return point?.player !== player;
}
case ".": {
return point?.player === playerColors.empty;
}
case " ": {
return point === null;
}
case "?": {
return true;
}
}
}
/**
* Finds all variations of the pattern list, by expanding it using rotation and mirroring
*/
function expandAllThreeByThreePatterns() {
const rotatedPatterns = [
...threeByThreePatterns,
...threeByThreePatterns.map(rotate90Degrees),
...threeByThreePatterns.map(rotate90Degrees).map(rotate90Degrees),
...threeByThreePatterns.map(rotate90Degrees).map(rotate90Degrees).map(rotate90Degrees),
];
const mirroredPatterns = [...rotatedPatterns, ...rotatedPatterns.map(verticalMirror)];
return [...mirroredPatterns, ...mirroredPatterns.map(horizontalMirror)];
}
function rotate90Degrees(pattern: string[]) {
return [
`${pattern[2][0]}${pattern[1][0]}${pattern[0][0]}`,
`${pattern[2][1]}${pattern[1][1]}${pattern[0][1]}`,
`${pattern[2][2]}${pattern[1][2]}${pattern[0][2]}`,
];
}
function verticalMirror(pattern: string[]) {
return [pattern[2], pattern[1], pattern[0]];
}
function horizontalMirror(pattern: string[]) {
return [
pattern[0].split("").reverse().join(),
pattern[1].split("").reverse().join(),
pattern[2].split("").reverse().join(),
];
}

@ -0,0 +1,179 @@
import {
BoardState,
getGoPlayerStartingState,
opponents,
PlayerColor,
playerColors,
PointState,
} from "../boardState/goConstants";
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
import { getKomi } from "./goAI";
import { Player } from "@player";
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
import { floor, isNotNull } from "../boardState/boardState";
import { Factions } from "../../Faction/Factions";
import { FactionName } from "@enums";
/**
* Returns the score of the current board.
* Each player gets one point for each piece on the board, and one point for any empty node
* fully surrounded by their pieces
*/
export function getScore(boardState: BoardState) {
const komi = getKomi(boardState.ai) ?? 6.5;
const whitePieces = getColoredPieceCount(boardState, playerColors.white);
const blackPieces = getColoredPieceCount(boardState, playerColors.black);
const territoryScores = getTerritoryScores(boardState);
return {
[playerColors.white]: {
pieces: whitePieces,
territory: territoryScores[playerColors.white],
komi: komi,
sum: whitePieces + territoryScores[playerColors.white] + komi,
},
[playerColors.black]: {
pieces: blackPieces,
territory: territoryScores[playerColors.black],
komi: 0,
sum: blackPieces + territoryScores[playerColors.black],
},
};
}
/**
* Handles ending the game. Sets the previous player to null to prevent further moves, calculates score, and updates
* player node count and power, and game history
*/
export function endGoGame(boardState: BoardState) {
if (boardState.previousPlayer === null) {
return;
}
boardState.previousPlayer = null;
const statusToUpdate = getPlayerStats(boardState.ai);
statusToUpdate.favor = statusToUpdate.favor ?? 0;
const score = getScore(boardState);
if (score[playerColors.black].sum < score[playerColors.white].sum) {
resetWinstreak(boardState.ai, true);
statusToUpdate.nodePower += floor(score[playerColors.black].sum * 0.25);
} else {
statusToUpdate.wins++;
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
statusToUpdate.winStreak = statusToUpdate.oldWinStreak < 0 ? 1 : statusToUpdate.winStreak + 1;
if (statusToUpdate.winStreak > statusToUpdate.highestWinStreak) {
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
}
const factionName = boardState.ai as unknown as FactionName;
if (
statusToUpdate.winStreak % 2 === 0 &&
Player.factions.includes(factionName) &&
statusToUpdate.favor < getMaxFavor() &&
Factions?.[factionName]
) {
Factions[factionName].favor++;
statusToUpdate.favor++;
}
}
statusToUpdate.nodePower +=
score[playerColors.black].sum *
getDifficultyMultiplier(score[playerColors.white].komi, boardState.board[0].length) *
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
statusToUpdate.nodes += score[playerColors.black].sum;
Player.go.boardState = boardState;
Player.go.previousGameFinalBoardState = boardState;
// Update multipliers with new bonuses, once at the end of the game
Player.applyEntropy(Player.entropy);
}
/**
* Sets the winstreak to zero for the given opponent, and adds a loss
*/
export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
const statusToUpdate = getPlayerStats(opponent);
statusToUpdate.losses++;
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
if (statusToUpdate.winStreak >= 0) {
statusToUpdate.winStreak = -1;
} else if (gameComplete) {
// Only increase the "dry streak" count if the game actually finished
statusToUpdate.winStreak--;
}
}
/**
* Gets the number pieces of a given color on the board
*/
function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
return boardState.board.reduce(
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.player === color).length,
0,
);
}
/**
* Finds all empty spaces fully surrounded by a single player's stones
*/
function getTerritoryScores(boardState: BoardState) {
const emptyTerritoryChains = getAllChains(boardState).filter((chain) => chain?.[0]?.player === playerColors.empty);
return emptyTerritoryChains.reduce(
(scores, currentChain) => {
const chainColor = checkTerritoryOwnership(boardState, currentChain);
return {
[playerColors.white]:
scores[playerColors.white] + (chainColor === playerColors.white ? currentChain.length : 0),
[playerColors.black]:
scores[playerColors.black] + (chainColor === playerColors.black ? currentChain.length : 0),
};
},
{
[playerColors.white]: 0,
[playerColors.black]: 0,
},
);
}
/**
* Finds all neighbors of the empty points in question. If they are all one color, that player controls that space
*/
function checkTerritoryOwnership(boardState: BoardState, emptyPointChain: PointState[]) {
if (emptyPointChain.length > boardState.board[0].length ** 2 - 3) {
return null;
}
const playerNeighbors = getPlayerNeighbors(boardState, emptyPointChain);
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.white);
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.black);
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
const isBlackTerritory = hasBlackPieceNeighbors && !hasWhitePieceNeighbors;
return isWhiteTerritory ? playerColors.white : isBlackTerritory ? playerColors.black : null;
}
/**
* prints the board state to the console
*/
export function logBoard(boardState: BoardState): void {
const state = boardState.board;
console.log("--------------");
for (let x = 0; x < state.length; x++) {
let output = `${x}: `;
for (let y = 0; y < state[x].length; y++) {
const point = state[x][y];
output += ` ${point?.chain ?? ""}`;
}
console.log(output);
}
}
export function getPlayerStats(opponent: opponents) {
if (!Player.go.status[opponent]) {
Player.go = getGoPlayerStartingState();
}
return Player.go.status[opponent];
}

@ -0,0 +1,242 @@
// The character 'wei' is part of the original Chinese name of the game Go, meaning to surround or enclose
export const weiArt = `
... .:lc;,'',:c:. ..
.:;,'.,dOOxddONWNNNNNWNOdxxl,,.
.''.... .:dxdolc::kWWNXK0KWMMMMMMMMMMMMMMMMMNK0kl:'..
...... 'colcc:;,'.,xNXXK0OkxxkXMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKkxo:;'..
'c:::;,,'.....;xK00Okxxdold0WMWWWNNNXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNX0xxo;.
.'......'. .'''...... .dkkxddollc:::dKWWWNNNXXKK000XMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0xooc.
:OKOOO0KOl::;. .. ....... .loolllc::;;,,'.'dNNNXKK00OOkxxONMMMMMMWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWXko;.
.kMMMMMMNXNXkdxd:.';;,,''....... ;0KK00OOkkxxddoollkWMMWWWWWWNNNNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNNWWWWWWWWWWWWWWWWWWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMW0d:,.
:KMMMMMMMMMMMMMN0KXNNNNXXXKKKK000XWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKkkOOOO00000KKKKXXXXXNNNNNWWWWWx,',,,;;;:::cccccllllcccc:;cOXXMMMMMMMMMMMMMMMMMMMMMMMMMMWKxd;
.cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKKKXXNNNWWMKocclloodxxkkO0Oo' .............''''',,,;;;:;. 'cdKWMMMMMMMMMMMMMMMMMMMMMMMMMMWKc
.:ONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNWWWWWWMMXkodxxkO0KKX0o'..',,;:cl; ... 'cONMMMMMMMMMMMMMMMMMMMMMMMMWOl'
;lxNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0kO00KXXNWXd;,;::cloddkx;...';:'.... .dNMMMMMMMMMMMMMMMMMMMMMMMNOo'
'kWMMMMMMMMMMMMMMMNXXXXKKKKKXXNNWWWOoclodxkO0x' ....',,;, .xK00000d;;:,.. 'odKMMMMMMMMMMMMMMMMMMMMMMXl,.
.ckNMMMMMMMMMMMMMWx'.........',;:cc:. . .dXWMMMWNXNKxoc''. .,0MMMMMMMMMMMMMMMMMMMMMXOd'
.cXMMMMMMMMMMMMMWx. .:l0MMMMMMMMMWK0koc:' cKMMMMMMMMMMMMMMMMMMMMMXo'
'kWMMMMMMMMMMMMMWx. .:KMMMMMMMMMMMMWWXkl;.. .dNMMMMMMMMMMMMMMMMMMMMKl,
,xkXMMMMMMMMMMMMWk. ,o0WMMMMMMMMMMMMMWXkd; .kWMMMMMMMMMMMMMMMMMMMMK:
.;0MMMMMMMMMMMMWk' .dWMMMMMMMMMMMMMMWKOc ... .... .lOXMMMMMMMMMMMMMMMMMMK0o.
:KMMMMMMMMMMMMWO' 'kWMMMMMMMMMMMMMNxc:. ..;c:o00kxxkOOd::c,.. .kMMMMMMMMMMMMMMMMMWo'.
.oXMMMMMMMMMMMMWO, 'kWMMMMMMMMMMMMXxl' .,;oxOXNWMMMMMMMWNXWKkxo;;' 'OMMMMMMMMMMMMMMMMMWx.
.xNMMMMMMMMMMMMW0, .kWMMMMMMMMMMMMKl' ''.. .:lllOKKWMMMMMMMMMMMMMMMMMWXK0kl'.. ;0MMMMMMMMMMMMMMMMMWk'
.OWMMMMMMMMMMMMW0; .dWMMMMMMMMMMMKxxocc::dKKOkxONWWMMMMMMMMMMMMMMMMMMMMMMMMMMN0ko' .cXMMMMMMMMMMMMMMMMMW0;
'ONWMMMMMMMMMMMMK: ',dWMMMMMMMMMMMKxxkKWWNNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXd:. .dNMMMMMMMMMMMMMMMMMMXc
.;OMMMMMMMMMMMMK: .''..... .xKXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKO: .ckKMMMMMMMMMMMMMMMWK0o
.xMMMMMMMMMMMMXc .. .:oc;,',xNNXK0OOkxkXMMMMMMMMMMMMMMMMMMMMMMWXXXXNWXdcoxkOkxdOWMMMMMMMMMMMMMMMMMMWXOx: .oWMMMMMMMMMMMMMMNc'.
.kMMMMMMMMMMMMNl ;xxdxXMWNNXXWMMMMMMMMMMMMMMMMMMMMMMMWWWMNkdkOKO;.',;c;. ;OWMMMMMMMMMMMMMMMMWXOdc' .xWMMMMMMMMMMMMMMNo.
.OMMMMMMMMMMMMNo 'coONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWOolol. ... ;KWMMMMMMMMMMMMMMWXOxc' 'kWMMMMMMMMMMMMMMNx.
'OMMMMMMMMMMMMWd. .';oKWNXXXXXXXXXNNW0dxd0MMMMMMMMMXxo; 'kWMMMMMMMMMMMMMWOlc' ,OWMMMMMMMMMMMMMMWO'
,0MMMMMMMMMMWOo, .;:,''......',;c',xx0MMMMMMMMMXl. .oNMMMMMMMMMMMMN0x; ,0WMMMMMMMMMMMMMMW0;
;0MMMMMMMMMMX: .kWMMMMMMMMMMWO' :oxNMMMMMMMMMMMNOc'. ;0WMMMMMMMMMMMMMMMK:
;0MMMMMMMMMMNc .lXMMMMMMMMMXd;. :0WMMMMMMMMMMM0o:. ;0MMMMMMMMMMMMMMMMXc
:KMMMMMMMMMMNl :KMMMMMMMMMXl. ,:xWMMMMMMMMMW0o:. :KMMMMMMMMMMMMMMMMNc
:KMMMMMMMMMMNo. ..,OMMMMMMMMNkc. .o0XMMMMMMMMMMWk;. .'.... .... :KMMMMMMMMMMMMMMMMWl
cKMMMMMMMMMMNo. ,kOXMMMMMMMM0; .;OMMMMMMMMMMWOc' .lolc::;',kNXK0OOkkO00x:;:,.. cXMMMMMMMMMMMMMMWKo.
cKMMMMMMMMMMNd. 'OWMMMMMMMMWXl. .;lddKMMMMMMMMMMW0xxxdoolkWMWWWNNNNWMMMMMMMMMMMNXNKkdo;. cXMMMMMMMMMMMMMMK;
cKMMMMMMMMMMNd. .dNMMMMMMMWO:...,'.... .cxkxddollc:lONNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXKOl:' lNMMMMMMMMMMMMMMK,
cKMMMMMMMMMMWx. ....lXMMMMMMMWO:',l0XXK0OkkxONMMMMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNkoc. lNMMMMMMMMMMMMMMK,
:KMMMMMMMMMMWk' .::;,'...'xK0kkNMMMMMMMMWNXXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKl. oWMMMMMMMMMMMMMMK,
:KMMMMMMMMMMWk' .,,,,''''.... .:dxdolc::kWWNNXXK0KNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0kkOOO0000000KKKKKKKKKKKKKKXXXXXXXXXXXXXXXXXXXXNXl. .dWMMMMMMMMMMMMMMK,
:KMMMMMMMMMMWO, .:;,c0NWNNNNXXK0OkkxkNMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKKXXXNNNWWWMMXdcclloddxkkOOOl. ...........................'''''''''''''''''. .:xNMMMMMMMMMMMMK,
;0MMMMMMMMMMWO, .xKXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWWWWKxdxkO00KXXo...'',;;:ccllc, ... :XMMMMMMMMMMMMK,
;0MMMMMMMMMMW0; .oxONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXO0KXNk;;:lol, ....... cNMMMMMMMMMMMMK,
,0MMMMMMMMMMWK; .,cxKXXWMMMMMMMMMMMMMMMNKXNN0olodxOl...',. lNMMMMMMMMMMMMK,
,OMMMMMMMMMMWK: .,::lkXWWMMMMMMWWXkxkkc,;::. .lNMMMMMMMMMMMMK,
'OMMMMMMMMMMMX: ..:x0KOkkOK0xl;.... .oNMMMMMMMMMMMMK,
'OMMMMMMMMMMMXc .,..;:..,' .. 'clc;,,,;:l:... .dNMMMMMMMMMMMMK,
.kMMMMMMMMMMMNc .. .;;:,'.,dOkdxKWWNNNNNWNOxo;''. .xNMMMMMMMMMMMMK,
.kMMMMMMMMMN0x, .'...',. ','....;dkxdlc:ldONNXK0KWMMMMMMMMMMMMMMMX00xoc,. .xNMMMMMMMMMMMMK,
.xMMMMMMMMMK, .,d0KKXOc;:c. .. ... .clc;,;dKXK0OkkXMMMMWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMW0occ;. .xWMMMMMMMMMMMMK,
.xMMMMMMMMMK; .,lXMMMNXNWNOxd:'...:O0OkdoxNWWNNXNWMMMMMMWNWWWWWWMMMMMMMMMMWWWMMMMMMMMMMMMMMMMMNNXk;. .xWMMMMMMMMMMMMK,
.d0NMMMMMMMMMK: .cdONMMMMMMMMN0KKK0XWMMMMMMMMMMMMMWKO0XNNOc;::clooodxxxxxxdloxKWMMMMMMMMMMMMMMMMMMNxc. .xNMMMMMMMMMMMMK,
'0MMMMMMMMMMMXc .':OWMMMMMMMMMMMMMMWKKKXXNWNxloxOk;...,;;. .'lNMMMMMMMMMMMMMMMMMMWKo. .xNMMMMMMMMMMMMK,
'OMMMMMMMMMMMXl. .cd0WMMMMMMMMWOOX0o'..';::. ',lNMMMMMMMMMMMMMMMWWXkl;. .dNMMMMMMMMMMMMK,
.kWMMMMMMMMMMXo. .;OWMMMMMMMMNxc:'. l0XWMMMMMMMMMMMMMWKxd:.. .dWMMMMMMMMMMMMK,
.kWMMMMMMMMMMNd. .cOWMMMMMMMMWNo ,OWMMMMMMMMMMMMWKxl:. .dNMMMMMMMMMMMMK,
.xNMMMMMMMMMMNx. 'kNMMMMMMMMMXc ';xWMMMMMMMMMMMKo:,. .dNMMMMMMMMMMMMK,
.dNMMMMMMMMMMWk. 'oXMMMMMMMMK: .l0XMMMMMMMMMMMXkc. .dNMMMMMMMMMMMMK,
.oXMMMMMMMMMMWO' .oXMMMMMMMWO, .c0MMMMMMMMMMWOol, .dNMMMMMMMMMMMMK,
.lXMMMMMMMMMMW0, 'ONWMMMMMMWk. ';',l0WMMMMMMMMMNOx: .dNMMMMMMMMMMMMK,
.cKMMMMMMMMMMMK, 'coKMMMMMMWd. .'... ;xxdccOWXKKNMMMMMMMMMNOo;. .dNMMMMMMMMMMMMK,
:KMMMMMMMMMMMX; .cKMMMMMMWx'. ... .clc;,:kKKOkKWWMWWMMMMMMMMMMMMMMMXko:' .dNMMMMMMMMMMMMK,
;0MMMMMMMMMXxc. .xXWMMMMMMNKo. .::;'..,d0OxddONWNNXNMMMMMMMMMMMMMMMMMMMMMMMMMMWXkl:' .dNMMMMMMMMMMMMK,
'0MMMMMMMMMO. .clOMMMMMMMXxc:o0WNXXK0KWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXk: .dNMMMMMMMMMMMMK,
.OMMMMMMMMMO. ;0MMMMMMMMWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMNKKXXNNWWWWMMKocclloodxko. .oNMMMMMMMMMMMMK,
.:dKMMMMMMMMMO' .oXWMMMMMMMMMMMMMMMMMMWNWWMMMMMMMMKxkO0KKOc'.',;::ccll; .oNMMMMMMMMMMMMK,
;KMMMMMMMMMMM0, .:oOWMMMMMMMMWKOO0KXXNk;:KMMMMMMMM0oddl,... .oNMMMMMMMMMMMMK,
,0WMMMMMMMMMM0, ;kXWMMMMXkOk, ....',. ;0MMMMMMMMMMN0dc:,. .oNMMMMMMMMMMMMK,
'kWMMMMMMMMMM0; ,cxNMMWKd,. .lXMMMMMMMMMMMWNNNO, .oNMMMMMMMMMMMMK,
.xNMMMMMMMMMMK; 'oOKX0o, .xNMMMMMMMMMMMMMMMO' .oNMMMMMMMMMMMM0,
.dNMMMMMMMMMMK: .,,,,. 'OWMMMMMMMMMMMMMMMK: .oNMMMMMMMMMMMMK,
.lXMMMMMMMMMMK: '0MMMMMMMMMMMMMW0dkl. .oNMMMMMMMMMMMMNd.
:XMMMMMMMMMMKc. '0MMMMMMMMMMMMMWd''. .''.'',. .oNMMMMMMMMMMMMMW0;
;KMMMMMMMMMMXc. .,l0MMMMMMMMMMMM0x; .. .cc,'dXXXXXOl:c:. .oNMMMMMMMMMMMMMMNl
.cxNMMMMMMMMMMXl. .dMMMMMMMMMMWOlc'.,:;'.;k0xdkNWNNWMMMMMWNNN0kl,'. .lNMMMMMMMMMMMMMMNc
cNMMMMMMMMMMMMXo. .''....xMMMMMMMMMMNxclccxXNXKXWMMMMMMMMMMMMMMMMMMMMN0xc. .lNMMMMMMMMMMMMMMXc
;KWMMMMMMMMMMMXo. ......... .:oolcc:;,'.;ONXXKOkKMMMMMMMMMMMWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMM0c:. .lNMMMMMMMMMMMMMMX:
,OWMMMMMMMMMMMNd. ..:OXXXXK0OOkxxdooldXMMWWWWNNNXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNWWWWWMKxddkOKx;;. lNMMMMMMMMMMMMMWK;
'kWMMMMMMMMMMMNd. ;ONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXOkO0KXNWXo,;:clodo, ... lNMMMMMMMMMMMMMW0;
.dNMMMMMMMMMMMWx. .lx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKKNMMMMMMMMMMWOoloodxkOx;. ...';,. lNMMMMMMMMMMMMMW0,
.lNMMMMMMMMMMMWk. .;dOO0WMMMMMMMMMWWWMMW0dddxkkOO0KKXNk,'kMMMMMMMMMMW0; . lNMMMMMMMMMMMMMWO'
cNMMMMMMMMMMMMO' ..;kWMMMMMMMMWkcodxd, ........ .kMMMMMMMMMMMNc cNMMMMMMMMMMMMMWk'
,d0WMMMMMMMMMMNXO' 'kWMMMMMMMMWd. .kMMMMMMMMMXd;. :XMMMMMMMMMMMMMWx.
lNMMMMMMMMMMMMk;;. .oNMMMMMMMMNO; .xMMMMMMMMMK, 'ckNMMMMMMMMMMMMMNd.
:KMMMMMMMMMMMMx. ;lxNMMMMMMMNxl, .xMMMMMMMMMK: dWMMMMMMMMMMMMMMMNo.
;0WMMMMMMMMMMMk. ;0WMMMMMMMWXd' .xMMMMMMMMMXc oNMMMMMMMMMMMMMMMNl.
'kWMMMMMMMMMMMk. ,oKWMMMMMMWXd' .xMMMMMMMMMXo. lNMMMMMMMMMMMMMMMNc
.dWMMMMMMMMMMMO' 'lKMMMMMMMMKdc. .xMMMMMMMMMNd. .'.... ..'. cXMMMMMMMMMMMMMMMXc
.'xWMMMMMMMMMMM0, ,okXMMMMMMMKoc. .xMMMMMMMMMNk,...... ;olc:;,'.:0NXK0OOkkkO00xc;... cXMMMMMMMMMMMMMMMNl..
.oKWMMMMMMMMMMMM0; ,dkXMMMMMMMW0:. .xMMMMMMMMMWXKK000Okkxddlo0MWWWNNNXNWMMMMMMMMMMMWNXOdoc;;' :KMMMMMMMMMMMMMMMMNXl
.oNMMMMMMMMMMMMMK: 'lkNMMMMMMMWO;. .''... 'dxdolc:ckNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKK0dc. :KMMMMMMMMMMMMMMMMMWo
cKMMMMMMMMMMMMMXc. ..lKWMMMMMMMMWk:;:loc:;,'cOXK0Okx0WMMMMWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKxc. ;0MMMMMMMMMMMMMMMMMNl
,0MMMMMMMMMMMMMXl. .;o0WMMMMMMMMMMW0xkXMWWNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM0dl. ;0MMMMMMMMMMMMMMMMMXl
.kMMMMMMMMMMMMMNo. .o0NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNOOO000KKXXNNNNNNNXXXXXKKKK000000000000000000000ko:. ,OWMMMMMMMMMMMMMMMMXc
.cxXMMMMMMMMMMMMMWd. .oxKMMMMMMMMMMMMMMMMMMMMMMMMMMWKKXXNNWWWOlooo0MMMMMMMMO' .......'',,,,'''''............................. ,OWMMMMMMMMMMMMMMMMK:
.kWMMMMMMMMMMMMMMWx. .cx0NMMMMMMMMMMMMWWWWWMXkxk0K0l'.',;:cl' .xMMMMMMMM0, 'OWMMMMMMMMMMMMMMMMK:
.lXMMMMMMMMMMMMWKkc. .,coONMMMMMN0OKN0c;codc. .... .xMMMMMMMMK: 'kWMMMMMMMMMMMMMMMM0;
;0MMMMMMMMMMMMWo .':xXWW0doc'.,. .xMMMMMMMMXl. 'kWMMMMMMMMMMMMMMMW0;
.,;OMMMMMMMMMMMMWo. .;::c, .xMMMMMMMMNd. .kWMMMMMMMMMMMMMMMWO,
,OKNMMMMMMMMMMMMWd. .kMMMMMMMMWk. .xWMMMMMMMMMMMMMMMWO,
.xNMMMMMMMMMMMMMWx. .kMMMMMMM0do. .xWMMMMMMMMMMMMMMMWO'
.cXMMMMMMMMMMMMMWk' 'OMMMMMMMk. .dWMMMMMMMMMMMMMMMWk'
,odXMMMMMMMMMMMMMWO, ,0MMMMMMM0; .oWMMMMMMMMMMMMMMMWk'
;0WMMMMMMMMMMMMMMM0; :KMMMMMMNKo. ..oWMMMMMMMMMMMMMMMWk.
.xNMMMMMMMMMMMMMMMXc .lXMMMMMWkc;. .oxOWMMMMMMMMMMMMMMMWx.
..lNMMMMMMMMMMMMMMMNl .d0XMMMWKk: .xWMMMMMMMMMMMMMMMMMWx.
ck0WMMMMMMMMMMMMMNkdc ';xWMWKd:. .oNMMMMMMMMMMMMMMMMMWd.
,OWMMMMMMMMMMMMMMNc. ;dOXKo:. .:c:;,,'''''',;;;. :KMMMMMMMMMMMMMMMMMWd.
.'dWMMMMMMMMMMMMMMNd. ..;dxl. ','''........ .dkxxdolc:;dNWWNNNXXXXXXNNNOlcol..'. ,OMMMMMMMMMMMMMMMMMWd.
.dOKMMMMMMMMMMMMMMMWk' ....... .:;. .cdoolllcc::;;,,''.,dKNNXXXKK00OOOkxxONMMMMMMMWWMMMMMMMMMMMMMMMMWWWMN0Okoc;. .;OMMMMMMMMMMMMMMMMMWo.
.lXMMMMMMMMMMMMMMMMW0; .;cc::;;,,''...... ,kKK00OOkkxxddoollxNMMMWWWWWWWNNNNNXXXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNXkc,,..dKWMMMMMMMMMMMMMMMMMWo.
;0MMMMMMMMMMMMMMMMMX: .','''....... .cxkkxxddoollcc::;ckNMWWWNNNXXXKKK000KWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWK0OolxXMMMMMMMMMMMMMMMMMMWo.
.kMMMMMMMMMMMMMMMNOxo;,''..lXWNNXXKK000OOkkxxdONMMMMMMMMMMMWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWMMMMMMMMMMMMMMMMMMMWd.
.,,xMMMMMMMMMMMMMMMWNNNNNNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNWWWWWWWWWWWWWWWWWWWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWd.
'OXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0kOOO000KKXXXNNNNWWWWO;',,;;:::cclllloooollllllldKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWx.
.xKNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKKKXXXXNNNNWWWWWMM0l:cclllooddxxkkkOO00x' .......''',,,;;;:. .cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMWk'
.,OMMMMMMMMMMMMMMMMWNWMMMMMMMMMMMMMMMMMMMMWWNNWWWWWWWWWMMMMMNkooddxxkkOO000KKXXNXo.....'',,;;;::ccll, ... .cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMW0;
cKMMMMMMMMMMMMMMMNOx0NXK00OO0000KKXXXNNWWKl',;;::cclloooddxl. ............ .:d0WMMMMMMMMMMMMMMMMMMMMMMMMWKOl
'oKWMMMMMMMMMMMMKddl;,.............'',,;;,. .:d0WMMMMMMMMMMMMMMMMMMMMMMMNo,.
.l0WMMMMMMMMMMM0,. .:o0WMMMMMMMMMMMMMMMMMMMMMMNO,
.lKWMMMMMMMMMMXc. .:o0MMMMMMMMMMMMMMMMMMMMMXxo,
.lxXMMMMMMMMNKx. .:d0WMMMMMMMMMMMMMMMMMMKdc.
.,xXWMMMMMWkc;. .:oOWMMMMMMMMMMMMMMMMKd:.
;dkKWMMWkc, .,xNMMMMMMMMMMMMMMMXx:.
.:lxKkoo; .:kNMMMMMMMMMMMMMNx;'
.;;. 'okXMMMMMMMMMMN0k:
,oONMMMMMWK0klc,.
.,cd0XXNXkc,.
.,;,',;.
`;
export const bitverseArt = `
.*/////*
.((((((/
.((((((/
...... .((((((/ .......
*((((((* .((((((/ .(((((((.
*((((((*. .((((((/ .(((((((.
....... *((((((*. .((((((/ ,(((((((. ......
,(((((((* *((((((*. .((((((/ ,(((((((. ,(((((((/.
,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/.
,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/.
*((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
*((((((*. ,(((((((* *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
*((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
/((((((, *((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((. .*((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. ,((((((/. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. .*((((((((/, ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. .*((((((((((/, ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. ,/(((((((((((((*. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. ,((((((((((((((((*. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((/. ,(((((((/ *((((((*. .*(((((((((((((((((((, .(((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((*. ./(((((((((((((((((((((, ,/((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *(((((((/*.*((((((((/(((((((((((((((/,,/((((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ *((((((((((((((((((*,((((((/,/((((((((((((((((((. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ .*((((((((((((((/,..((((((/..*((((((((((((((/, ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ .*(((((((((((/. .((((((/. *(((((((((((/,. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ ./((((((((*. .((((((/ ./((((((((,. ,(((((((/. ,(((((((. ./((((((,
.(((((((, *((((((*. ,(((((((/ .*((((((((,. .((((((/ ./(((((((/, ,(((((((/. ,(((((((. ./((((((,
.(((((((, .*(((((((/. ,(((((((/ .((((((((/. .((((((/ ,(((((##(/. ,(((((((/. ,((((((((,. ./((((((,
.(((((((, ./(((((((((*. ,(((((((/. ,((((((((*. .((((((/ *((((((((*. ,(((((((/. ,(((((((((/,. ./((((((,
.(((((((, ,/(#(((((((/. ,((((((((/(((((((((((((((((((((, .((((((/ ./(((((((((((((((((((((/(((((((/. .,((((((((((*. ./((((((,
.(((((((, ,/(((((((((/, ,((((((((((((((((((#((((((((((, .((((((/ .*((((((((((((((((((((((((((((/. .*((((((((((*. ./((((((,
.(((((((,..*((((((((((*. .,(((((((((((((((((((((((((((/. .((((((/ .,(((((((((((((((((((((((((((/. ,/(((((((((/.../((((((,
.(((((((**((((((((((/. ,(((((((((((((((((((((((((((*. .((((((/ ,(((((((((((((((((((((((((((/. ,((((((((((/*/((((((,
.(((((((((((((((#/,. ,/(((((((((/,. ./((((((*. .((((((/ ,(((((((,. .*((((((((((*. .*((((((((((((((((,
.(((((((((((((((*. .*((((((((#(*. /((((((*. .((((((/ ,(((((((. ,/(((((((((/,. ,/((((((((((((((,
.((((((((((((/. ,/(((((((((/. /((((((*. .((((((/ ,(((((((. .*((((((((((*. .*((((((((((((,
.((((((((((/, ,/(((((((((/,. *((((((*. .((((((/ .(((((((. .*((((((((((*. .*((((((((((,
.((((((((*. .*((((((((((*. *((((((*. .((((((/ ,(((((((. ,/(((((((((/, ,/(((((((,
.(((((((,. .,/(((((((((/. *((((((*. .((((((/ ,(((((((. .,((((((((((*. ./((((((,
.(((((#(, ./((((((((((,. *((((((*. .((((((/ ,(((((((. ./((((((((((,. ./((((((,
.(((((((, .*((((((((#(*. *((((((*. .((((((/ ,(((((((. ./(((((((((/, .*((((((,
.(((((((, ,/(((((((((/. *((((((*. .((((((/ ,(((((((. .*((((((((((*. ./((((((,
.(((((((, ./(((((((((/, *((((((*. .((((((/ ,(((((((. .*((((((((((* ./((((((,
.(((((((, *((((((((*. ./(((((((*. .(((#((/ ,((((((((*. ,/((((((((. ./((((((,
.(((((((, *((((((/, ,/((((((((, .(((#((/ ./((((((((*. .*(((((((. ./((((((,
.(((((((, *((((((/. ./((((((((*. .((((((/. ,(((((((((,. ,(((((((. ./((((((,
.(((((((, *((((((*. ,/((((((((*. ,((((((((*. ,(((((((((*. ,((#((((. ./((((((,
.(((((((, *((((((/. .*((((((((/. .*(#(((((((#/, ,((((((((/, ,(((((((. ./((((((,
.(((((((, *((((((/. ,/((((((((*. ,/(((((((((((((*. ,/((((((((,. ,(((((((. ./((((((,
.(((((((, *((((((/. .*((((((((/. *((((((((((((((((/. *((((((((/,......................*(((((((. ./((((((,
.(((((((, *((((((*. ./((((((((*. .*(((((((/,,/((((((((, ,(((((((((((((((((((((((((((((((((((((((. ./((((((,
.(((((((,. *((((((((((((((((((((((((((((((((((((/. .*((((((((/. .,(((((((#/, .*(((((((((((((((((((((((((((((((((((((. ./((((((,
.(((((((/,. *(((((((((((((((((((((((((((((((((((/. .*((((((#(*. ,(((((((((, .*((((((((((/(((((((((((((((((((((((((. .*(((((((,
.((((((((#(,. *((((((((((((((((((((((((((((((((((,. ,((((((((/, .*#(((((((*. ./((((((/. .,(((((((. .*(#(((((((,
.*((((((((((, *((((((/*,,,,,,,,,,,,,,,*,,*((((((/. .*((((((((*. ,((((((((/, .*((((((/ ,(((((((. ./(((((((((/,.
,/(((((((((/. *((((((*. .((((((/. ./(((((((/, .*((((((((, *((((((/ ,(((((((. .*((((((((((*.
.,((((((((((/. *((((((*. .((((((/. .(((((((/. .,(((((((, *((((((/ ,(((((((. .*((((((((((/.
,/((((((((#/,*((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((,*((((((((((*.
,/((((((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((((((((((((*.
./#(((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((((((((((,.
.*((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,((((((((((((/,
.*((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,((((((((((/,.
.,/(((((((*. .((((((/. ./((((((, ./((((((, .*((((((/ ,((((((((*,
.*((((((*. .((((((/. ./((((((*. ./((((((, .*((((((/ ,(((((((.
*((((((*. .((((((/. ./((((((*. .,/((((((, .*((((((/ ,(((((((.
*((((((*. .((((((/. ./(((((((/. .*((((((((, .*((((((/ ,(((((((.
*((((((/. .((((((/. ./(((((((((/,. .*((((((((((. *((((((/ ,(((((((.
*((((((*. .((((((/. ,/((((((((#/. .*((((((((((*. *((((((/ .,(((((((.
*(((((((/, .((((((/. .*((((((((((*..,/(((((((((/, .*((((((/ .*((((((((.
,((((((((((*. .((((((/. ,/(((((((((((((((((((*. .*((((((/ ./(((((((((/.
.*((((((((#(,. .((((((/. ,/(((((((((((((#(*. .*((((((/ ./(((((((((/,
.*((((((((((*. .((((((/. ,/(((((((((((*. .*((((((/ ./(((((((((/,
.,((((((((((*. .((((((/. .*(((((((,. .*((((((/ .,/(((((((((/.
.*((((((((((*. .((((((/. .((((((/ .*((((((/ ,/(((((((((/,
,/(((((((*. .((((((/. .((((((/ *((((((/ ./(((((((*.
.,/(((*. ..... .... .... ,/(((*.
.. ..
`;

@ -0,0 +1,344 @@
import {
bitverseBoardShape,
Board,
BoardState,
Move,
Neighbor,
opponents,
PlayerColor,
playerColors,
PointState,
validityReason,
} from "./goConstants";
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
import {
evaluateIfMoveIsValid,
findAllCapturedChains,
findLibertiesForChain,
getAllChains,
getBoardFromSimplifiedBoardState,
} from "../boardAnalysis/boardAnalysis";
import { endGoGame } from "../boardAnalysis/scoring";
import { cloneDeep } from "lodash";
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
/**
* Generates a new BoardState object with the given opponent and size
*/
export function getNewBoardState(
boardSize: number,
ai = opponents.Netburners,
applyObstacles = false,
boardToCopy?: Board,
): BoardState {
if (ai === opponents.w0r1d_d43m0n) {
boardToCopy = resetCoordinates(rotate90Degrees(getBoardFromSimplifiedBoardState(bitverseBoardShape).board));
}
const newBoardState = {
history: [],
previousPlayer: playerColors.white,
ai: ai,
passCount: 0,
cheatCount: 0,
board: Array.from({ length: boardSize }, (_, x) =>
Array.from({ length: boardSize }, (_, y) =>
!boardToCopy || boardToCopy?.[x]?.[y]
? {
player: boardToCopy?.[x]?.[y]?.player ?? playerColors.empty,
chain: "",
liberties: null,
x,
y,
}
: null,
),
),
};
if (applyObstacles) {
addObstacles(newBoardState);
}
const handicap = getHandicap(newBoardState.board[0].length, ai);
if (handicap) {
applyHandicap(newBoardState, handicap);
}
return newBoardState;
}
/**
* Determines how many starting pieces the opponent has on the board
*/
export function getHandicap(boardSize: number, opponent: opponents) {
// Illuminati and WD get a few starting routers
if (opponent === opponents.Illuminati || opponent === opponents.w0r1d_d43m0n) {
return ceil(boardSize * 0.35);
}
return 0;
}
/**
* Make a new move on the given board, and update the board state accordingly
*/
export function makeMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
// Do not update on invalid moves
const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
if (validity !== validityReason.valid || !boardState.board[x][y]?.player) {
console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
return false;
}
boardState.history.push(getBoardCopy(boardState).board);
boardState.history = boardState.history.slice(-4);
const point = boardState.board[x][y];
if (!point) {
return false;
}
point.player = player;
boardState.previousPlayer = player;
boardState.passCount = 0;
return updateCaptures(boardState, player);
}
/**
* Pass the current player's turn without making a move.
* Ends the game if this is the second pass in a row.
*/
export function passTurn(boardState: BoardState, player: playerColors, allowEndGame = true) {
if (boardState.previousPlayer === null || boardState.previousPlayer === player) {
return;
}
boardState.previousPlayer =
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
boardState.passCount++;
if (boardState.passCount >= 2 && allowEndGame) {
endGoGame(boardState);
}
}
/**
* Makes a number of random moves on the board before the game starts, to give one player an edge.
*/
export function applyHandicap(boardState: BoardState, handicap: number) {
const availableMoves = getEmptySpaces(boardState);
const handicapMoveOptions = getExpansionMoveArray(boardState, playerColors.black, availableMoves);
const handicapMoves: Move[] = [];
// select random distinct moves from the move options list up to the specified handicap amount
for (let i = 0; i < handicap && i < handicapMoveOptions.length; i++) {
const index = floor(Math.random() * handicapMoveOptions.length);
handicapMoves.push(handicapMoveOptions[index]);
handicapMoveOptions.splice(index, 1);
}
handicapMoves.forEach((move: Move) => {
const point = boardState.board[move.point.x][move.point.y];
return move.point && point && (point.player = playerColors.white);
});
return updateChains(boardState);
}
/**
* Finds all groups of connected stones on the board, and updates the points in them with their
* chain information and liberties.
*/
export function updateChains(boardState: BoardState, resetChains = true) {
resetChains && clearChains(boardState);
for (let x = 0; x < boardState.board.length; x++) {
for (let y = 0; y < boardState.board[x].length; y++) {
const point = boardState.board[x][y];
// If the current point is already analyzed, skip it
if (!point || point.chain !== "") {
continue;
}
const chainMembers = findAdjacentPointsInChain(boardState, x, y);
const libertiesForChain = findLibertiesForChain(boardState, chainMembers);
const id = `${point.x},${point.y}`;
chainMembers.forEach((member) => {
member.chain = id;
member.liberties = libertiesForChain;
});
}
}
return boardState;
}
/**
* Assign each point on the board a chain ID, and link its list of 'liberties' (which are empty spaces
* adjacent to some point on the chain including the current point).
*
* Then, remove any chains with no liberties.
*/
export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerColor, resetChains = true): BoardState {
const boardState = updateChains(initialState, resetChains);
const chains = getAllChains(boardState);
const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
if (!chainsToCapture?.length) {
return boardState;
}
chainsToCapture?.forEach((chain) => captureChain(chain));
return updateChains(boardState);
}
/**
* Removes a chain from the board, after being captured
*/
function captureChain(chain: PointState[]) {
chain.forEach((point) => {
point.player = playerColors.empty;
point.chain = "";
point.liberties = [];
});
}
/**
* Removes the chain data from given points, in preparation for being recalculated later
*/
function clearChains(boardState: BoardState): BoardState {
for (const x in boardState.board) {
for (const y in boardState.board[x]) {
const point = boardState.board[x][y];
if (point && point.chain && point.liberties) {
point.chain = "";
point.liberties = null;
}
}
}
return boardState;
}
/**
* Finds all the pieces in the current continuous group, or 'chain'
*
* Iteratively traverse the adjacent pieces of the same color to find all the pieces in the same chain,
* which are the pieces connected directly via a path consisting only of only up/down/left/right
*/
export function findAdjacentPointsInChain(boardState: BoardState, x: number, y: number) {
const point = boardState.board[x][y];
if (!point) {
return [];
}
const checkedPoints: PointState[] = [];
const adjacentPoints: PointState[] = [point];
const pointsToCheckNeighbors: PointState[] = [point];
while (pointsToCheckNeighbors.length) {
const currentPoint = pointsToCheckNeighbors.pop();
if (!currentPoint) {
break;
}
checkedPoints.push(currentPoint);
const neighbors = findNeighbors(boardState, currentPoint.x, currentPoint.y);
[neighbors.north, neighbors.east, neighbors.south, neighbors.west]
.filter(isNotNull)
.filter(isDefined)
.forEach((neighbor) => {
if (neighbor && neighbor.player === currentPoint.player && !contains(checkedPoints, neighbor)) {
adjacentPoints.push(neighbor);
pointsToCheckNeighbors.push(neighbor);
}
checkedPoints.push(neighbor);
});
}
return adjacentPoints;
}
/**
* Finds all empty spaces on the board.
*/
export function getEmptySpaces(boardState: BoardState): PointState[] {
const emptySpaces: PointState[] = [];
boardState.board.forEach((column) => {
column.forEach((point) => {
if (point && point.player === playerColors.empty) {
emptySpaces.push(point);
}
});
});
return emptySpaces;
}
/**
* Makes a deep copy of the given board state
*/
export function getStateCopy(initialState: BoardState) {
const boardState = cloneDeep(initialState);
boardState.history = [...initialState.history];
boardState.previousPlayer = initialState.previousPlayer;
boardState.ai = initialState.ai;
boardState.passCount = initialState.passCount;
return boardState;
}
/**
* Makes a deep copy of the given BoardState's board
*/
export function getBoardCopy(boardState: BoardState) {
const boardCopy = getNewBoardState(boardState.board[0].length);
const board = boardState.board;
for (let x = 0; x < board.length; x++) {
for (let y = 0; y < board[x].length; y++) {
const pointToEdit = boardCopy.board[x][y];
const point = board[x][y];
if (!point || !pointToEdit) {
boardCopy.board[x][y] = null;
} else {
pointToEdit.player = point.player;
}
}
}
return boardCopy;
}
export function contains(arr: PointState[], point: PointState) {
return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
}
export function findNeighbors(boardState: BoardState, x: number, y: number): Neighbor {
const board = boardState.board;
return {
north: board[x]?.[y + 1],
east: board[x + 1]?.[y],
south: board[x]?.[y - 1],
west: board[x - 1]?.[y],
};
}
export function getArrayFromNeighbor(neighborObject: Neighbor): PointState[] {
return [neighborObject.north, neighborObject.east, neighborObject.south, neighborObject.west]
.filter(isNotNull)
.filter(isDefined);
}
export function isNotNull<T>(argument: T | null): argument is T {
return argument !== null;
}
export function isDefined<T>(argument: T | undefined): argument is T {
return argument !== undefined;
}
export function floor(n: number) {
return ~~n;
}
export function ceil(n: number) {
const floored = floor(n);
return floored === n ? n : floored + 1;
}

@ -0,0 +1,311 @@
import { getNewBoardState } from "./boardState";
import { FactionName } from "@enums";
export enum playerColors {
white = "White",
black = "Black",
empty = "Empty",
}
export enum validityReason {
pointBroken = "That node is offline; a piece cannot be placed there",
pointNotEmpty = "That node is already occupied by a piece",
boardRepeated = "It is illegal to repeat prior board states",
noSuicide = "It is illegal to cause your own pieces to be captured",
notYourTurn = "It is not your turn to play",
gameOver = "The game is over",
invalid = "Invalid move",
valid = "Valid move",
}
export enum opponents {
none = "No AI",
Netburners = FactionName.Netburners,
SlumSnakes = FactionName.SlumSnakes,
TheBlackHand = FactionName.TheBlackHand,
Tetrads = FactionName.Tetrads,
Daedalus = FactionName.Daedalus,
Illuminati = FactionName.Illuminati,
w0r1d_d43m0n = "????????????",
}
export const opponentList = [
opponents.Netburners,
opponents.SlumSnakes,
opponents.TheBlackHand,
opponents.Tetrads,
opponents.Daedalus,
opponents.Illuminati,
];
export const opponentDetails = {
[opponents.none]: {
komi: 5.5,
description: "Practice Board",
flavorText: "Practice on a subnet where you place both colors of routers.",
bonusDescription: "",
bonusPower: 0,
},
[opponents.Netburners]: {
komi: 1.5,
description: "Easy AI",
flavorText:
"The Netburners faction are a mysterious group with only the most tenuous control over their subnets. Concentrating mainly on their hacknet server business, IPvGO is not their main strength.",
bonusDescription: "increased hacknet production",
bonusPower: 1.3,
},
[opponents.SlumSnakes]: {
komi: 3.5,
description: "Spread AI",
flavorText:
"The Slum Snakes faction are a small-time street gang who turned to organized crime using their subnets. They are known to use long router chains snaking across the subnet to encircle territory.",
bonusDescription: "crime success rate",
bonusPower: 1.2,
},
[opponents.TheBlackHand]: {
komi: 3.5,
description: "Aggro AI",
flavorText:
"The Black Hand faction is a black-hat hacking group who uses their subnets to launch targeted DDOS attacks. They are famous for their unrelenting aggression, surrounding and strangling any foothold their opponents try to establish.",
bonusDescription: "hacking money",
bonusPower: 0.9,
},
[opponents.Tetrads]: {
komi: 5.5,
description: "Martial AI",
flavorText:
"The faction known as Tetrads prefers to get up close and personal. Their combat style excels at circling around and cutting through their opponents, both on and off of the subnets.",
bonusDescription: "strength, dex, and agility levels",
bonusPower: 0.7,
},
[opponents.Daedalus]: {
komi: 5.5,
description: "Mid AI",
flavorText:
"Not much is known about this shadowy faction. They do not easily let go of subnets that they control, and are known to lease IPvGO cycles in exchange for reputation among other factions.",
bonusDescription: "reputation gain",
bonusPower: 1.1,
},
[opponents.Illuminati]: {
komi: 7.5,
description: "Hard AI",
flavorText:
"The Illuminati are thought to only exist in myth. Said to always have prepared defenses in their IPvGO subnets. Provoke them at your own risk.",
bonusDescription: "faster hack(), grow(), and weaken()",
bonusPower: 0.7,
},
[opponents.w0r1d_d43m0n]: {
komi: 9.5,
description: "???",
flavorText: "What you have seen is only the shadow of the truth. It's time to leave the cave.",
bonusDescription: "hacking level",
bonusPower: 2,
},
};
export const boardSizes = [5, 7, 9, 13];
export type PlayerColor = playerColors.white | playerColors.black | playerColors.empty;
export type Board = (PointState | null)[][];
export type MoveOptions = {
capture: Move | null;
defendCapture: Move | null;
eyeMove: EyeMove | null;
eyeBlock: EyeMove | null;
pattern: PointState | null;
growth: Move | null;
expansion: Move | null;
jump: Move | null;
defend: Move | null;
surround: Move | null;
corner: PointState | null;
random: PointState | null;
};
export type Move = {
point: PointState;
oldLibertyCount: number | null;
newLibertyCount: number | null;
};
export type EyeMove = {
point: PointState;
createsLife: boolean;
};
export type BoardState = {
board: Board;
previousPlayer: PlayerColor | null;
history: Board[];
ai: opponents;
passCount: number;
cheatCount: number;
};
export type PointState = {
player: PlayerColor;
chain: string;
liberties: (PointState | null)[] | null;
x: number;
y: number;
};
/**
* "invalid" or "move" or "pass" or "gameOver"
*/
export enum playTypes {
invalid = "invalid",
move = "move",
pass = "pass",
gameOver = "gameOver",
}
export type Play = {
success: boolean;
type: playTypes;
x: number;
y: number;
};
export type Neighbor = {
north: PointState | null;
east: PointState | null;
south: PointState | null;
west: PointState | null;
};
export type goScore = {
White: { pieces: number; territory: number; komi: number; sum: number };
Black: { pieces: number; territory: number; komi: number; sum: number };
};
export const columnIndexes = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
type opponentHistory = {
wins: number;
losses: number;
nodes: number;
nodePower: number;
winStreak: number;
oldWinStreak: number;
highestWinStreak: number;
favor: number;
};
export function getGoPlayerStartingState(): {
previousGameFinalBoardState: BoardState | null;
boardState: BoardState;
status: { [o in opponents]: opponentHistory };
} {
const previousGame: BoardState | null = null;
return {
boardState: getNewBoardState(7),
status: {
[opponents.none]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.Netburners]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.SlumSnakes]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.TheBlackHand]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.Tetrads]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.Daedalus]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.Illuminati]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
[opponents.w0r1d_d43m0n]: {
wins: 0,
losses: 0,
nodes: 0,
nodePower: 0,
winStreak: 0,
oldWinStreak: 0,
highestWinStreak: 0,
favor: 0,
},
},
previousGameFinalBoardState: previousGame,
};
}
export const bitverseBoardShape = [
"########...########",
"######.#...#.######",
"###.#..#...#..#.###",
".#..#..#...#..#..#.",
".#.....#...#.....#.",
"...................",
"...................",
"...................",
"...................",
".....##.....##.....",
"....###.....###....",
"....##.......##....",
"....#.........#....",
".........#.........",
"#........#........#",
"##.......#.......##",
"##.......#.......##",
"###.............###",
"####...........####",
];

@ -0,0 +1,605 @@
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
export const pointStyle = makeStyles((theme: Theme) =>
createStyles({
hover: {},
valid: {},
priorPoint: {},
point: {
position: "relative",
height: "100%",
width: "100%",
"&$hover$valid:hover $innerPoint": {
outlineColor: theme.colors.white,
},
"&$hover$priorPoint $innerPoint": {
outlineColor: theme.colors.white,
},
"&$hover$priorPoint $priorStoneTrad$blackPoint": {
outlineColor: theme.colors.white,
display: "block",
},
"&$hover$priorPoint $priorStoneTrad$whitePoint": {
outlineColor: theme.colors.black,
display: "block",
},
"&$hover:hover $coordinates": {
display: "block",
},
"&:hover $broken": {
opacity: "0.4",
},
},
broken: {
backgroundImage: "repeating-radial-gradient(circle at 17% 32%, white, black 0.00085px)",
backgroundPosition: "center",
animation: `$static 5s linear infinite`,
opacity: "0",
margin: "8px",
borderRadius: "4px",
width: "83%",
height: "83%",
transition: "all 0.3s",
"& $coordinates": {
fontSize: "10px",
},
},
"@keyframes static": {
from: {
backgroundSize: "100% 100%",
},
to: {
backgroundSize: "200% 200%",
},
},
hideOverflow: {
overflow: "hidden",
},
traditional: {
"& $innerPoint": {
display: "none",
},
"& $broken": {
backgroundImage: "none",
backgroundColor: "black",
},
"& $tradStone": {
display: "block",
},
"& $liberty": {
backgroundColor: "black",
transition: "none",
"&:not($northLiberty):not($southLiberty):not($eastLiberty):not($westLiberty)": {
width: 0,
height: 0,
},
},
"& $northLiberty, & $southLiberty": {
width: "0.9px",
},
"& $eastLiberty, & $westLiberty": {
height: "0.9px",
},
"&$nineteenByNineteen": {
"& $blackPoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
},
"& $whitePoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
"& $coordinates": {
fontSize: "0.9vw",
},
},
"&$thirteenByThirteen": {
"& $blackPoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
},
"& $whitePoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
"& $coordinates": {
fontSize: "0.9vw",
},
},
"&$nineByNine": {
"& $blackPoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
},
"& $whitePoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
},
"&$sevenBySeven": {
"& $blackPoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
},
"& $whitePoint": {
"&:before": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
},
"& $coordinates": {
color: "black",
left: "15%",
},
"& $blackPoint ~ $coordinates": {
color: "white",
},
},
fiveByFive: {
"& $blackPoint": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
"& $whitePoint": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
sevenBySeven: {
"& $blackPoint": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(23px, 3vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
"& $whitePoint": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(25px, 3vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
nineByNine: {
"& $filledPoint": {
boxShadow: "0px 0px 30px hsla(0, 100%, 100%, 0.48)",
},
"& $blackPoint": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
"& $whitePoint": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
thirteenByThirteen: {
"& $filledPoint": {
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
},
"& $blackPoint": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
"& $whitePoint": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
nineteenByNineteen: {
"& $filledPoint": {
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
},
"& $blackPoint": {
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
"& $whitePoint": {
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
"& $innerPoint": {
width: "70%",
height: "70%",
margin: "15%",
},
},
tradStone: {
display: "none",
borderRadius: "50%",
margin: 0,
"&:before": {
zIndex: 2,
borderRadius: "50%",
bottom: 0,
content: '" "',
display: "block",
left: 0,
position: "absolute",
right: 0,
top: 0,
},
"&:after": {
boxShadow: "2.5px 4px 0.5em hsla(0, 0%, 0%, 0.5)",
zIndex: 1,
borderRadius: "50%",
bottom: 0,
content: '" "',
display: "block",
left: 0,
position: "absolute",
right: 0,
top: 0,
},
"&$blackPoint": {
position: "static",
outlineWidth: 0,
width: 0,
height: 0,
margin: 0,
"&:before": {
backgroundColor: "black",
backgroundImage:
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
},
},
"&$whitePoint": {
backgroundColor: "transparent",
width: 0,
height: 0,
margin: 0,
"&:before": {
backgroundColor: "hsla(0, 0%, 90%, 1)",
backgroundImage:
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
},
},
"&$emptyPoint": {
width: 0,
height: 0,
margin: 0,
backgroundColor: "transparent",
"&:before": {
display: "none",
},
"&:after": {
display: "none",
},
},
},
innerPoint: {
outlineStyle: "solid",
outlineWidth: "1px",
outlineColor: "transparent",
borderRadius: "50%",
width: "50%",
height: "50%",
margin: "25%",
position: "absolute",
},
emptyPoint: {
width: "10%",
height: "10%",
margin: "45%",
backgroundColor: "white",
position: "relative",
},
filledPoint: {
outlineStyle: "solid",
outlineWidth: "1px",
borderRadius: "50%",
position: "relative",
boxShadow: "0px 0px 40px hsla(0, 100%, 100%, 0.48)",
},
whitePoint: {
width: "70%",
height: "70%",
margin: "15%",
backgroundColor: "hsla(0, 0%, 85%, 1)",
outlineStyle: "none",
},
blackPoint: {
width: "70%",
height: "70%",
margin: "15%",
backgroundColor: "black",
outlineColor: "white",
},
fadeLoopAnimation: {
animation: `$fadeLoop 800ms ${theme.transitions.easing.easeInOut} infinite alternate`,
},
"@keyframes fadeLoop": {
"0%": {
opacity: 0.4,
},
"100%": {
opacity: 1,
},
},
liberty: {
position: "absolute",
transition: "all 0.5s ease-out",
backgroundColor: "transparent",
width: "2%",
height: "2%",
top: "50%",
left: "50%",
},
libertyWhite: {
backgroundColor: theme.colors.cha,
},
libertyBlack: {
backgroundColor: theme.colors.success,
},
northLiberty: {
width: "2%",
height: "54%",
top: "-3%",
left: "50%",
},
southLiberty: {
width: "2%",
height: "50%",
top: "50%",
left: "50%",
},
eastLiberty: {
width: "50%",
height: "2%",
top: "50%",
left: "50%",
},
westLiberty: {
width: "50%",
height: "2%",
top: "50%",
left: "0",
},
coordinates: {
color: "white",
fontFamily: `"Lucida Console", "Lucida Sans Unicode", "Fira Mono", Consolas, "Courier New", Courier, monospace, "Times New Roman"`,
fontSize: "calc(min(1.3vw, 12px))",
display: "none",
position: "relative",
top: "15%",
left: "8%",
zIndex: "10",
userSelect: "none",
},
priorStoneTrad: {
display: "none",
outlineStyle: "solid",
outlineWidth: "4px",
outlineColor: "transparent",
borderRadius: "50%",
width: "50%",
height: "50%",
margin: "25%",
background: "none",
position: "absolute",
zIndex: "10",
},
}),
);
export const boardStyles = makeStyles((theme: Theme) =>
createStyles({
tab: {
paddingTop: 0,
paddingBottom: 0,
whiteSpace: "pre",
height: "50px",
minHeight: "unset",
width: "210px",
},
gameboardWrapper: {
position: "relative",
width: "calc(min(100vw - 250px, 90vh - 150px, 800px))",
height: "calc(min(100vw - 250px, 90vh - 150px, 800px))",
padding: "5px",
},
boardFrame: {
position: "relative",
width: "752px",
},
statusPageGameboard: {
position: "relative",
width: "calc(min(400px, max(60vw - 250px, 300px)))",
height: "calc(min(400px, max(60vw - 250px, 300px)))",
},
historyPageGameboard: {
position: "relative",
width: "calc(min(300px, max(60vw - 250px, 250px)))",
height: "calc(min(300px, max(60vw - 250px, 250px)))",
},
statusPageScore: {
width: "400px",
paddingLeft: "20px",
},
factionStatus: {
padding: "10px",
margin: "10px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: theme.colors.success,
width: "320px",
},
board: {
width: "100%",
height: "100%",
position: "relative",
},
traditional: {
backgroundColor: "#ca973e",
},
opponentName: {
paddingTop: "3px",
paddingBottom: "5px",
paddingRight: "10px",
},
opponentLabel: {
padding: "3px 10px 5px 10px",
},
opponentTitle: {
padding: "10px 0 0 0",
},
flavorText: {
minHeight: "120px",
padding: "0px 12px",
},
link: {
textDecoration: "underline",
opacity: 0.7,
cursor: "pointer",
"&:hover": {
opacity: 1,
},
},
inlineFlexBox: {
display: "inline-flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
},
resetBoard: {
width: "200px",
},
buttonHighlight: {
borderStyle: "solid",
borderWidth: "8px",
borderColor: theme.colors.success,
padding: "0 12px",
width: "200px",
animation: `$fadeLoop 600ms ${theme.transitions.easing.easeInOut} infinite alternate`,
},
"@keyframes fadeLoop": {
"0%": {
opacity: 0.6,
},
"100%": {
opacity: 1,
},
},
scoreBox: {
display: "inline-flex",
flexDirection: "row",
whiteSpace: "pre",
padding: "10px 30px",
},
searchBox: {
maxWidth: "550px",
minHeight: "460px",
},
fiveByFive: {
height: "20%",
"& $fiveByFive": {
width: "20%",
height: "100%",
},
},
sevenBySeven: {
height: "14%",
"& $sevenBySeven": {
width: "14%",
height: "100%",
},
},
nineByNine: {
height: "11%",
"& $nineByNine": {
width: "11%",
height: "100%",
},
},
thirteenByThirteen: {
height: "7.5%",
"& $thirteenByThirteen": {
width: "7.5%",
height: "100%",
},
},
nineteenByNineteen: {
height: "5.2%",
"& $nineteenByNineteen": {
width: "5.2%",
height: "100%",
},
},
background: {
position: "absolute",
opacity: 0.09,
color: theme.colors.white,
fontFamily: "monospace",
fontSize: "calc(min(.65vh - 2px, 0.65vw - 2px))",
whiteSpace: "pre",
pointerEvents: "none",
paddingTop: "15px",
},
bitverseBackground: {
"&$background": {
fontSize: "calc(min(.83vh - 1px, 0.72vw, 7.856px))",
opacity: 0.11,
},
},
instructionScroller: {
height: "calc(100vh - 80px)",
overflowY: "scroll",
marginTop: "10px",
},
instructionBoard: {
width: "350px",
height: "350px",
},
instructionBoardWrapper: {
maxWidth: "400px",
minHeight: "485px",
marginRight: "20px",
},
instructionsBlurb: {
width: "60%",
minWidth: "500px",
marginRight: "20px",
},
translucent: {
opacity: 0.6,
},
cellNone: {
borderBottom: "none",
padding: 0,
margin: 0,
color: theme.colors.success,
},
cellBottomPadding: {
paddingBottom: "20px",
},
keyText: {
paddingTop: "10px",
color: theme.colors.int,
},
scoreModal: {
width: "400px",
},
centeredText: {
textAlign: "center",
},
}),
);

@ -0,0 +1,127 @@
import { Board, boardSizes, BoardState, PointState } from "./goConstants";
import { WHRNG } from "../../Casino/RNG";
import { Player } from "@player";
import { floor } from "./boardState";
type rand = (n1: number, n2: number) => number;
export function addObstacles(boardState: BoardState) {
const rng = new WHRNG(Player.totalPlaytime);
const random = (n1: number, n2: number) => n1 + floor((n2 - n1 + 1) * rng.random());
const shouldRemoveCorner = !random(0, 4);
const shouldRemoveRows = !shouldRemoveCorner && !random(0, 4);
const shouldAddCenterBreak = !shouldRemoveCorner && !shouldRemoveRows && random(0, 3);
const obstacleTypeCount = +shouldRemoveCorner + +shouldRemoveRows + +shouldAddCenterBreak;
const edgeDeadCount = random(0, (getScale(boardState.board) + 2 - obstacleTypeCount) * 1.5);
if (shouldRemoveCorner) {
boardState.board = addDeadCorners(boardState.board, random);
}
if (shouldAddCenterBreak) {
boardState.board = addCenterBreak(boardState.board, random);
}
boardState.board = randomizeRotation(boardState.board, random);
if (shouldRemoveRows) {
boardState.board = removeRows(boardState.board, random);
}
boardState.board = addDeadNodesToEdge(boardState.board, random, edgeDeadCount);
boardState.board = resetCoordinates(boardState.board);
}
export function resetCoordinates(board: Board) {
const size = board[0].length;
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const point = board[x]?.[y];
if (point) {
point.x = x;
point.y = y;
}
}
}
return board;
}
function getScale(board: Board) {
return boardSizes.indexOf(board[0].length);
}
function removeRows(board: Board, random: rand) {
const rowsToRemove = Math.max(random(-2, getScale(board)), 1);
for (let i = 0; i < rowsToRemove; i++) {
board[i] = board[i].map(() => null);
}
board = rotateNTimes(board, 3);
return board;
}
function addDeadNodesToEdge(board: Board, random: rand, maxPerEdge: number) {
const size = board[0].length;
for (let i = 0; i < 4; i++) {
const count = random(0, maxPerEdge);
for (let j = 0; j < count; j++) {
const yIndex = Math.max(random(-2, size - 1), 0);
board[0][yIndex] = null;
}
board = rotate90Degrees(board);
}
return board;
}
function addDeadCorners(board: Board, random: rand) {
const scale = getScale(board) + 1;
addDeadCorner(board, random, scale);
if (!random(0, 3)) {
board = rotate90Degrees(board);
board = rotate90Degrees(board);
addDeadCorner(board, random, scale - 2);
}
return randomizeRotation(board, random);
}
function addDeadCorner(board: Board, random: rand, size: number) {
let currentSize = size;
for (let i = 0; i < size && i < currentSize; i++) {
random(0, 1) && currentSize--;
board[i].forEach((point, index) => index < currentSize && point && (board[point.x][point.y] = null));
}
return board;
}
function addCenterBreak(board: Board, random: rand) {
const size = board[0].length;
const maxOffset = getScale(board);
const xIndex = random(0, maxOffset * 2) - maxOffset + floor(size / 2);
const length = random(1, floor(size / 2 - 1));
board[xIndex] = board[xIndex].map((point, index) => (index < length ? null : point));
return randomizeRotation(board, random);
}
function randomizeRotation(board: Board, random: rand) {
return rotateNTimes(board, random(0, 3));
}
function rotateNTimes(board: Board, rotations: number) {
for (let i = 0; i < rotations; i++) {
board = rotate90Degrees(board);
}
return board;
}
export function rotate90Degrees(board: Board) {
return board[0].map((_, index: number) => board.map((row: (PointState | null)[]) => row[index]).reverse());
}

140
src/Go/effects/effect.ts Normal file

@ -0,0 +1,140 @@
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { getGoPlayerStartingState, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
import { Player } from "@player";
import { defaultMultipliers, mergeMultipliers, Multipliers } from "../../PersonObjects/Multipliers";
import { PlayerObject } from "../../PersonObjects/Player/PlayerObject";
import { formatPercent } from "../../ui/formatNumber";
import { getPlayerStats } from "../boardAnalysis/scoring";
/**
* Calculates the effect size of the given player boost, based on the node power (points based on number of subnet
* nodes captured and player wins) and effect power (scalar for individual boosts)
*/
export function CalculateEffect(nodes: number, faction: opponents): number {
const power = getEffectPowerForFaction(faction);
const sourceFileBonus = Player.sourceFileLvl(14) ? 1.25 : 1;
return (
1 + Math.log(nodes + 1) * Math.pow(nodes + 1, 0.33) * 0.005 * power * currentNodeMults.GoPower * sourceFileBonus
);
}
export function getMaxFavor() {
const sourceFileLevel = Player.sourceFileLvl(14);
if (sourceFileLevel === 1) {
return 90;
}
if (sourceFileLevel === 2) {
return 100;
}
if (sourceFileLevel >= 3) {
return 120;
}
return 80;
}
/**
* Gets a formatted description of the current bonus from this faction
*/
export function getBonusText(opponent: opponents) {
const nodePower = getPlayerStats(opponent).nodePower;
const effectPercent = formatPercent(CalculateEffect(nodePower, opponent) - 1);
const effectDescription = getEffectTypeForFaction(opponent);
return `${effectPercent} ${effectDescription}`;
}
/**
* Update the player object, using the multipliers gained from node power for each faction
*/
export function updateGoMults(): void {
const mults = calculateMults();
Player.mults = mergeMultipliers(Player.mults, mults);
Player.updateSkillLevels();
}
/**
* Creates a multiplier object based on the player's total node power for each faction
*/
function calculateMults(): Multipliers {
const mults = defaultMultipliers();
[...opponentList, opponents.w0r1d_d43m0n].forEach((opponent) => {
if (!Player.go?.status?.[opponent]) {
Player.go = getGoPlayerStartingState();
}
const effect = CalculateEffect(getPlayerStats(opponent).nodePower, opponent);
switch (opponent) {
case opponents.Netburners:
mults.hacknet_node_money *= effect;
break;
case opponents.SlumSnakes:
mults.crime_success *= effect;
break;
case opponents.TheBlackHand:
mults.hacking_money *= effect;
break;
case opponents.Tetrads:
mults.strength *= effect;
mults.dexterity *= effect;
mults.agility *= effect;
break;
case opponents.Daedalus:
mults.company_rep *= effect;
mults.faction_rep *= effect;
break;
case opponents.Illuminati:
mults.hacking_speed *= effect;
break;
case opponents.w0r1d_d43m0n:
mults.hacking *= effect;
break;
}
});
return mults;
}
export function resetGoNodePower(player: PlayerObject) {
opponentList.forEach((opponent) => {
player.go.status[opponent].nodePower = 0;
player.go.status[opponent].nodes = 0;
player.go.status[opponent].winStreak = 0;
});
}
export function playerHasDiscoveredGo() {
const playedGame = Player.go.boardState.history.length || Player.go.previousGameFinalBoardState?.history?.length;
const hasRecords = opponentList.find((opponent) => getPlayerStats(opponent).wins + getPlayerStats(opponent).losses);
const isInBn14 = Player.bitNodeN === 14;
// TODO: remove this once testing is completed
const isInTesting = true;
return !!(playedGame || hasRecords || isInBn14 || isInTesting);
}
function getEffectPowerForFaction(opponent: opponents) {
return opponentDetails[opponent].bonusPower;
}
export function getEffectTypeForFaction(opponent: opponents) {
return opponentDetails[opponent].bonusDescription;
}
export function getWinstreakMultiplier(winStreak: number, previousWinStreak: number) {
if (winStreak < 0) {
return 0.5;
}
// If you break a dry streak, gain extra bonus based on the length of the dry streak (up to 5x bonus)
if (previousWinStreak < 0 && winStreak > 0) {
const dryStreakBroken = -1 * previousWinStreak;
return 1 + 0.5 * Math.min(dryStreakBroken, 8);
}
// Win streak bonus caps at x3
return 1 + 0.25 * Math.min(winStreak, 8);
}
export function getDifficultyMultiplier(komi: number, boardSize: number) {
const isTinyBoardVsIlluminati = boardSize === 5 && komi === opponentDetails[opponents.Illuminati].komi;
return isTinyBoardVsIlluminati ? 8 : (komi + 0.5) * 0.25;
}

@ -0,0 +1,411 @@
import { BoardState, opponentList, Play, playerColors, playTypes, validityReason } from "../boardState/goConstants";
import { getMove, sleep } from "../boardAnalysis/goAI";
import { Player } from "@player";
import {
getNewBoardState,
getStateCopy,
makeMove,
passTurn,
updateCaptures,
updateChains,
} from "../boardState/boardState";
import { evaluateIfMoveIsValid, getControlledSpace, getSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { WHRNG } from "../../Casino/RNG";
/**
* 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(Player.go.boardState, playerColors.black);
if (Player.go.boardState.previousPlayer === null) {
logEndGame(logger);
return Promise.resolve({
type: playTypes.gameOver,
x: -1,
y: -1,
success: true,
});
}
return getAIMove(logger, Player.go.boardState);
}
/**
* Validates and applies the player's router placement
*/
export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) {
const validity = evaluateIfMoveIsValid(Player.go.boardState, x, y, playerColors.black);
const result = makeMove(Player.go.boardState, x, y, playerColors.black);
if (validity !== validityReason.valid || !result) {
await sleep(500);
logger(`ERROR: Invalid move: ${validity}`);
return Promise.resolve(invalidMoveResponse);
}
logger(`Go move played: ${x}, ${y}`);
const playerUpdatedBoard = getStateCopy(result);
return getAIMove(logger, playerUpdatedBoard);
}
/**
* Retrieves a move from the current faction in response to the player's move
*/
async function getAIMove(logger: (s: string) => void, boardState: BoardState, success = true): Promise<Play> {
let resolve: (value: Play) => void;
const aiMoveResult = new Promise<Play>((res) => {
resolve = res;
});
getMove(boardState, playerColors.white, Player.go.boardState.ai).then(async (result) => {
// If a new game has started while this async code ran, drop it
if (boardState.history.length > Player.go.boardState.history.length) {
return resolve({ ...result, success: false });
}
if (result.type === "gameOver") {
logEndGame(logger);
}
if (result.type !== playTypes.move) {
Player.go.boardState = boardState;
return resolve({ ...result, success });
}
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, playerColors.white);
if (!aiUpdatedBoard) {
boardState.previousPlayer = playerColors.white;
Player.go.boardState = boardState;
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
} else {
Player.go.boardState = aiUpdatedBoard;
logger(`Opponent played move: ${result.x}, ${result.y}`);
}
await sleep(200);
resolve({ ...result, success });
});
return aiMoveResult;
}
/**
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
*/
export function getValidMoves() {
const boardState = Player.go.boardState;
// 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, playerColors.black) === validityReason.valid;
validityArray.push(isValid);
return validityArray;
}, []),
);
}
/**
* Returns a grid with an ID for each contiguous chain of same-state nodes (excluding dead/offline nodes)
*/
export function getChains() {
const chains: string[] = [];
// Turn the internal chain IDs into nice consecutive numbers for display to the player
return Player.go.boardState.board.map((column) =>
column.reduce((chainIdArray: (number | null)[], point) => {
if (!point) {
chainIdArray.push(null);
return chainIdArray;
}
if (!chains.includes(point.chain)) {
chains.push(point.chain);
}
chainIdArray.push(chains.indexOf(point.chain));
return chainIdArray;
}, []),
);
}
/**
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
*/
export function getLiberties() {
return Player.go.boardState.board.map((column) =>
column.reduce((libertyArray: number[], point) => {
libertyArray.push(point?.liberties?.length || -1);
return libertyArray;
}, []),
);
}
/**
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
*/
export function getControlledEmptyNodes() {
const boardState = Player.go.boardState;
const controlled = getControlledSpace(boardState);
return controlled.map((column, x: number) =>
column.reduce((ownedPoints: string, owner: playerColors, y: number) => {
if (owner === playerColors.white) {
return ownedPoints + "O";
}
if (owner === playerColors.black) {
return ownedPoints + "X";
}
if (!boardState.board[x][y]) {
return ownedPoints + "#";
}
if (boardState.board[x][y]?.player === playerColors.empty) {
return ownedPoints + "?";
}
return ownedPoints + ".";
}, ""),
);
}
/**
* Handle post-game logging
*/
function logEndGame(logger: (s: string) => void) {
const boardState = Player.go.boardState;
const score = getScore(boardState);
logger(
`Subnet complete! Final score: ${boardState.ai}: ${score[playerColors.white].sum}, Player: ${
score[playerColors.black].sum
}`,
);
}
/**
* Clears the board, resets winstreak if applicable
*/
export function resetBoardState(error: (s: string) => void, opponentString: string, boardSize: number) {
const opponent = opponentList.find((faction) => faction === opponentString);
if (![5, 7, 9, 13].includes(boardSize)) {
error(`Invalid subnet size requested (${boardSize}, size must be 5, 7, 9, or 13`);
return;
}
if (!opponent) {
error(`Invalid opponent requested (${opponentString}), valid options are ${opponentList.join(", ")}`);
return;
}
const oldBoardState = Player.go.boardState;
if (oldBoardState.previousPlayer !== null && oldBoardState.history.length) {
resetWinstreak(oldBoardState.ai, false);
}
Player.go.boardState = getNewBoardState(boardSize, opponent, true);
return getSimplifiedBoardState(Player.go.boardState.board);
}
/** Validate singularity access by throwing an error if the player does not have access. */
export function checkCheatApiAccess(error: (s: string) => void): void {
const hasSourceFile = Player.sourceFileLvl(14) > 1;
const isBitnodeFourteenTwo = Player.sourceFileLvl(14) === 1 && Player.bitNodeN === 14;
if (!hasSourceFile && !isBitnodeFourteenTwo) {
error(
`The go.cheat API requires Source-File 14.2 to run, a power up you obtain later in the game.
It will be very obvious when and how you can obtain it.`,
);
}
}
export const invalidMoveResponse: Play = {
success: false,
type: playTypes.invalid,
x: -1,
y: -1,
};
/**
* Determines if the attempted cheat move is successful. If so, applies the cheat via the callback, and gets the opponent's response.
*
* If it fails, determines if the player's turn is skipped, or if the player is ejected from the subnet.
*/
export async function determineCheatSuccess(
logger: (s: string) => void,
callback: () => void,
successRngOverride?: number,
ejectRngOverride?: number,
): Promise<Play> {
const state = Player.go.boardState;
const rng = new WHRNG(Player.totalPlaytime);
// If cheat is successful, run callback
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
callback();
state.cheatCount++;
return getAIMove(logger, state, true);
}
// 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) {
logger(`Cheat failed! You have been ejected from the subnet.`);
resetBoardState(logger, state.ai, state.board[0].length);
return {
type: playTypes.gameOver,
x: -1,
y: -1,
success: false,
};
}
// If the cheat fails, your turn is skipped
else {
logger(`Cheat failed. Your turn has been skipped.`);
passTurn(state, playerColors.black, false);
state.cheatCount++;
return getAIMove(logger, state, false);
}
}
/**
* Cheating success rate scales with player's crime success rate, and decreases with prior cheat attempts.
*/
export function cheatSuccessChance(cheatCount: number) {
return Math.min(0.6 * (0.8 ^ cheatCount) * Player.mults.crime_success, 1);
}
/**
* Throw a runtime error that halts the player's script
*/
export function throwError(ws: WorkerScript, errorMessage: string) {
throw `RUNTIME ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n ${errorMessage}`;
}
/**
* Attempts to remove an existing router from the board. Can fail. If failed, can immediately end the game
*/
export function cheatRemoveRouter(
logger: (s: string) => void,
x: number,
y: number,
successRngOverride?: number,
ejectRngOverride?: number,
) {
const point = Player.go.boardState.board[x][y];
if (!point) {
logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`);
return invalidMoveResponse;
}
if (point.player === playerColors.empty) {
logger(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`);
return invalidMoveResponse;
}
return determineCheatSuccess(
logger,
() => {
point.player = playerColors.empty;
Player.go.boardState = updateChains(Player.go.boardState);
Player.go.boardState.previousPlayer = playerColors.black;
logger(`Cheat successful. The point ${x},${y} was cleared.`);
},
successRngOverride,
ejectRngOverride,
);
}
/**
* Attempts play two moves at once. Can fail. If failed, can immediately end the game
*/
export function cheatPlayTwoMoves(
logger: (s: string) => void,
x1: number,
y1: number,
x2: number,
y2: number,
successRngOverride?: number,
ejectRngOverride?: number,
) {
const point1 = Player.go.boardState.board[x1][y1];
if (!point1) {
logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`);
return invalidMoveResponse;
}
if (point1.player !== playerColors.empty) {
logger(`The point ${x1},${y1} is not empty, so you cannot place a router there.`);
return invalidMoveResponse;
}
const point2 = Player.go.boardState.board[x2][y2];
if (!point2) {
logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`);
return invalidMoveResponse;
}
if (point2.player !== playerColors.empty) {
logger(`The point ${x2},${y2} is not empty, so you cannot place a router there.`);
return invalidMoveResponse;
}
return determineCheatSuccess(
logger,
() => {
point1.player = playerColors.black;
point2.player = playerColors.black;
Player.go.boardState = updateCaptures(Player.go.boardState, playerColors.black);
Player.go.boardState.previousPlayer = playerColors.black;
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
},
successRngOverride,
ejectRngOverride,
);
}
export function cheatRepairOfflineNode(
logger: (s: string) => void,
x: number,
y: number,
successRngOverride?: number,
ejectRngOverride?: number,
) {
const point = Player.go.boardState.board[x][y];
if (point) {
logger(`The node ${x},${y} is not offline, so you cannot repair the node.`);
return invalidMoveResponse;
}
return determineCheatSuccess(
logger,
() => {
Player.go.boardState.board[x][y] = {
chain: "",
liberties: null,
y,
player: playerColors.empty,
x,
};
Player.go.boardState = updateChains(Player.go.boardState);
Player.go.boardState.previousPlayer = playerColors.black;
logger(`Cheat successful. The point ${x},${y} was repaired.`);
},
successRngOverride,
ejectRngOverride,
);
}
export function cheatDestroyNode(
logger: (s: string) => void,
x: number,
y: number,
successRngOverride?: number,
ejectRngOverride?: number,
) {
const point = Player.go.boardState.board[x][y];
if (!point) {
logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`);
return invalidMoveResponse;
}
if (point.player !== playerColors.empty) {
logger(`The point ${x},${y} is not empty, so you cannot destroy this node.`);
return invalidMoveResponse;
}
return determineCheatSuccess(
logger,
() => {
Player.go.boardState.board[x][y] = null;
Player.go.boardState = updateChains(Player.go.boardState);
Player.go.boardState.previousPlayer = playerColors.black;
logger(`Cheat successful. The point ${x},${y} was repaired.`);
},
successRngOverride,
ejectRngOverride,
);
}

73
src/Go/ui/GoGameboard.tsx Normal file

@ -0,0 +1,73 @@
import React, { useMemo } from "react";
import Grid from "@mui/material/Grid";
import { getSizeClass, GoPoint } from "./GoPoint";
import { useRerender } from "../../ui/React/hooks";
import { boardStyles } from "../boardState/goStyles";
import { getAllValidMoves, getControlledSpace } from "../boardAnalysis/boardAnalysis";
import { BoardState, opponents, playerColors } from "../boardState/goConstants";
interface IProps {
boardState: BoardState;
traditional: boolean;
clickHandler: (x: number, y: number) => any;
hover: boolean;
}
export function GoGameboard({ boardState, traditional, clickHandler, hover }: IProps): React.ReactElement {
useRerender(400);
const currentPlayer =
boardState.ai !== opponents.none || boardState.previousPlayer === playerColors.white
? playerColors.black
: playerColors.white;
const availablePoints = useMemo(
() => (hover ? getAllValidMoves(boardState, currentPlayer) : []),
[boardState, hover, currentPlayer],
);
const ownedEmptyNodes = useMemo(() => getControlledSpace(boardState), [boardState]);
function pointIsValid(x: number, y: number) {
return !!availablePoints.find((point) => point.x === x && point.y === y);
}
const boardSize = boardState.board[0].length;
const classes = boardStyles();
return (
<>
<Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}>
{boardState.board.map((row, y) => {
const yIndex = boardState.board[0].length - y - 1;
return (
<Grid container key={`column_${yIndex}`} item className={getSizeClass(boardSize, classes)}>
{row.map((point, x: number) => {
const xIndex = x;
return (
<Grid
item
key={`point_${xIndex}_${yIndex}`}
onClick={() => clickHandler(xIndex, yIndex)}
className={getSizeClass(boardSize, classes)}
>
<GoPoint
state={boardState}
x={xIndex}
y={yIndex}
traditional={traditional}
hover={hover}
valid={pointIsValid(xIndex, yIndex)}
emptyPointOwner={ownedEmptyNodes[xIndex]?.[yIndex]}
/>
</Grid>
);
})}
</Grid>
);
})}
</Grid>
</>
);
}

@ -0,0 +1,292 @@
import React, { useEffect, useMemo, useState } from "react";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
import { Box, Button, Typography } from "@mui/material";
import { BoardState, opponents, playerColors, playTypes, validityReason } from "../boardState/goConstants";
import { getNewBoardState, getStateCopy, makeMove, passTurn } from "../boardState/boardState";
import { getMove } from "../boardAnalysis/goAI";
import { bitverseArt, weiArt } from "../boardState/asciiArt";
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
import { useRerender } from "../../ui/React/hooks";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { boardStyles } from "../boardState/goStyles";
import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText";
interface IProps {
showInstructions: () => void;
}
// FUTURE: bonus time?
/*
// FUTURE: add AI cheating.
* unlikely unless player cheats first
* more common on some factions
* play two moves that don't capture
*/
export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactElement {
const rerender = useRerender(400);
const boardState = Player.go.boardState;
const traditional = Settings.GoTraditionalStyle;
const [showPriorMove, setShowPriorMove] = useState(false);
const [opponent, setOpponent] = useState<opponents>(boardState.ai);
const [scoreOpen, setScoreOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [waitingOnAI, setWaitingOnAI] = useState(false);
const classes = boardStyles();
const boardSize = boardState.board[0].length;
const currentPlayer = boardState.previousPlayer === playerColors.white ? playerColors.black : playerColors.white;
const score = getScore(boardState);
// Only run this once on first component mount, to handle scenarios where the game was saved or closed while waiting on the AI to make a move
useEffect(() => {
if (boardState.previousPlayer === playerColors.black && !waitingOnAI) {
takeAiTurn(Player.go.boardState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function clickHandler(x: number, y: number) {
if (showPriorMove) {
SnackbarEvents.emit(
`Currently showing a past board state. Please disable "Show previous move" to continue.`,
ToastVariant.WARNING,
2000,
);
return;
}
// Lock the board when it isn't the player's turn
const gameOver = boardState.previousPlayer === null;
const notYourTurn = boardState.previousPlayer === playerColors.black && opponent !== opponents.none;
if (notYourTurn) {
SnackbarEvents.emit(`It is not your turn to play.`, ToastVariant.WARNING, 2000);
return;
}
if (gameOver) {
SnackbarEvents.emit(`The game is complete, please reset to continue.`, ToastVariant.WARNING, 2000);
return;
}
const validity = evaluateIfMoveIsValid(boardState, x, y, currentPlayer);
if (validity != validityReason.valid) {
SnackbarEvents.emit(`Invalid move: ${validity}`, ToastVariant.ERROR, 2000);
return;
}
const updatedBoard = makeMove(boardState, x, y, currentPlayer);
if (updatedBoard) {
updateBoard(updatedBoard);
opponent !== opponents.none && takeAiTurn(updatedBoard);
}
}
function passPlayerTurn() {
if (boardState.previousPlayer === playerColors.white) {
passTurn(boardState, playerColors.black);
updateBoard(boardState);
}
if (boardState.previousPlayer === null) {
endGame();
return;
}
setTimeout(() => {
opponent !== opponents.none && takeAiTurn(boardState);
}, 100);
}
async function takeAiTurn(board: BoardState) {
if (board.previousPlayer === null) {
return;
}
setWaitingOnAI(true);
const initialState = getStateCopy(board);
const move = await getMove(initialState, playerColors.white, opponent);
// If a new game has started while this async code ran, just drop it
if (boardState.history.length > Player.go.boardState.history.length) {
return;
}
if (move.type === playTypes.pass) {
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
updateBoard(initialState);
return;
}
if (move.type === playTypes.gameOver || move.x === null || move.y === null) {
endGame(initialState);
return;
}
const updatedBoard = await makeMove(initialState, move.x, move.y, playerColors.white);
if (updatedBoard) {
setTimeout(() => {
updateBoard(updatedBoard);
setWaitingOnAI(false);
}, 500);
}
}
function newSubnet() {
setScoreOpen(false);
setSearchOpen(true);
}
function resetState(newBoardSize = boardSize, newOpponent = opponent) {
setScoreOpen(false);
setSearchOpen(false);
setOpponent(newOpponent);
if (boardState.previousPlayer !== null && boardState.history.length) {
resetWinstreak(boardState.ai, false);
}
const newBoardState = getNewBoardState(newBoardSize, newOpponent, false);
updateBoard(newBoardState);
}
function updateBoard(initialBoardState: BoardState) {
Player.go.boardState = getStateCopy(initialBoardState);
rerender();
}
function endGame(state = boardState) {
setScoreOpen(true);
updateBoard(state);
}
function getPriorMove() {
if (!boardState.history.length) {
return boardState;
}
const priorBoard = boardState.history.slice(-1)[0];
const updatedState = getStateCopy(boardState);
updatedState.board = priorBoard;
updatedState.previousPlayer =
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
return updatedState;
}
function showPreviousMove(newValue: boolean) {
if (boardState.history.length) {
setShowPriorMove(newValue);
}
}
function setTraditional(newValue: boolean) {
Settings.GoTraditionalStyle = newValue;
}
const endGameAvailable = boardState.previousPlayer === playerColors.white && boardState.passCount;
const noLegalMoves = useMemo(
() => boardState.previousPlayer === playerColors.white && !getAllValidMoves(boardState, playerColors.black).length,
[boardState],
);
const disablePassButton =
opponent !== opponents.none && boardState.previousPlayer === playerColors.black && waitingOnAI;
const scoreBoxText = boardState.history.length
? `Score: Black: ${score[playerColors.black].sum} White: ${score[playerColors.white].sum}`
: "Place a router to begin!";
const getPassButtonLabel = () => {
if (endGameAvailable) {
return "End Game";
}
if (boardState.previousPlayer === null) {
return "View Final Score";
}
if (boardState.previousPlayer === playerColors.black && waitingOnAI) {
return "Waiting for opponent";
}
const currentPlayer = boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
return `Pass Turn${boardState.ai === opponents.none ? ` (${currentPlayer})` : ""}`;
};
return (
<>
<GoSubnetSearch
open={searchOpen}
search={resetState}
cancel={() => setSearchOpen(false)}
showInstructions={showInstructions}
/>
<GoScoreModal
open={scoreOpen}
onClose={() => setScoreOpen(false)}
newSubnet={() => newSubnet()}
finalScore={score}
opponent={opponent}
></GoScoreModal>
<div className={classes.boardFrame}>
{traditional ? (
""
) : (
<div className={`${classes.background} ${boardSize === 19 ? classes.bitverseBackground : ""}`}>
{boardSize === 19 ? bitverseArt : weiArt}
</div>
)}
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<br />
<Typography variant={"h6"} className={classes.opponentLabel}>
{opponent !== opponents.none ? "Subnet owner: " : ""}{" "}
{opponent === opponents.w0r1d_d43m0n ? <CorruptableText content={opponent} /> : opponent}
</Typography>
<br />
</Box>
<div className={`${classes.gameboardWrapper} ${showPriorMove ? classes.translucent : ""}`}>
<GoGameboard
boardState={showPriorMove ? getPriorMove() : boardState}
traditional={traditional}
clickHandler={clickHandler}
hover={!showPriorMove}
/>
</div>
<Box className={classes.inlineFlexBox}>
<Button onClick={() => setSearchOpen(true)} className={classes.resetBoard}>
Find New Subnet
</Button>
<Typography className={classes.scoreBox}>{scoreBoxText}</Typography>
<Button
disabled={disablePassButton}
onClick={passPlayerTurn}
className={endGameAvailable || noLegalMoves ? classes.buttonHighlight : classes.resetBoard}
>
{getPassButtonLabel()}
</Button>
</Box>
<div className={classes.opponentLabel}>
<Box className={classes.inlineFlexBox}>
<br />
<OptionSwitch
checked={traditional}
onChange={(newValue) => setTraditional(newValue)}
text="Traditional Go look"
tooltip={<>Show stones and grid as if it was a standard Go board</>}
/>
<OptionSwitch
checked={showPriorMove}
disabled={!boardState.history.length}
onChange={(newValue) => showPreviousMove(newValue)}
text="Show previous move"
tooltip={<>Show the board as it was before the last move</>}
/>
</Box>
</div>
</div>
</>
);
}

140
src/Go/ui/GoHistoryPage.tsx Normal file

@ -0,0 +1,140 @@
import React from "react";
import Typography from "@mui/material/Typography";
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
import { opponentList, opponents } from "../boardState/goConstants";
import { getPlayerStats, getScore } from "../boardAnalysis/scoring";
import { Player } from "@player";
import { GoGameboard } from "./GoGameboard";
import { boardStyles } from "../boardState/goStyles";
import { useRerender } from "../../ui/React/hooks";
import { getBonusText, getMaxFavor } from "../effects/effect";
import { formatNumber } from "../../ui/formatNumber";
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
import { getNewBoardState } from "../boardState/boardState";
import { CorruptableText } from "../../ui/React/CorruptableText";
import { showWorldDemon } from "../boardAnalysis/goAI";
export const GoHistoryPage = (): React.ReactElement => {
useRerender(400);
const classes = boardStyles();
const priorBoard = Player.go.previousGameFinalBoardState ?? getNewBoardState(7);
const score = getScore(priorBoard);
const opponent = priorBoard.ai;
const opponentsToShow = showWorldDemon() ? [...opponentList, opponents.w0r1d_d43m0n] : opponentList;
return (
<div>
<Grid container>
<Grid item>
<div className={classes.statusPageScore}>
<Typography variant="h5">Previous Subnet:</Typography>
<GoScoreSummaryTable score={score} opponent={opponent} />
</div>
</Grid>
<Grid item>
<div className={`${classes.historyPageGameboard} ${classes.translucent}`}>
<GoGameboard
boardState={priorBoard}
traditional={false}
clickHandler={(x, y) => ({ x, y })}
hover={false}
/>
</div>
</Grid>
</Grid>
<br />
<br />
<Typography variant="h5">Faction Stats:</Typography>
<Grid container style={{ maxWidth: "1020px" }}>
{opponentsToShow.map((faction, index) => {
const data = getPlayerStats(faction);
return (
<Grid item key={opponentsToShow[index]} className={classes.factionStatus}>
<Typography>
{" "}
<strong className={classes.keyText}>
{faction === opponents.w0r1d_d43m0n ? <CorruptableText content="????????????" /> : faction}
</strong>
</Typography>
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody>
<TableRow>
<TableCell className={classes.cellNone}>Wins:</TableCell>
<TableCell className={classes.cellNone}>
{data.wins} / {data.losses + data.wins}
</TableCell>
</TableRow>
<TableRow>
<TableCell className={classes.cellNone}>Current winstreak:</TableCell>
<TableCell className={classes.cellNone}>{data.winStreak}</TableCell>
</TableRow>
<TableRow>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
Highest winstreak:
</TableCell>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{data.highestWinStreak}
</TableCell>
</TableRow>
<Tooltip
title={
<>
The total number of empty points and routers <br /> you took control of, across all subnets
</>
}
>
<TableRow>
<TableCell className={classes.cellNone}>Captured nodes:</TableCell>
<TableCell className={classes.cellNone}>{data.nodes}</TableCell>
</TableRow>
</Tooltip>
<Tooltip
title={
<>
Node power is what stat bonuses scale from, and is gained on each completed subnet. <br />
It is calculated from the number of nodes you control, multiplied by modifiers for the <br />
opponent difficulty, if you won or lost, and your current winstreak.
</>
}
>
<TableRow>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>Node power:</TableCell>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{formatNumber(data.nodePower, 2)}
</TableCell>
</TableRow>
</Tooltip>
<Tooltip
title={
<>
Win streaks against a faction will give you +1 favor to that faction <br />
at certain numbers of wins (up to a max of 100 favor), <br />
if you are currently a member of that faction
</>
}
>
<TableRow>
<TableCell className={classes.cellNone}>Favor from winstreaks:</TableCell>
<TableCell className={classes.cellNone}>
{data.favor ?? 0} {data.favor === getMaxFavor() ? "(max)" : ""}
</TableCell>
</TableRow>
</Tooltip>
</TableBody>
</Table>
<br />
<Tooltip title={<>The total stat multiplier gained via your current node power.</>}>
<Typography>
<strong className={classes.keyText}>Bonus:</strong>
<br />
<strong className={classes.keyText}>{getBonusText(faction)}</strong>
</Typography>
</Tooltip>
</Grid>
);
})}
</Grid>
</div>
);
};

@ -0,0 +1,243 @@
import React from "react";
import { boardStyles } from "../boardState/goStyles";
import { Grid, Link, Typography } from "@mui/material";
import { getBoardFromSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
import { opponents, playerColors } from "../boardState/goConstants";
import { GoTutorialChallenge } from "./GoTutorialChallenge";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
import { getMaxFavor } from "../effects/effect";
const captureChallenge = (
<GoTutorialChallenge
state={getBoardFromSimplifiedBoardState(
[".....", "OX...", "OXX..", "OOX.O", "OOX.."],
opponents.none,
playerColors.white,
)}
description={
"CHALLENGE: This white network on the bottom is vulnerable! Click on the board to place a router. Capture some white pieces by cutting off their access to any empty nodes."
}
correctMoves={[{ x: 0, y: 0 }]}
correctText={
"Correct! With no open ports, the white routers are destroyed. Now you surround and control the empty nodes in the bottom-right."
}
incorrectText={"Unfortunately the white routers still touch at least one empty node. Hit 'Reset' to try again."}
/>
);
const saveTheNetworkChallenge = (
<GoTutorialChallenge
state={getBoardFromSimplifiedBoardState(
["OO.##", "XO..#", "XX..#", "XO...", "XO..."],
opponents.none,
playerColors.white,
)}
description={
"CHALLENGE: Your routers are in trouble! They only have one open port. Save the black network by connecting them to more empty nodes."
}
correctMoves={[{ x: 2, y: 2 }]}
correctText={
"Correct! Now the network touches three empty nodes instead of one, making it much harder to cut them off."
}
incorrectText={
"Unfortunately your network can still be cut off from all empty ports in just one move by white. Hit 'Reset' to try again."
}
/>
);
const onlyGoodMoveChallenge = (
<GoTutorialChallenge
state={getBoardFromSimplifiedBoardState(
["XXO.O", "XO.O.", ".OOOO", "XXXXX", "X.X.X"],
opponents.none,
playerColors.white,
)}
description={"CHALLENGE: Save the black network on the left! Connect the network to more than one empty node."}
correctMoves={[{ x: 2, y: 0 }]}
correctText={
"Correct! Now the network touches two empty nodes instead of one, making it much harder to cut them off."
}
incorrectText={
"Incorrect. Your left network can still be cut off from empty ports in just one move. Also, you blocked one of your only open ports from your right network!"
}
/>
);
const makeTwoEyesChallenge = (
<GoTutorialChallenge
state={getBoardFromSimplifiedBoardState(
["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."],
opponents.none,
playerColors.white,
)}
description={
"CHALLENGE: The black routers are only connected to one empty-node group. Place a router such that they are connected to TWO empty node groups instead."
}
correctMoves={[{ x: 2, y: 0 }]}
correctText={
"Correct! Now that your network surrounds empty nodes in multiple different areas, it is impossible for the network to be captured by white because of the suicide rule (unless you fill in your own empty nodes!)."
}
incorrectText={
"Incorrect. The black network still only touches one group of open nodes. (Hint: Try dividing up the bottom open-node group.) Hit 'Reset' to try again."
}
/>
);
export const GoInstructionsPage = (): React.ReactElement => {
const classes = boardStyles();
return (
<div className={classes.instructionScroller}>
<>
<Typography variant="h4">IPvGO</Typography>
<br />
<Typography>
In late 2070, the .org bubble burst, and most of the newly-implemented IPvGO 'net collapsed overnight. Since
then, various factions have been fighting over small subnets to control their computational power. These
subnets are very valuable in the right hands, if you can wrest them from their current owners.
</Typography>
<br />
<br />
<Grid container columns={2}>
<Grid item className={classes.instructionsBlurb}>
<Typography variant="h5">How to take over IPvGO Subnets</Typography>
<br />
<Typography>
Your goal is to control more <i>empty nodes</i> in the subnet than the faction currently holding it, by
surrounding those open nodes with your routers.
<br />
<br />
Each turn you place a router in an empty node (or pass). The router will connect to your adjacent routers,
forming networks. A network's remaining open ports are indicated by lines heading out towards the empty
nodes adjacent to the network.
<br />
<br />
If a group of routers no longer is connected to any empty nodes, they will experience intense packet loss
and be removed from the subnet. Make sure you ALWAYS have access to several empty nodes in each of your
networks! A network with only one remaining open port will start to fade in and out, because it is at risk
of being destroyed.
<br />
<br />
You also can use your routers to limit your opponent's access to empty nodes as much as possible. Cut a
network off from any empty nodes, and their entire group of routers will be removed!
<br />
<br />
</Typography>
</Grid>
<Grid item className={classes.instructionBoardWrapper}>
{captureChallenge}
</Grid>
</Grid>
<br />
<br />
<Grid container>
<Grid item className={classes.instructionBoardWrapper}>
{saveTheNetworkChallenge}
</Grid>
<Grid item className={classes.instructionsBlurb}>
<Typography variant="h5">Winning the Subnet</Typography>
<br />
<Typography>
The game ends when all of the open nodes on the subnet are completely surrounded by a single color, or
when both players pass consecutively.
<br />
<br />
Once the subnet is fully claimed, each player will get one point for each empty node they fully surround
on the subnet, plus a point for each router they have. You can use the edge of the board along with your
routers to fully surround and claim empty nodes. <br />
<br />
White will also get a few points (called "komi") as a home-field advantage in the subnet, and to balance
black's advantage of having the first move.
<br />
<br />
Any territory you control at the end of the game will award you stat multiplier bonuses. Winning the node
will increase the amount gained, but is not required.
<br />
<br />
Win streaks against a faction will give you +1 favor to that faction at certain numbers of wins (up to a
max of {getMaxFavor()} favor), if you are currently a member of that faction.
</Typography>
</Grid>
</Grid>
<br />
<br />
<Grid container>
<Grid item className={classes.instructionsBlurb}>
<Typography variant="h5">Special Rule Details</Typography>
<br />
<Typography>
* Because these subnets have fallen into disrepair, they are not always perfectly square. Dead areas, such
as the top-left corner in the example above, are not part of the subnet. They do not count as territory,
and do not provide open ports to adjacent routers.
<br />
<br />
* You cannot suicide your own routers by cutting off access to their last remaining open node. You also
cannot suicide a router by placing it in a node that is completely surrounded by your opponent's routers.
<br />
<br />
* There is one exception to the suicide rule: You can place a router on ANY node if it would capture any
of the opponent's routers.
<br />
<br />
* You cannot repeat previous board states. This rule prevents infinite loops of capturing and
re-capturing. This means that in some cases you cannot immediately capture an enemy network that is
flashing and vulnerable.
<br />
<br />
Note that you CAN re-capture eventually, but you must play somewhere else on the board first, to make the
overall board state different.
</Typography>
</Grid>
<Grid item className={classes.instructionBoardWrapper}>
{onlyGoodMoveChallenge}
</Grid>
</Grid>
<br />
<br />
<Grid container>
<Grid item className={classes.instructionBoardWrapper}>
{makeTwoEyesChallenge}
</Grid>
<Grid item className={classes.instructionsBlurb}>
<Typography variant="h5">Strategy</Typography>
<br />
<br />
<Typography>
* You can place routers and look at the board state via the "ns.go" api. For more details, go to the IPvGO
page in the{" "}
<Link style={{ cursor: "pointer" }} onClick={() => Router.toPage(Page.Documentation)}>
Bitburner Documentation
</Link>
<br />
<br />
* If a network surrounds a single empty node, the opponent can eventually capture it by filling in that
node. However, if your network has two separate empty nodes inside of it, the suicide rule prevents the
opponent from filling up either of them. This means your network cannot be captured! Try to place your
networks surround several different empty nodes, and avoid filling in your network's empty nodes when
possible.
<br />
<br />
* Pay attention to when a network of routers has only one or two open ports to empty spaces! That is your
opportunity to defend your network, or capture the opposing faction's.
<br />
<br />
* Every faction has a different style, and different weaknesses. Try to identify what they are good and
bad at doing.
<br />
<br />
* The best way to learn strategies is to experiment and find out what works!
<br />
<br />* This game is Go with slightly simplified scoring. For more rule details and strategies try{" "}
<Link href={"https://way-to-go.gitlab.io/#/en/capture-stones"} target={"_blank"} rel="noreferrer">
The Way to Go interactive guide.
</Link>{" "}
<br />
<br />
</Typography>
</Grid>
</Grid>
<br />
</>
</div>
);
};

111
src/Go/ui/GoPoint.tsx Normal file

@ -0,0 +1,111 @@
import React from "react";
import { ClassNameMap } from "@mui/styles";
import { BoardState, columnIndexes, playerColors } from "../boardState/goConstants";
import { findNeighbors } from "../boardState/boardState";
import { pointStyle } from "../boardState/goStyles";
import { findAdjacentLibertiesAndAlliesForPoint } from "../boardAnalysis/boardAnalysis";
interface IProps {
state: BoardState;
x: number;
y: number;
traditional: boolean;
hover: boolean;
valid: boolean;
emptyPointOwner: playerColors;
}
export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: IProps): React.ReactElement {
const classes = pointStyle();
const currentPoint = state.board[x]?.[y];
const player = currentPoint?.player;
const isInAtari =
currentPoint && currentPoint.liberties?.length === 1 && player !== playerColors.empty && !traditional;
const liberties = player !== playerColors.empty ? findAdjacentLibertiesAndAlliesForPoint(state, x, y) : null;
const neighbors = findNeighbors(state, x, y);
const hasNorthLiberty = traditional ? neighbors.north : liberties?.north;
const hasEastLiberty = traditional ? neighbors.east : liberties?.east;
const hasSouthLiberty = traditional ? neighbors.south : liberties?.south;
const hasWestLiberty = traditional ? neighbors.west : liberties?.west;
const pointClass =
player === playerColors.white
? classes.whitePoint
: player === playerColors.black
? classes.blackPoint
: classes.emptyPoint;
const colorLiberty = `${player === playerColors.white ? classes.libertyWhite : classes.libertyBlack} ${
classes.liberty
}`;
const sizeClass = getSizeClass(state.board[0].length, classes);
const isNewStone = state.history?.[state.history?.length - 1]?.[x]?.[y]?.player === playerColors.empty;
const isPriorMove = player === state.previousPlayer && isNewStone;
const emptyPointColorClass =
emptyPointOwner === playerColors.white
? classes.libertyWhite
: emptyPointOwner === playerColors.black
? classes.libertyBlack
: "";
const mainClassName = `${classes.point} ${sizeClass} ${traditional ? classes.traditional : ""} ${
hover ? classes.hover : ""
} ${valid ? classes.valid : ""} ${isPriorMove ? classes.priorPoint : ""}
${isInAtari ? classes.fadeLoopAnimation : ""}`;
return (
<div className={`${mainClassName} ${currentPoint ? "" : classes.hideOverflow}`}>
{currentPoint ? (
<>
<div className={hasNorthLiberty ? `${classes.northLiberty} ${colorLiberty}` : classes.liberty}></div>
<div className={hasEastLiberty ? `${classes.eastLiberty} ${colorLiberty}` : classes.liberty}></div>
<div className={hasSouthLiberty ? `${classes.southLiberty} ${colorLiberty}` : classes.liberty}></div>
<div className={hasWestLiberty ? `${classes.westLiberty} ${colorLiberty}` : classes.liberty}></div>
<div className={`${classes.innerPoint} `}>
<div
className={`${pointClass} ${player !== playerColors.empty ? classes.filledPoint : emptyPointColorClass}`}
></div>
</div>
<div className={`${pointClass} ${classes.tradStone}`} />
{traditional ? <div className={`${pointClass} ${classes.priorStoneTrad}`}></div> : ""}
<div className={classes.coordinates}>
{columnIndexes[x]}
{traditional ? "" : "."}
{y + 1}
</div>
</>
) : (
<>
<div className={classes.broken}>
<div className={classes.coordinates}>no signal</div>
</div>
</>
)}
</div>
);
}
export function getSizeClass(
size: number,
classes: ClassNameMap<"fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen">,
) {
switch (size) {
case 5:
return classes.fiveByFive;
case 7:
return classes.sevenBySeven;
case 9:
return classes.nineByNine;
case 13:
return classes.thirteenByThirteen;
case 19:
return classes.nineteenByNineteen;
}
}

@ -0,0 +1,54 @@
import React from "react";
import { Button, Typography } from "@mui/material";
import { Modal } from "../../ui/React/Modal";
import { goScore, opponents, playerColors } from "../boardState/goConstants";
import { boardStyles } from "../boardState/goStyles";
import { GoScorePowerSummary } from "./GoScorePowerSummary";
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
interface IProps {
open: boolean;
onClose: () => void;
finalScore: goScore;
newSubnet: () => void;
opponent: opponents;
}
export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: IProps): React.ReactElement => {
const classes = boardStyles();
const blackScore = finalScore[playerColors.black];
const whiteScore = finalScore[playerColors.white];
const playerWinsText = opponent === opponents.none ? "Black wins!" : "You win!";
const opponentWinsText = opponent === opponents.none ? "White wins!" : `Winner: ${opponent}`;
return (
<Modal open={open} onClose={onClose}>
<>
<div className={classes.scoreModal}>
<Typography variant="h5" className={classes.centeredText}>
Game complete!
</Typography>
<GoScoreSummaryTable score={finalScore} opponent={opponent} />
<br />
<Typography variant="h5" className={classes.centeredText}>
{blackScore.sum > whiteScore.sum ? playerWinsText : opponentWinsText}
</Typography>
<br />
{opponent !== opponents.none ? (
<>
<GoScorePowerSummary opponent={opponent} finalScore={finalScore} />
<br />
<br />
</>
) : (
""
)}
<Button onClick={newSubnet}>New Subnet</Button>
</div>
</>
</Modal>
);
};

@ -0,0 +1,118 @@
import React from "react";
import { Table, TableBody, TableCell, TableRow, Typography, Tooltip } from "@mui/material";
import { Player } from "@player";
import { getBonusText, getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
import { goScore, opponents, playerColors } from "../boardState/goConstants";
import { boardStyles } from "../boardState/goStyles";
import { formatNumber } from "../../ui/formatNumber";
import { FactionName } from "@enums";
import { getPlayerStats } from "../boardAnalysis/scoring";
interface IProps {
finalScore: goScore;
opponent: opponents;
}
export const GoScorePowerSummary = ({ finalScore, opponent }: IProps) => {
const classes = boardStyles();
const status = getPlayerStats(opponent);
const winStreak = status.winStreak;
const oldWinStreak = status.winStreak;
const nodePower = formatNumber(status.nodePower, 2);
const blackScore = finalScore[playerColors.black];
const whiteScore = finalScore[playerColors.white];
const difficultyMultiplier = getDifficultyMultiplier(whiteScore.komi, Player.go.boardState.board[0].length);
const winstreakMultiplier = getWinstreakMultiplier(winStreak, oldWinStreak);
const nodePowerIncrease = formatNumber(blackScore.sum * difficultyMultiplier * winstreakMultiplier, 2);
return (
<>
<Typography>
<strong>Subnet power gained:</strong>
</Typography>
<br />
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody>
<Tooltip title={<>The total number of empty points and routers you took control of on this subnet</>}>
<TableRow>
<TableCell className={classes.cellNone}>Nodes Captured:</TableCell>
<TableCell className={classes.cellNone}>{blackScore.sum}</TableCell>
</TableRow>
</Tooltip>
<Tooltip title={<>The difficulty multiplier for this opponent faction</>}>
<TableRow>
<TableCell className={classes.cellNone}>Difficulty Multiplier:</TableCell>
<TableCell className={classes.cellNone}>{formatNumber(difficultyMultiplier, 2)}x</TableCell>
</TableRow>
</Tooltip>
<TableRow>
<TableCell className={classes.cellNone}>{winStreak >= 0 ? "Win" : "Loss"} Streak:</TableCell>
<TableCell className={classes.cellNone}>{winStreak}</TableCell>
</TableRow>
<Tooltip
title={
<>
Consecutive wins award progressively higher multipliers for node power. Coming back from a loss streak
also gives an extra bonus.
</>
}
>
<TableRow>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{winStreak >= 0 ? "Win Streak" : "Loss"} Multiplier:
</TableCell>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{formatNumber(winstreakMultiplier, 2)}x
</TableCell>
</TableRow>
</Tooltip>
<Tooltip
title={
<>
Node power is what stat bonuses scale from, and is gained on each completed subnet. <br />
It is calculated from the number of nodes you control, multiplied by modifiers for the <br />
opponent difficulty, if you won or lost, and your current winstreak.
</>
}
>
<TableRow>
<TableCell className={classes.cellNone}>Node power gained:</TableCell>
<TableCell className={classes.cellNone}>{nodePowerIncrease}</TableCell>
</TableRow>
</Tooltip>
<Tooltip title={<>Your total node power from all subnets</>}>
<TableRow>
<TableCell className={classes.cellNone}>Total node power:</TableCell>
<TableCell className={classes.cellNone}>{nodePower}</TableCell>
</TableRow>
</Tooltip>
</TableBody>
</Table>
{winStreak && winStreak % 2 === 0 && Player.factions.includes(opponent as unknown as FactionName) ? (
<Tooltip
title={
<>
Win streaks against a faction will give you +1 favor to that faction <br /> at certain numbers of wins (up
to a max of {getMaxFavor()} favor), <br />
if you are currently a member of that faction
</>
}
>
<Typography className={`${classes.inlineFlexBox} ${classes.keyText}`}>
<span>Winstreak Bonus: </span>
<span>+1 favor to {opponent}</span>
</Typography>
</Tooltip>
) : (
""
)}
<Tooltip title={<>The total stat multiplier gained via your current node power.</>}>
<Typography className={`${classes.inlineFlexBox} ${classes.keyText}`}>
<span>New Total Bonus: </span>
<span>{getBonusText(opponent)}</span>
</Typography>
</Tooltip>
</>
);
};

@ -0,0 +1,72 @@
import React from "react";
import { Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
import { boardStyles } from "../boardState/goStyles";
import { goScore, opponents, playerColors } from "../boardState/goConstants";
interface IProps {
score: goScore;
opponent: opponents;
}
export const GoScoreSummaryTable = ({ score, opponent }: IProps) => {
const classes = boardStyles();
const blackScore = score[playerColors.black];
const whiteScore = score[playerColors.white];
const blackPlayerName = opponent === opponents.none ? "Black" : "You";
const whitePlayerName = opponent === opponents.none ? "White" : opponent;
return (
<>
<br />
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody>
<TableRow>
<TableCell className={classes.cellNone} />
<TableCell className={classes.cellNone}>
<strong>{whitePlayerName}:</strong>
</TableCell>
<TableCell className={classes.cellNone}>
<strong>{blackPlayerName}:</strong>
</TableCell>
</TableRow>
<TableRow>
<TableCell className={classes.cellNone}>Owned Empty Nodes:</TableCell>
<TableCell className={classes.cellNone}>{whiteScore.territory}</TableCell>
<TableCell className={classes.cellNone}>{blackScore.territory}</TableCell>
</TableRow>
<TableRow>
<TableCell className={classes.cellNone}>Routers placed:</TableCell>
<TableCell className={classes.cellNone}>{whiteScore.pieces}</TableCell>
<TableCell className={classes.cellNone}>{blackScore.pieces}</TableCell>
</TableRow>
<Tooltip
title={
<>
Komi represents the current faction's home-field advantage on this subnet, <br />
to balance the first-move advantage that the player with the black routers has.
</>
}
>
<TableRow>
<TableCell className={classes.cellNone}>Komi:</TableCell>
<TableCell className={classes.cellNone}>{whiteScore.komi}</TableCell>
<TableCell className={classes.cellNone} />
</TableRow>
</Tooltip>
<TableRow>
<TableCell className={classes.cellNone}>
<br />
<strong className={classes.keyText}>Total score:</strong>
</TableCell>
<TableCell className={classes.cellNone}>
<strong className={classes.keyText}>{whiteScore.sum}</strong>
</TableCell>
<TableCell className={classes.cellNone}>
<strong className={classes.keyText}>{blackScore.sum}</strong>
</TableCell>
</TableRow>
</TableBody>
</Table>
</>
);
};

@ -0,0 +1,71 @@
import React from "react";
import Typography from "@mui/material/Typography";
import { opponentList } from "../boardState/goConstants";
import { getScore } from "../boardAnalysis/scoring";
import { Player } from "@player";
import { Grid, Table, TableBody, TableCell, TableRow } from "@mui/material";
import { GoGameboard } from "./GoGameboard";
import { boardStyles } from "../boardState/goStyles";
import { useRerender } from "../../ui/React/hooks";
import { getBonusText } from "../effects/effect";
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
export const GoStatusPage = (): React.ReactElement => {
useRerender(400);
const classes = boardStyles();
const score = getScore(Player.go.boardState);
const opponent = Player.go.boardState.ai;
return (
<div>
<Grid container>
<Grid item>
<div className={classes.statusPageScore}>
<Typography variant="h5">Current Subnet:</Typography>
<GoScoreSummaryTable score={score} opponent={opponent} />
</div>
</Grid>
<Grid item>
<div className={classes.statusPageGameboard}>
<GoGameboard
boardState={Player.go.boardState}
traditional={false}
clickHandler={(x, y) => ({ x, y })}
hover={false}
/>
</div>
</Grid>
</Grid>
<br />
<Typography variant="h5">Summary of All Subnet Boosts:</Typography>
<br />
<Table sx={{ display: "table", mb: 1, width: "550px" }}>
<TableBody>
<TableRow>
<TableCell className={classes.cellNone}>
<strong>Faction:</strong>
</TableCell>
<TableCell className={classes.cellNone}>
<strong>Effect:</strong>
</TableCell>
</TableRow>
{opponentList.map((faction, index) => {
return (
<TableRow key={index}>
<TableCell className={classes.cellNone}>
<br />
<span>{faction}:</span>
</TableCell>
<TableCell className={classes.cellNone}>
<br />
<strong className={classes.keyText}>{getBonusText(faction)}</strong>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
};

@ -0,0 +1,152 @@
import { Box, Button, MenuItem, Select, SelectChangeEvent, Tooltip, Typography } from "@mui/material";
import React, { useState } from "react";
import { boardSizes, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
import { Player } from "@player";
import { boardStyles } from "../boardState/goStyles";
import { Modal } from "../../ui/React/Modal";
import { getHandicap } from "../boardState/boardState";
import { CorruptableText } from "../../ui/React/CorruptableText";
import { Settings } from "../../Settings/Settings";
import { getPlayerStats } from "../boardAnalysis/scoring";
import { showWorldDemon } from "../boardAnalysis/goAI";
interface IProps {
open: boolean;
search: (size: number, opponent: opponents) => void;
cancel: () => void;
showInstructions: () => void;
}
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
const classes = boardStyles();
const [opponent, setOpponent] = useState<opponents>(Player.go.boardState?.ai ?? opponents.SlumSnakes);
const preselectedBoardSize =
opponent === opponents.w0r1d_d43m0n ? 19 : Math.min(Player.go.boardState?.board?.[0]?.length ?? 7, 13);
const [boardSize, setBoardSize] = useState(preselectedBoardSize);
const opponentFactions = [opponents.none, ...opponentList];
if (showWorldDemon()) {
opponentFactions.push(opponents.w0r1d_d43m0n);
}
const handicap = getHandicap(boardSize, opponent);
function changeOpponent(event: SelectChangeEvent): void {
const newOpponent = event.target.value as opponents;
setOpponent(newOpponent);
if (newOpponent === opponents.w0r1d_d43m0n) {
setBoardSize(19);
const stats = getPlayerStats(opponents.w0r1d_d43m0n);
if (stats?.wins + stats?.losses === 0) {
Settings.GoTraditionalStyle = false;
}
} else if (boardSize > 13) {
setBoardSize(13);
}
}
function changeBoardSize(event: SelectChangeEvent) {
const newSize = +event.target.value;
setBoardSize(newSize);
}
const onSearch = () => {
search(boardSize, opponent);
};
return (
<Modal open={open} onClose={cancel}>
<div className={classes.searchBox}>
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<br />
<Typography variant="h4">IPvGO Subnet Search</Typography>
<br />
</Box>
<br />
<br />
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Typography className={classes.opponentLabel}>
{opponent !== opponents.none ? "Opponent Faction: " : ""}
</Typography>
<Select value={opponent} onChange={changeOpponent} sx={{ mr: 1 }}>
{opponentFactions.map((faction) => (
<MenuItem key={faction} value={faction}>
{faction === opponents.w0r1d_d43m0n ? (
<CorruptableText content="???????????????" />
) : (
`${faction} (${opponentDetails[faction].description})`
)}
</MenuItem>
))}
</Select>
</Box>
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Typography className={classes.opponentLabel}>Subnet size: </Typography>
{opponent === opponents.w0r1d_d43m0n ? (
<Typography>????</Typography>
) : (
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
{boardSizes.map((size) => (
<MenuItem key={size} value={size}>
{size}x{size}
</MenuItem>
))}
</Select>
)}
</Box>
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Tooltip
title={
<>
This faction will also get a few points as a home-field advantage in the subnet, and to balance the
player's advantage of having the first move.
</>
}
>
<Typography className={classes.opponentLabel}>Komi: {opponentDetails[opponent].komi}</Typography>
</Tooltip>
{handicap ? (
<Tooltip title={<>This faction has placed a few routers to defend their subnet already.</>}>
<Typography className={classes.opponentLabel}>Handicap: {handicap}</Typography>
</Tooltip>
) : (
""
)}
</Box>
<br />
<br />
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle} ${classes.flavorText}`}>
<Typography>
{opponent === opponents.w0r1d_d43m0n ? (
<>
<CorruptableText content={opponentDetails[opponent].flavorText.slice(0, 40)} />
<CorruptableText content={opponentDetails[opponent].flavorText.slice(40)} />
</>
) : (
opponentDetails[opponent].flavorText
)}
</Typography>
</Box>
<br />
<br />
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Typography>
{opponent !== opponents.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
</Typography>
</Box>
<br />
<br />
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Button onClick={onSearch}>Search for Subnet</Button>
<Button onClick={cancel}>Cancel</Button>
</Box>
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
<Typography onClick={showInstructions} className={classes.link}>
How to Play
</Typography>
</Box>
</div>
</Modal>
);
};

@ -0,0 +1,87 @@
import React, { useState } from "react";
import { Typography, Button } from "@mui/material";
import { BoardState, playerColors, validityReason } from "../boardState/goConstants";
import { GoGameboard } from "./GoGameboard";
import { evaluateIfMoveIsValid } from "../boardAnalysis/boardAnalysis";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
import { getStateCopy, makeMove } from "../boardState/boardState";
import { boardStyles } from "../boardState/goStyles";
interface IProps {
state: BoardState;
description: string;
correctMoves: [{ x: number; y: number }];
correctText: string;
incorrectText: string;
incorrectMoves1?: [{ x: number; y: number }];
incorrectText1?: string;
incorrectMoves2?: [{ x: number; y: number }];
incorrectText2?: string;
}
export function GoTutorialChallenge({
state,
description,
correctMoves,
correctText,
incorrectText,
incorrectMoves1,
incorrectText1,
incorrectMoves2,
incorrectText2,
}: IProps): React.ReactElement {
const classes = boardStyles();
const [currentState, setCurrentState] = useState(getStateCopy(state));
const [displayText, setDisplayText] = useState(description);
const [showReset, setShowReset] = useState(false);
const handleClick = (x: number, y: number) => {
if (currentState.history.length) {
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
return;
}
setShowReset(true);
const validity = evaluateIfMoveIsValid(currentState, x, y, playerColors.black);
if (validity != validityReason.valid) {
setDisplayText(
"Invalid move: You cannot suicide your routers by placing them with no access to any empty ports.",
);
return;
}
const updatedBoard = makeMove(currentState, x, y, playerColors.black);
if (updatedBoard) {
setCurrentState(getStateCopy(updatedBoard));
if (correctMoves.find((move) => move.x === x && move.y === y)) {
setDisplayText(correctText);
} else if (incorrectMoves1?.find((move) => move.x === x && move.y === y)) {
setDisplayText(incorrectText1 ?? "");
} else if (incorrectMoves2?.find((move) => move.x === x && move.y === y)) {
setDisplayText(incorrectText2 ?? "");
} else {
setDisplayText(incorrectText);
}
}
};
const reset = () => {
setCurrentState(getStateCopy(state));
setDisplayText(description);
setShowReset(false);
};
return (
<div>
<div className={classes.instructionBoard}>
<GoGameboard boardState={currentState} traditional={false} clickHandler={handleClick} hover={true} />
</div>
<Typography>{displayText}</Typography>
{showReset ? <Button onClick={reset}>Reset</Button> : ""}
</div>
);
}

@ -69,7 +69,9 @@ export function calculateHackingTime(server: IServer, person: IPerson): number {
const hackTimeMultiplier = 5;
const hackingTime =
(hackTimeMultiplier * skillFactor) /
(person.mults.hacking_speed * calculateIntelligenceBonus(person.skills.intelligence, 1));
(person.mults.hacking_speed *
currentNodeMults.HackingSpeedMultiplier *
calculateIntelligenceBonus(person.skills.intelligence, 1));
return hackingTime;
}

@ -184,7 +184,7 @@ export const LocationsMetadata: IConstructorParams[] = [
startingSecurityLevel: 7.18,
},
name: LocationName.NewTokyoDefComm,
types: [LocationType.Company],
types: [LocationType.Company, LocationType.Special],
},
{
city: CityName.NewTokyo,
@ -241,7 +241,7 @@ export const LocationsMetadata: IConstructorParams[] = [
{
city: CityName.Sector12,
name: LocationName.Sector12CIA,
types: [LocationType.Company],
types: [LocationType.Company, LocationType.Special],
},
{
city: CityName.Sector12,

@ -331,6 +331,17 @@ export function SpecialLocation(props: SpecialLocationProps): React.ReactElement
case LocationName.NewTokyoArcade: {
return <ArcadeRoot />;
}
case LocationName.Sector12CIA:
case LocationName.NewTokyoDefComm: {
return (
<>
<br />
<br />
<br />
<Button onClick={() => Router.toPage(Page.Go)}>IPvGO Subnet Takeover</Button>
</>
);
}
default:
console.error(`Location ${props.loc.name} doesn't have any special properties`);
return <></>;

@ -243,6 +243,28 @@ const gang = {
nextUpdate: RamCostConstants.CycleTiming,
} as const;
// Go API
const go = {
makeMove: 4,
passTurn: 0,
getBoardState: 4,
getOpponent: 0,
resetBoardState: 0,
analysis: {
getValidMoves: 8,
getChains: 16,
getLiberties: 16,
getControlledEmptyNodes: 16,
},
cheat: {
getCheatSuccessChance: 1,
removeRouter: 8,
playTwoMoves: 8,
repairOfflineNode: 8,
destroyNode: 8,
},
} as const;
// Bladeburner API
const bladeburner = {
inBladeburner: RamCostConstants.BladeburnerApiBase / 4,
@ -434,6 +456,7 @@ export const RamCosts: RamCostTree<NSFull> = {
stock,
singularity,
gang,
go,
bladeburner,
infiltration,
codingcontract,

@ -66,6 +66,7 @@ import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"
import { LogBoxEvents, LogBoxCloserEvents } from "./ui/React/LogBoxManager";
import { arrayToString } from "./utils/helpers/ArrayHelpers";
import { NetscriptGang } from "./NetscriptFunctions/Gang";
import { NetscriptGo } from "./NetscriptFunctions/Go";
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
import { NetscriptExtra } from "./NetscriptFunctions/Extra";
import { NetscriptHacknet } from "./NetscriptFunctions/Hacknet";
@ -126,6 +127,7 @@ export type NSFull = Readonly<Omit<NS & INetscriptExtra, "pid" | "args" | "enums
export const ns: InternalAPI<NSFull> = {
singularity: NetscriptSingularity(),
gang: NetscriptGang(),
go: NetscriptGo(),
bladeburner: NetscriptBladeburner(),
codingcontract: NetscriptCodingContract(),
sleeve: NetscriptSleeve(),
@ -956,7 +958,7 @@ export const ns: InternalAPI<NSFull> = {
throw helpers.makeRuntimeErrorMsg(ctx, "Requires Source-File 5 to run.");
const n = Math.round(helpers.number(ctx, "n", _n));
const lvl = Math.round(helpers.number(ctx, "lvl", _lvl));
if (n < 1 || n > 13) throw new Error("n must be between 1 and 13");
if (n < 1 || n > 14) throw new Error("n must be between 1 and 14");
if (lvl < 1) throw new Error("lvl must be >= 1");
return Object.assign({}, getBitNodeMultipliers(n, lvl));

@ -0,0 +1,142 @@
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { Player } from "@player";
import { Go } from "@nsdefs";
import { Play, playerColors } from "../Go/boardState/goConstants";
import { getSimplifiedBoardState } from "../Go/boardAnalysis/boardAnalysis";
import {
cheatDestroyNode,
cheatPlayTwoMoves,
cheatRemoveRouter,
cheatRepairOfflineNode,
cheatSuccessChance,
checkCheatApiAccess,
getChains,
getControlledEmptyNodes,
getLiberties,
getValidMoves,
handlePassTurn,
invalidMoveResponse,
makePlayerMove,
resetBoardState,
throwError,
} from "../Go/effects/netscriptGoImplementation";
const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.workerScript, message);
/**
* Ensures the given coordinates are valid for the current board size
*/
function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) {
const boardSize = Player.go.boardState.board.length;
if (x < 0 || x >= boardSize) {
throwError(
ctx.workerScript,
`Invalid column number (x = ${x}), column must be a number 0 through ${boardSize - 1}`,
);
}
if (y < 0 || y >= boardSize) {
throwError(ctx.workerScript, `Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`);
}
}
/**
* Go API implementation
*/
export function NetscriptGo(): InternalAPI<Go> {
return {
makeMove:
(ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => {
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
validateRowAndColumn(ctx, x, y);
return makePlayerMove(logger(ctx), x, y);
},
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
if (Player.go.boardState.previousPlayer === playerColors.black) {
helpers.log(ctx, () => `It is not your turn; you cannot pass.`);
return Promise.resolve(invalidMoveResponse);
}
return handlePassTurn(logger(ctx));
},
getBoardState: () => () => {
return getSimplifiedBoardState(Player.go.boardState.board);
},
getOpponent: () => () => {
return Player.go.boardState.ai;
},
resetBoardState: (ctx) => (_opponent, _boardSize) => {
const opponentString = helpers.string(ctx, "opponent", _opponent);
const boardSize = helpers.number(ctx, "boardSize", _boardSize);
return resetBoardState(error(ctx), opponentString, boardSize);
},
analysis: {
getValidMoves: () => () => {
return getValidMoves();
},
getChains: () => () => {
return getChains();
},
getLiberties: () => () => {
return getLiberties();
},
getControlledEmptyNodes: () => () => {
return getControlledEmptyNodes();
},
},
cheat: {
getCheatSuccessChance: (ctx: NetscriptContext) => () => {
checkCheatApiAccess(error(ctx));
return cheatSuccessChance(Player.go.boardState.cheatCount);
},
removeRouter:
(ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
validateRowAndColumn(ctx, x, y);
return cheatRemoveRouter(logger(ctx), x, y);
},
playTwoMoves:
(ctx: NetscriptContext) =>
async (_x1, _y1, _x2, _y2): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x1 = helpers.number(ctx, "x", _x1);
const y1 = helpers.number(ctx, "y", _y1);
validateRowAndColumn(ctx, x1, y1);
const x2 = helpers.number(ctx, "x", _x2);
const y2 = helpers.number(ctx, "y", _y2);
validateRowAndColumn(ctx, x2, y2);
return cheatPlayTwoMoves(logger(ctx), x1, y1, x2, y2);
},
repairOfflineNode:
(ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
validateRowAndColumn(ctx, x, y);
return cheatRepairOfflineNode(logger(ctx), x, y);
},
destroyNode:
(ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
validateRowAndColumn(ctx, x, y);
return cheatDestroyNode(logger(ctx), x, y);
},
},
};
}

@ -16,7 +16,7 @@ import * as generalMethods from "./PlayerObjectGeneralMethods";
import * as serverMethods from "./PlayerObjectServerMethods";
import * as workMethods from "./PlayerObjectWorkMethods";
import { setPlayer } from "../../Player";
import { setPlayer } from "@player";
import { CompanyName, FactionName, JobName, LocationName } from "@enums";
import { HashManager } from "../../Hacknet/HashManager";
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
@ -28,6 +28,7 @@ import { CONSTANTS } from "../../Constants";
import { Person } from "../Person";
import { isMember } from "../../utils/EnumHelper";
import { PartialRecord } from "../../Types/Record";
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
export class PlayerObject extends Person implements IPlayer {
// Player-specific properties
@ -36,6 +37,7 @@ export class PlayerObject extends Person implements IPlayer {
gang: Gang | null = null;
bladeburner: Bladeburner | null = null;
currentServer = "";
go = getGoPlayerStartingState();
factions: FactionName[] = [];
factionInvitations: FactionName[] = [];
factionRumors = new JSONSet<FactionName>();

@ -1,6 +1,7 @@
/** Augmentation-related methods for the Player class (PlayerObject) */
import { calculateEntropy } from "../Grafting/EntropyAccumulation";
import { staneksGift } from "../../CotMG/Helper";
import { updateGoMults } from "../../Go/effects/effect";
import type { PlayerObject } from "./PlayerObject";
@ -11,4 +12,5 @@ export function applyEntropy(this: PlayerObject, stacks = 1): void {
this.mults = calculateEntropy(stacks);
staneksGift.updateMults();
updateGoMults();
}

@ -51,6 +51,8 @@ import { achievements } from "../../Achievements/Achievements";
import { isCompanyWork } from "../../Work/CompanyWork";
import { isMember } from "../../utils/EnumHelper";
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
import { resetGoNodePower } from "../../Go/effects/effect";
export function init(this: PlayerObject): void {
/* Initialize Player's home computer */
@ -112,6 +114,8 @@ export function prestigeAugmentation(this: PlayerObject): void {
this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));
resetGoNodePower(this);
this.lastUpdate = new Date().getTime();
// Statistics Trackers
@ -148,6 +152,7 @@ export function prestigeSourceFile(this: PlayerObject): void {
resetGangs();
this.corporation = null;
this.bladeburner = null;
this.go = getGoPlayerStartingState();
// Reset Stock market
this.hasWseAccount = false;

@ -1,5 +1,7 @@
/** All netscript definitions */
import { opponents } from "../Go/boardState/goConstants";
/** @public */
interface HP {
current: number;
@ -3806,6 +3808,317 @@ export interface Gang {
nextUpdate(): Promise<number>;
}
/**
* IPvGO api
* @public
*/
export interface Go {
/**
* Make a move on the IPvGO subnet gameboard, and await the opponent's response.
* x:0 y:0 represents the bottom-left corner of the board in the UI.
*
* @remarks
* RAM cost: 4 GB
*
* @returns a promise that contains if your 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
*/
makeMove(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
/**
* 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.
*
* @returns a promise that contains if your 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
*
* @remarks
* RAM cost: 0 GB
*
*/
passTurn(): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
/**
* Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points.
* "#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)
*
* For example, a 5x5 board might look like this:
```
[
"XX.O.",
"X..OO",
".XO..",
"XXO.#",
".XO.#",
]
```
*
* Each string represents a vertical column on the board, and each character in the string represents a point.
*
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index [1][0].
*
* 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.
*
* @remarks
* RAM cost: 4 GB
*/
getBoardState(): string[];
/**
* Returns the name of the opponent faction in the current subnet.
* "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati"
*/
getOpponent(): opponents;
/**
* 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 on the subnet at this state.
*
* opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",
*
* @returns a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details
*
* @remarks
* RAM cost: 0 GB
*/
resetBoardState(
opponent: "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati",
boardSize: 5 | 7 | 9 | 13,
): string[] | undefined;
/**
* Tools to analyze the IPvGO subnet.
*/
analysis: {
/**
* Shows if each point on the board is a valid move for the player.
*
* 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.
*
* @remarks
* RAM cost: 8 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() )
*/
getValidMoves(): boolean[][];
/**
* Returns an ID for each point. All points that share an ID are part of the same network (or "chain"). Empty points
* are also given chain IDs to represent continuous empty space. Dead nodes are given the value `null.`
*
* The data from getChains() can be used with the data from getBoardState() to see which player (or empty) each chain is
*
* For example, a 5x5 board might look like this. There is a large chain #1 on the left side, smaller chains
* 2 and 3 on the right, and a large chain 0 taking up the center of the board.
*
```
[
[ 0,0,0,3,4],
[ 1,0,0,3,3],
[ 1,1,0,0,0],
[null,1,0,2,2],
[null,1,0,2,5],
]
```
* @remarks
* RAM cost: 16 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() )
*
*/
getChains(): (number | null)[][];
/**
* 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.
*
* 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],
]
```
*
* @remarks
* RAM cost: 16 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() )
*/
getLiberties(): number[][];
/**
* Returns 'X', 'O', 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.
*
* 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#",
]
```
*
* @remarks
* RAM cost: 16 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() )
*/
getControlledEmptyNodes(): string[];
};
/**
* Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use.
*/
cheat: {
/**
* Returns your chance of successfully playing one of the special moves in the ns.go.cheat API.
* Scales with your crime success rate stat. Caps at 80%.
*
* 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.
*
* @remarks
* RAM cost: 1 GB
* Requires Bitnode 14.2 to use
*/
getCheatSuccessChance(): number;
/**
* 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.
*
* @remarks
* RAM cost: 8 GB
* Requires Bitnode 14.2 to use
*
* @returns a promise that contains if your 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
*/
removeRouter(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
/**
* 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.
*
* @remarks
* RAM cost: 8 GB
* Requires Bitnode 14.2 to use
*
* @returns a promise that contains if your 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
*/
playTwoMoves(
x1: number,
y1: number,
x2: number,
x2: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
/**
* 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.
*
* @remarks
* RAM cost: 8 GB
* Requires Bitnode 14.2 to use
*
* @returns a promise that contains if your 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
*/
repairOfflineNode(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
/**
* 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.
*
* @remarks
* RAM cost: 8 GB
* Requires Bitnode 14.2 to use
*
* @returns a promise that contains if your 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
*/
destroyNode(
x: number,
y: number,
): Promise<{
type: "invalid" | "move" | "pass" | "gameOver";
x: number;
y: number;
success: boolean;
}>;
};
}
/**
* Sleeve API
* @remarks
@ -4772,6 +5085,12 @@ export interface NS {
*/
readonly gang: Gang;
/**
* Namespace for Go functions.
* @remarks RAM cost: 0 GB
*/
readonly go: Go;
/**
* Namespace for sleeve functions. Contains spoilers.
* @remarks RAM cost: 0 GB

@ -28,6 +28,8 @@ export const Settings = {
EnableBashHotkeys: false,
/** Whether to enable terminal history search */
EnableHistorySearch: false,
/** Whether to show IPvGO in a traditional stone-and-shell-on-wood style, or the cyberpunk style */
GoTraditionalStyle: false,
/** Timestamps format string */
TimestampsFormat: "",
/** Locale used for display numbers. */

@ -39,6 +39,7 @@ import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; // Achievements
import AccountBoxIcon from "@mui/icons-material/AccountBox";
import PublicIcon from "@mui/icons-material/Public";
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
import BorderInnerSharp from "@mui/icons-material/BorderInnerSharp";
import { Router } from "../../ui/GameRoot";
import { Page, isSimplePage } from "../../ui/Router";
@ -55,6 +56,7 @@ import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
import { hash } from "../../hash/hash";
import { Locations } from "../../Locations/Locations";
import { useRerender } from "../../ui/React/hooks";
import { playerHasDiscoveredGo } from "../../Go/effects/effect";
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
props: { color: "primary" | "secondary" | "error" },
@ -158,6 +160,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
const canStockMarket = Player.hasWseAccount;
const canBladeburner = !!Player.bladeburner;
const canStaneksGift = Player.augmentations.some((aug) => aug.name === AugmentationName.StaneksGift1);
const canIPvGO = playerHasDiscoveredGo();
const clickPage = useCallback(
(page: Page) => {
@ -350,6 +353,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canBladeburner && { key_: Page.Bladeburner, icon: FormatBoldIcon },
canCorporation && { key_: Page.Corporation, icon: BusinessIcon },
canGang && { key_: Page.Gang, icon: SportsMmaIcon },
canIPvGO && { key_: Page.Go, icon: BorderInnerSharp },
]}
/>
<Divider />

@ -232,4 +232,28 @@ export function initSourceFiles() {
13,
<>Each level of this Source-File increases the size of Stanek's Gift.</>,
);
SourceFiles.SourceFile14 = new SourceFile(
14,
(
<>
This Source-File grants the following benefits:
<br />
<br />
Level 1: 25% increased stat multipliers from node Power
<br />
Level 2: Permanently unlocks the go.cheat API in other BitNodes
<br />
Level 3: 25% increased success rate for the go.cheat API
<br />
<br />
This Source-File also increases the maximum favor you can gain for each faction from IPvGO by:
<br />
Level 1: +10
<br />
Level 2: +20
<br />
Level 3: +40
</>
),
);
}

@ -167,6 +167,9 @@ export function applySourceFile(bn: number, lvl: number): void {
case 13: // They're Lunatics
// Grants more space on Stanek's Gift.
break;
case 14: // IPvGO
// Grands increased buffs and favor limit from IPvGO
break;
default:
console.error(`Invalid source file number: ${bn}`);
break;

@ -136,7 +136,7 @@ export const calculateCompanyWorkStats = (
const jobPerformance = companyPosition.calculateJobPerformance(worker);
gains.reputation = jobPerformance * worker.mults.company_rep * favorMult;
gains.reputation = jobPerformance * worker.mults.company_rep * favorMult * currentNodeMults.CompanyWorkRepGain;
return gains;
};

@ -10,12 +10,13 @@ import { Settings } from "../Settings/Settings";
import { MoneySourceTracker } from "../utils/MoneySourceTracker";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { Player } from "@player";
import { formatPercent } from "./formatNumber";
import { formatPercent, formatNumber } from "./formatNumber";
import { Modal } from "./React/Modal";
import { Money } from "./React/Money";
import { StatsRow } from "./React/StatsRow";
import { StatsTable } from "./React/StatsTable";
import { useRerender } from "./React/hooks";
import { getMaxFavor } from "../Go/effects/effect";
interface EmployersModalProps {
open: boolean;
@ -49,6 +50,9 @@ interface IMultRow {
// The text color for the row
color?: string;
// Whether to format as percent or scalar
isNumber?: boolean;
}
interface MultTableProps {
@ -69,13 +73,26 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
<StatsRow key={mult} name={mult} color={color} data={{}}>
<>
<Typography color={color}>
<span style={{ opacity: 0.5 }}>{formatPercent(value)}</span> {formatPercent(effValue)}
{data.isNumber ? (
formatNumber(value, 0)
) : (
<>
<span style={{ opacity: 0.5 }}>{formatPercent(value)}</span> {formatPercent(effValue)}
</>
)}
</Typography>
</>
</StatsRow>
);
}
return <StatsRow key={mult} name={mult} color={color} data={{ content: formatPercent(value) }} />;
return (
<StatsRow
key={mult}
name={mult}
color={color}
data={{ content: data.isNumber ? formatNumber(value, 0) : formatPercent(value) }}
/>
);
})}
</TableBody>
</Table>
@ -345,6 +362,7 @@ export function CharacterStats(): React.ReactElement {
{
mult: "Hacking Speed",
value: Player.mults.hacking_speed,
effValue: Player.mults.hacking_speed * currentNodeMults.HackingSpeedMultiplier,
},
{
mult: "Hacking Money",
@ -479,6 +497,7 @@ export function CharacterStats(): React.ReactElement {
{
mult: "Company Reputation Gain",
value: Player.mults.company_rep,
effValue: Player.mults.company_rep * currentNodeMults.CompanyWorkRepGain,
color: Settings.theme.rep,
},
{
@ -501,6 +520,7 @@ export function CharacterStats(): React.ReactElement {
{
mult: "Crime Success Chance",
value: Player.mults.crime_success,
effValue: Player.mults.crime_success * currentNodeMults.CrimeSuccessRate,
},
{
mult: "Crime Money",
@ -532,6 +552,23 @@ export function CharacterStats(): React.ReactElement {
},
]}
color={Settings.theme.primary}
noMargin={!Player.sourceFileLvl(14) && Player.bitNodeN !== 14}
/>
)}
{(Player.sourceFileLvl(14) || Player.bitNodeN === 14) && (
<MultiplierTable
rows={[
{
mult: "IPvGO Node Power bonus",
value: Player.sourceFileLvl(14) ? 1.25 * currentNodeMults.GoPower : currentNodeMults.GoPower,
},
{
mult: "IPvGO Max Favor",
value: getMaxFavor(),
isNumber: true,
},
]}
color={Settings.theme.combat}
noMargin
/>
)}

@ -71,6 +71,7 @@ import { V2Modal } from "../utils/V2Modal";
import { MathJaxContext } from "better-react-mathjax";
import { useRerender } from "./React/hooks";
import { HistoryProvider } from "./React/Documentation";
import { GoRoot } from "../Go/GoRoot";
const htmlLocation = location;
@ -354,6 +355,10 @@ export function GameRoot(): React.ReactElement {
);
break;
}
case Page.Go: {
mainPage = <GoRoot />;
break;
}
case Page.Achievements: {
mainPage = <AchievementsRoot />;
break;

@ -3,12 +3,19 @@ import React, { useState } from "react";
type OptionSwitchProps = {
checked: boolean;
disabled?: boolean;
onChange: (newValue: boolean, error?: string) => void;
text: React.ReactNode;
tooltip: React.ReactNode;
};
export function OptionSwitch({ checked, onChange, text, tooltip }: OptionSwitchProps): React.ReactElement {
export function OptionSwitch({
checked,
disabled = false,
onChange,
text,
tooltip,
}: OptionSwitchProps): React.ReactElement {
const [value, setValue] = useState(checked);
function handleSwitchChange(event: React.ChangeEvent<HTMLInputElement>): void {
@ -20,6 +27,7 @@ export function OptionSwitch({ checked, onChange, text, tooltip }: OptionSwitchP
return (
<>
<FormControlLabel
disabled={disabled}
control={<Switch checked={value} onChange={handleSwitchChange} />}
label={
<Tooltip title={<Typography>{tooltip}</Typography>}>

@ -19,6 +19,7 @@ export enum SimplePage {
DevMenu = "Dev",
Factions = "Factions",
Gang = "Gang",
Go = "IPvGO Subnet",
Hacknet = "Hacknet",
Milestones = "Milestones",
Options = "Options",

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

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

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

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

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