2022-08-13 21:04:12 +00:00

270 lines
6.8 KiB
Svelte

<script lang="ts">
import {
Button,
Column,
DataTable,
Grid,
ListItem,
RadioButton,
RadioButtonGroup,
Row,
Toggle,
UnorderedList
} from "carbon-components-svelte";
import wasmInit, { count_empty, find_winner, get_score, predict } from "@game-algorithms/o-x-rust";
import OXBoard from "$lib/components/o-x/OXBoard.svelte";
import OXOIcon from "$lib/components/o-x/OXOIcon.svelte";
import OXXIcon from "$lib/components/o-x/OXXIcon.svelte";
import { onMount } from "svelte";
let board = Array(9).fill(0);
let turn = -1;
let playAs = "X";
let algorithm = "mm+d";
let human = 0;
let bot = 0;
let loaded = false;
let showPreviousScores = false;
let showCurrentScores = false;
let showScoresFor = "human";
let status = "Press 'Start' to play";
// step: cell: [human, bot]
let scores: [number | null, number | null][][] = [];
onMount(async () => {
await wasmInit();
loaded = true;
});
/**
* Complete a turn logic
*/
function completeTurn() {
const convertedBoard = Uint8Array.from(board);
scores = [...scores, getBoardScore(board, human, bot, algorithm)];
const winner = find_winner(convertedBoard);
if (winner !== 0 || count_empty(convertedBoard) === 0) {
turn = -1;
switch (winner) {
case 0:
status = "You drew!";
break;
case human:
status = "You won!";
break;
case bot:
status = `The ${algorithm} algorithm defeated you!`;
break;
default:
status = "How did we get here?";
break;
}
alert(status);
return;
}
turn %= 2;
turn += 1;
if (turn === human) {
status = "Your turn!";
} else {
status = "Algorithm's turn!";
const p = predict(bot, human, bot === 1, convertedBoard, algorithm);
board[p] = bot;
completeTurn();
}
}
/**
* Get the scores for all possible moves on the board for each player
*
* @param board The board
* @param human The human player ID
* @param bot The bot player ID
* @param algorithm The algorithm to use for scoring
* @returns The scores for the moves for each cell and player
*/
function getBoardScore(
board: number[],
human: number,
bot: number,
algorithm: string
): [number | null, number | null][] {
return board.map((v, i) => {
if (v === 0) {
const humanCopy = board.slice();
humanCopy[i] = human;
const hUint8Array = Uint8Array.from(humanCopy);
const botCopy = board.slice();
botCopy[i] = bot;
const bUint8Array = Uint8Array.from(humanCopy);
return [
get_score(human, bot, human === 1, hUint8Array, algorithm),
get_score(bot, human, bot === 1, bUint8Array, algorithm)
];
} else {
return [null, null];
}
});
}
/**
* Helper function to add up to <dp> decimal places to a number
*
* @param n Number
* @param dp Maximum decimal places
* @returns String formated number
*/
function toFixedOrLess(n: number, dp: number) {
const f = n.toFixed(dp);
const [u, d] = f.split(".");
const minD = d.slice(0, d.indexOf("0"));
if (minD.length === 0) {
return u;
}
return u + "." + minD;
}
/**
* Generate a table row from an array of scores
*
* @param player Player to get scores for
* @param moveOverride Override the move key value
* @returns The generated table row
*/
function scoreToRows(player: string, moveOverride?: string) {
return (score: [number | null, number | null][], i: number) => {
const rowScores: Record<string, string> = {};
for (let cell = 0;cell < score.length;cell++) {
const s = score[cell][player === "human" ? 0 : 1];
rowScores[cell.toString()] = s === null ? "-" : toFixedOrLess(s, 3);
}
return {
id: i,
move: moveOverride || i + 1,
...rowScores
};
};
}
</script>
<svelte:head>
<title>Noughts & Crosses</title>
</svelte:head>
<Grid>
<Row>
<Column>
<h1>Noughts and Crosses</h1>
<p>
This is a noughts and crosses bot using various algorithms.
</p>
<UnorderedList>
<ListItem>sa: score adding without optimizations</ListItem>
<ListItem>sa+rd: score adding with ratio optimization including draw (score between win and loose)</ListItem>
<ListItem>sa+r-d: score adding with ratio optimization not including draw (draw = loss)</ListItem>
<ListItem>mm: minimax</ListItem>
<ListItem>mm+d: minimax with depth</ListItem>
</UnorderedList>
<div class="selection">
<RadioButtonGroup disabled={turn !== -1} legendText="Algorithm" bind:selected={algorithm}>
<RadioButton labelText="sa" value="sa" />
<RadioButton labelText="sa+rd" value="sa+rd" />
<RadioButton labelText="sa+r-d" value="sa+r-d" />
<RadioButton labelText="mm" value="mm" />
<RadioButton labelText="mm+d" value="mm+d" />
</RadioButtonGroup>
</div>
<div class="selection">
<RadioButtonGroup disabled={turn !== -1} legendText="Play as" bind:selected={playAs}>
<RadioButton labelText="Noughts" value="O" />
<RadioButton labelText="Crosses" value="X" />
</RadioButtonGroup>
</div>
<Button disabled={!loaded || turn !== -1} on:click={() => {
board = Array(9).fill(0);
scores = [];
turn = 2;
human = Number(playAs === "O") + 1;
bot = human % 2 + 1;
completeTurn();
}}>Start</Button>
</Column>
</Row>
<hr/>
<Row>
<Column>
<h2>Board</h2>
<p>{status}</p>
{#if loaded}
<OXBoard
disabled={turn !== human}
board={board}
iconMap={{
1: OXXIcon,
2: OXOIcon
}}
on:click={event => {
board[event.detail.idx] = human;
completeTurn();
}}
/>
{:else}
<h3>Loading...</h3>
{/if}
</Column>
</Row>
<hr/>
<Row>
<Column>
<h2>Scores</h2>
<p>
View scores for each potential move here.
Cells are numbered from 1 to 9 going left-to-right, top-to-bottom.
The table headers show the cell numbers.
</p>
<Toggle labelText="Show previous scores" bind:toggled={showPreviousScores}/>
<Toggle labelText="Show current scores" bind:toggled={showCurrentScores}/>
<div class="selection">
<RadioButtonGroup legendText="Shows scores for" bind:selected={showScoresFor}>
<RadioButton labelText="You" value="human" />
<RadioButton labelText="The algorithm" value="bot" />
</RadioButtonGroup>
</div>
{#if showPreviousScores || showCurrentScores}
<DataTable headers={[
{
key: "move",
value: "Move"
},
...board.map((_, c) => {
return {
key: c.toString(),
value: (c + 1).toString()
};
})
]}
rows={[
...(showPreviousScores ?
scores.slice(0, -1).map(scoreToRows(showScoresFor)) : []
),
...(showCurrentScores && scores.length !== 0 ?
[scoreToRows(showScoresFor, "Now")(scores.slice(-1)[0], scores.length)] : []
)
]}
/>
{/if}
</Column>
</Row>
</Grid>
<style>
h1, h2, h3 {
margin-bottom: var(--cds-layout-01);
}
p, .selection {
margin-top: var(--cds-layout-01);
margin-bottom: var(--cds-layout-01);
}
</style>