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
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:
parent
ca29e826f1
commit
f5da6c01aa
|
@ -8,7 +8,7 @@
|
|||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -82,9 +82,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@game-algorithms/connect-four-rust": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.1/connect-four-rust-0.0.1.tgz",
|
||||
"integrity": "sha512-XFVCupzmxi6lL9jfrETenwQwo7JDvGby8PB2jOfRuRpmGBZwgx9GQvc9euw+LwlGCXUhN8zYqKIdoU7IDfd6ng==",
|
||||
"version": "0.0.2",
|
||||
"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-wJ10cBbrMFVqv6gjrWEGNt0kBk1BoDtz8x3VFpiZjcwvarIGh0y1x0F+VnKJiYNoDh3ozdtaeiOFViDluH3xOw==",
|
||||
"license": "GNU GPLv3"
|
||||
},
|
||||
"node_modules/@game-algorithms/o-x-rust": {
|
||||
|
@ -2935,9 +2935,9 @@
|
|||
}
|
||||
},
|
||||
"@game-algorithms/connect-four-rust": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://git.koval.net/api/packages/cyclane/npm/%40game-algorithms%2Fconnect-four-rust/-/0.0.1/connect-four-rust-0.0.1.tgz",
|
||||
"integrity": "sha512-XFVCupzmxi6lL9jfrETenwQwo7JDvGby8PB2jOfRuRpmGBZwgx9GQvc9euw+LwlGCXUhN8zYqKIdoU7IDfd6ng=="
|
||||
"version": "0.0.2",
|
||||
"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-wJ10cBbrMFVqv6gjrWEGNt0kBk1BoDtz8x3VFpiZjcwvarIGh0y1x0F+VnKJiYNoDh3ozdtaeiOFViDluH3xOw=="
|
||||
},
|
||||
"@game-algorithms/o-x-rust": {
|
||||
"version": "0.1.0",
|
||||
|
|
|
@ -33,6 +33,6 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"@game-algorithms/o-x-rust": "^0.1.0",
|
||||
"@game-algorithms/connect-four-rust": "^0.0.1"
|
||||
"@game-algorithms/connect-four-rust": "^0.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,8 +0,0 @@
|
|||
<div></div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -29,6 +29,7 @@
|
|||
<HeaderNav>
|
||||
<HeaderNavItem href="/" text="About"/>
|
||||
<HeaderNavItem href="/o-x" text="Noughts & Crosses"/>
|
||||
<HeaderNavItem href="/connect-four" text="Connect Four"/>
|
||||
</HeaderNav>
|
||||
<HeaderUtilities>
|
||||
<HeaderGlobalAction icon={dark ? Moon : Sun} on:click={() => dark = !dark}/>
|
||||
|
|
|
@ -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>
|
|
@ -13,15 +13,11 @@
|
|||
} 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 OXNoneIcon from "$lib/components/o-x/OXNoneIcon.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: number[] = [];
|
||||
for (let i = 0;i < 9;i++) {
|
||||
board.push(0);
|
||||
}
|
||||
let board = Array(9).fill(0);
|
||||
|
||||
let turn = -1;
|
||||
let playAs = "X";
|
||||
|
@ -94,7 +90,7 @@
|
|||
bot: number,
|
||||
algorithm: string
|
||||
): [number | null, number | null][] {
|
||||
return board.flat().map((v, i) => {
|
||||
return board.map((v, i) => {
|
||||
if (v === 0) {
|
||||
const humanCopy = board.slice();
|
||||
humanCopy[i] = human;
|
||||
|
@ -139,7 +135,7 @@
|
|||
function scoreToRows(player: string, moveOverride?: string) {
|
||||
return (score: [number | null, number | null][], i: number) => {
|
||||
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];
|
||||
rowScores[cell.toString()] = s === null ? "-" : toFixedOrLess(s, 3);
|
||||
}
|
||||
|
@ -186,11 +182,8 @@
|
|||
</RadioButtonGroup>
|
||||
</div>
|
||||
<Button disabled={!loaded || turn !== -1} on:click={() => {
|
||||
board = [];
|
||||
board = Array(9).fill(0);
|
||||
scores = [];
|
||||
for (let i = 0;i < 9;i++) {
|
||||
board.push(0);
|
||||
}
|
||||
turn = 2;
|
||||
human = Number(playAs === "O") + 1;
|
||||
bot = human % 2 + 1;
|
||||
|
@ -208,7 +201,6 @@
|
|||
disabled={turn !== human}
|
||||
board={board}
|
||||
iconMap={{
|
||||
0: OXNoneIcon,
|
||||
1: OXXIcon,
|
||||
2: OXOIcon
|
||||
}}
|
||||
|
|
|
@ -4,7 +4,7 @@ import wasmPack from "vite-plugin-wasm-pack";
|
|||
const config = {
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
wasmPack([], ["@game-algorithms/o-x-rust"])
|
||||
wasmPack([], ["@game-algorithms/o-x-rust", "@game-algorithms/connect-four-rust"])
|
||||
]
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue