Connect four initial
ci/woodpecker/push/tools-wasm-pack-plugin Pipeline was successful Details
ci/woodpecker/push/wasm-o-x-rust Pipeline was successful Details
ci/woodpecker/push/wasm-connect-four-rust Pipeline was successful Details
ci/woodpecker/push/frontend Pipeline failed Details
ci/woodpecker/manual/tools-wasm-pack-plugin Pipeline failed Details
ci/woodpecker/manual/wasm-o-x-rust unknown status Details
ci/woodpecker/manual/wasm-connect-four-rust unknown status Details
ci/woodpecker/manual/frontend unknown status Details

This commit is contained in:
Gleb Koval 2022-08-13 21:04:12 +00:00
parent ca29e826f1
commit f5da6c01aa
Signed by: cyclane
GPG Key ID: 15E168A8B332382C
10 changed files with 432 additions and 29 deletions

View File

@ -8,7 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@game-algorithms/connect-four-rust": "^0.0.1", "@game-algorithms/connect-four-rust": "^0.0.2",
"@game-algorithms/o-x-rust": "^0.1.0" "@game-algorithms/o-x-rust": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -82,9 +82,9 @@
} }
}, },
"node_modules/@game-algorithms/connect-four-rust": { "node_modules/@game-algorithms/connect-four-rust": {
"version": "0.0.1", "version": "0.0.2",
"resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.1/connect-four-rust-0.0.1.tgz", "resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.2/connect-four-rust-0.0.2.tgz",
"integrity": "sha512-XFVCupzmxi6lL9jfrETenwQwo7JDvGby8PB2jOfRuRpmGBZwgx9GQvc9euw+LwlGCXUhN8zYqKIdoU7IDfd6ng==", "integrity": "sha512-wJ10cBbrMFVqv6gjrWEGNt0kBk1BoDtz8x3VFpiZjcwvarIGh0y1x0F+VnKJiYNoDh3ozdtaeiOFViDluH3xOw==",
"license": "GNU GPLv3" "license": "GNU GPLv3"
}, },
"node_modules/@game-algorithms/o-x-rust": { "node_modules/@game-algorithms/o-x-rust": {
@ -2935,9 +2935,9 @@
} }
}, },
"@game-algorithms/connect-four-rust": { "@game-algorithms/connect-four-rust": {
"version": "0.0.1", "version": "0.0.2",
"resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.1/connect-four-rust-0.0.1.tgz", "resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.2/connect-four-rust-0.0.2.tgz",
"integrity": "sha512-XFVCupzmxi6lL9jfrETenwQwo7JDvGby8PB2jOfRuRpmGBZwgx9GQvc9euw+LwlGCXUhN8zYqKIdoU7IDfd6ng==" "integrity": "sha512-wJ10cBbrMFVqv6gjrWEGNt0kBk1BoDtz8x3VFpiZjcwvarIGh0y1x0F+VnKJiYNoDh3ozdtaeiOFViDluH3xOw=="
}, },
"@game-algorithms/o-x-rust": { "@game-algorithms/o-x-rust": {
"version": "0.1.0", "version": "0.1.0",

View File

@ -33,6 +33,6 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@game-algorithms/o-x-rust": "^0.1.0", "@game-algorithms/o-x-rust": "^0.1.0",
"@game-algorithms/connect-four-rust": "^0.0.1" "@game-algorithms/connect-four-rust": "^0.0.2"
} }
} }

View File

@ -0,0 +1,71 @@
<script lang="ts">
import { type SvelteComponent, createEventDispatcher } from "svelte";
export let board: number[];
export let w = 7;
export let iconMap: Record<number, typeof SvelteComponent>;
export let disabled = false;
const dispatch = createEventDispatcher();
const gridTemplate = Array(w).fill("1fr").join(" ");
/**
* Check if top item of column is empty
*
* @param idx Index of any cell in the column
* @returns Whether the column has an empty top cell
*/
function columnTopEmpty(idx: number): boolean {
const col = idx % w;
return board[col] === 0;
}
/**
* Generate handler function for board cell click
*
* @param idx Index of cell for event
* @returns Handler function
*/
function handleClick(idx: number) {
return () => dispatch("click", {
col: idx % w
});
}
</script>
<div class="grid" style={`grid-template-columns: ${gridTemplate};`}>
{#each board as b, idx}
<button disabled={b !== 0 || disabled || !columnTopEmpty(idx)} class="item" on:click={handleClick(idx)}>
<svelte:component this="{iconMap[b]}"/>
</button>
{/each}
</div>
<style>
.grid {
display: grid;
max-width: min(100%, 56rem);
border: 1px solid var(--cds-interactive-03);
}
button.item {
display: flex;
flex-direction: column;
padding: 1rem;
cursor: pointer;
aspect-ratio: 1/1;
background: none;
border: 2px solid var(--cds-interactive-03);
color: var(--cds-interactive-03);
transition: background 70ms cubic-bezier(0, 0, 0.38, 0.9),
color 70ms cubic-bezier(0, 0, 0.38, 0.9);
}
button.item:enabled:hover {
background-color: var(--cds-hover-tertiary);
color: var(--cds-inverse-01);
}
button.item:disabled {
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,12 @@
<div></div>
<style>
div {
width: 100%;
flex: 1;
text-align: center;
font-size: 4rem;
border-radius: 50%;
background-color: red;
}
</style>

View File

@ -0,0 +1,12 @@
<div></div>
<style>
div {
width: 100%;
flex: 1;
text-align: center;
font-size: 4rem;
border-radius: 50%;
background-color: yellow;
}
</style>

View File

@ -1,8 +0,0 @@
<div></div>
<style>
div {
width: 100%;
height: 100%;
}
</style>

View File

@ -29,6 +29,7 @@
<HeaderNav> <HeaderNav>
<HeaderNavItem href="/" text="About"/> <HeaderNavItem href="/" text="About"/>
<HeaderNavItem href="/o-x" text="Noughts & Crosses"/> <HeaderNavItem href="/o-x" text="Noughts & Crosses"/>
<HeaderNavItem href="/connect-four" text="Connect Four"/>
</HeaderNav> </HeaderNav>
<HeaderUtilities> <HeaderUtilities>
<HeaderGlobalAction icon={dark ? Moon : Sun} on:click={() => dark = !dark}/> <HeaderGlobalAction icon={dark ? Moon : Sun} on:click={() => dark = !dark}/>

View File

@ -0,0 +1,323 @@
<script lang="ts">
import {
Button,
Column,
DataTable,
Grid,
ListItem,
NumberInput,
RadioButton,
RadioButtonGroup,
Row,
Toggle,
UnorderedList
} from "carbon-components-svelte";
import wasmInit, { count_empty, find_winner, get_score, predict } from "@game-algorithms/connect-four-rust";
import ConnectFourBoard from "$lib/components/connect-four/ConnectFourBoard.svelte";
import ConnectFourRed from "$lib/components/connect-four/ConnectFourRed.svelte";
import ConnectFourYellow from "$lib/components/connect-four/ConnectFourYellow.svelte";
import { onMount } from "svelte";
let board: number[] = [];
let turn = -1;
let playAs = "red";
let algorithm = "mm+d";
let w = 7;
let h = 6;
let d = 3;
let min = 4;
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: col: [human, bot]
let scores: [number | null, number | null][][] = [];
onMount(async () => {
await wasmInit();
loaded = true;
});
/**
* Complete a turn logic
*/
function completeTurn() {
turn %= 2;
turn += 1;
const convertedBoard = Uint8Array.from(board);
console.log("1");
scores = [...scores, getBoardScore(board, w, min, human, bot, algorithm)];
console.log("2");
const winner = find_winner(w, convertedBoard, min);
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;
}
if (turn === human) {
status = "Your turn!";
} else {
status = "Algorithm's turn!";
const p = predict(bot, human, bot === 1, w, convertedBoard, min, d, algorithm);
const r = getLowestEmptyRow(board, w, p);
board[r*w + p] = bot;
completeTurn();
}
}
/**
* Get lowest row with no item.
*
* @param board The board
* @param w Board width
* @param col Column to use
*
* @returns The row index
*/
function getLowestEmptyRow(board: number[], w: number, col: number): number {
let r = board.length / w - 1;
while (r >= 0 && board[r*w + col] !== 0) {
r--;
}
return r;
}
/**
* Get the scores for all possible moves on the board for each player
*
* @param board The board
* @param w Board width
* @param min Min in a row to win
* @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[],
w: number,
min: number,
human: number,
bot: number,
algorithm: string
): [number | null, number | null][] {
let scores: [number | null, number | null][] = [];
for (let col = 0;col < w;col++) {
if (getLowestEmptyRow(board, w, col) === -1) {
scores.push([null, null]);
} else {
let i = getLowestEmptyRow(board, w, col)*w + col;
const humanCopy = board.slice();
humanCopy[i] = human;
const hUint8Array = Uint8Array.from(humanCopy);
const botCopy = board.slice();
botCopy[i] = bot;
const bUint8Array = Uint8Array.from(humanCopy);
scores.push([
get_score(human, bot, human === 1, w, hUint8Array, min, d, algorithm),
get_score(bot, human, bot === 1, w, bUint8Array, min, d, algorithm)
]);
}
}
return scores;
}
/**
* 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>Connect Four</title>
</svelte:head>
<Grid>
<Row>
<Column>
<h1>Connect Four</h1>
<p>
This is a connect four bot, with custom board and game options using various algorithms.
</p>
<UnorderedList>
<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="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="Red" value="red" />
<RadioButton labelText="Yellow" value="yellow" />
</RadioButtonGroup>
</div>
<div class="selection numeric">
<NumberInput
bind:value={w}
label="Board width"
min={min}
/>
<NumberInput
bind:value={h}
label="Board height"
min={min}
/>
<NumberInput
bind:value={min}
label="How many in a row to win?"
min={2}
/>
<NumberInput
bind:value={d}
label="Max depth to use for minimax"
min={0}
/>
</div>
<Button disabled={!loaded || turn !== -1} on:click={() => {
board = Array(w * h).fill(0);
scores = [];
turn = 2;
human = Number(playAs === "yellow") + 1;
bot = human % 2 + 1;
completeTurn();
}}>Start</Button>
</Column>
</Row>
<hr/>
<Row>
<Column>
<h2>Board</h2>
{#if loaded && board.length !== 0}
<ConnectFourBoard
w={w}
disabled={false}
board={board}
iconMap={{
1: ConnectFourRed,
2: ConnectFourYellow
}}
on:click={event => {
const r = getLowestEmptyRow(board, w, event.detail.col);
board[r*w + event.detail.col] = human;
setTimeout(completeTurn);
}}
/>
{:else}
<h3>{loaded ? status : "Loading..."}</h3>
{/if}
</Column>
</Row>
<hr/>
<Row>
<Column>
<h2>Scores</h2>
<p>
View scores for each potential move here.
Cells are numbered 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.filter((_, i) => i < w )
.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);
}
.selection.numeric {
max-width: min(100%, 32rem);
}
</style>

View File

@ -13,15 +13,11 @@
} from "carbon-components-svelte"; } from "carbon-components-svelte";
import wasmInit, { count_empty, find_winner, get_score, predict } from "@game-algorithms/o-x-rust"; 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 OXBoard from "$lib/components/o-x/OXBoard.svelte";
import OXNoneIcon from "$lib/components/o-x/OXNoneIcon.svelte";
import OXOIcon from "$lib/components/o-x/OXOIcon.svelte"; import OXOIcon from "$lib/components/o-x/OXOIcon.svelte";
import OXXIcon from "$lib/components/o-x/OXXIcon.svelte"; import OXXIcon from "$lib/components/o-x/OXXIcon.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
let board: number[] = []; let board = Array(9).fill(0);
for (let i = 0;i < 9;i++) {
board.push(0);
}
let turn = -1; let turn = -1;
let playAs = "X"; let playAs = "X";
@ -94,7 +90,7 @@
bot: number, bot: number,
algorithm: string algorithm: string
): [number | null, number | null][] { ): [number | null, number | null][] {
return board.flat().map((v, i) => { return board.map((v, i) => {
if (v === 0) { if (v === 0) {
const humanCopy = board.slice(); const humanCopy = board.slice();
humanCopy[i] = human; humanCopy[i] = human;
@ -139,7 +135,7 @@
function scoreToRows(player: string, moveOverride?: string) { function scoreToRows(player: string, moveOverride?: string) {
return (score: [number | null, number | null][], i: number) => { return (score: [number | null, number | null][], i: number) => {
const rowScores: Record<string, string> = {}; const rowScores: Record<string, string> = {};
for (let cell = 0;cell < 9;cell++) { for (let cell = 0;cell < score.length;cell++) {
const s = score[cell][player === "human" ? 0 : 1]; const s = score[cell][player === "human" ? 0 : 1];
rowScores[cell.toString()] = s === null ? "-" : toFixedOrLess(s, 3); rowScores[cell.toString()] = s === null ? "-" : toFixedOrLess(s, 3);
} }
@ -186,11 +182,8 @@
</RadioButtonGroup> </RadioButtonGroup>
</div> </div>
<Button disabled={!loaded || turn !== -1} on:click={() => { <Button disabled={!loaded || turn !== -1} on:click={() => {
board = []; board = Array(9).fill(0);
scores = []; scores = [];
for (let i = 0;i < 9;i++) {
board.push(0);
}
turn = 2; turn = 2;
human = Number(playAs === "O") + 1; human = Number(playAs === "O") + 1;
bot = human % 2 + 1; bot = human % 2 + 1;
@ -208,7 +201,6 @@
disabled={turn !== human} disabled={turn !== human}
board={board} board={board}
iconMap={{ iconMap={{
0: OXNoneIcon,
1: OXXIcon, 1: OXXIcon,
2: OXOIcon 2: OXOIcon
}} }}

View File

@ -4,7 +4,7 @@ import wasmPack from "vite-plugin-wasm-pack";
const config = { const config = {
plugins: [ plugins: [
sveltekit(), sveltekit(),
wasmPack([], ["@game-algorithms/o-x-rust"]) wasmPack([], ["@game-algorithms/o-x-rust", "@game-algorithms/connect-four-rust"])
] ]
}; };