frontend with build and publish ci
This commit is contained in:
		| @@ -10,3 +10,4 @@ insert_final_newline = true | |||||||
|  |  | ||||||
| [*.yml] | [*.yml] | ||||||
| indent_style=space | indent_style=space | ||||||
|  | indent_size = 2 | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								.woodpecker/frontend.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								.woodpecker/frontend.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | pipeline: | ||||||
|  |   deps: | ||||||
|  |     image: node:alpine | ||||||
|  |     commands: | ||||||
|  |       - cd frontend | ||||||
|  |       - npm install --save-dev | ||||||
|  |   eslint: | ||||||
|  |     image: node:alpine | ||||||
|  |     commands: | ||||||
|  |       - cd frontend | ||||||
|  |       - npm run lint | ||||||
|  |     group: build | ||||||
|  |   svelte: | ||||||
|  |     image: node:alpine | ||||||
|  |     commands: | ||||||
|  |       - cd frontend | ||||||
|  |       - npm run build | ||||||
|  |   docker build: | ||||||
|  |     image: plugins/docker | ||||||
|  |     settings: | ||||||
|  |       dry_run: true | ||||||
|  |       repo: git.koval.net/cyclane/game-algorithms/frontend | ||||||
|  |       tags: latest | ||||||
|  |       context: frontend | ||||||
|  |       dockerfile: frontend/Dockerfile | ||||||
|  |     when: | ||||||
|  |       branch: | ||||||
|  |         exclude: [main] | ||||||
|  |       path: | ||||||
|  |         include: | ||||||
|  |           - frontend/* | ||||||
|  |           - .woodpecker/frontend.yml | ||||||
|  |   docker build and publish: | ||||||
|  |     image: plugins/docker | ||||||
|  |     settings: | ||||||
|  |       registry: git.koval.net | ||||||
|  |       username: cyclane | ||||||
|  |       password: | ||||||
|  |         from_secret: DEPLOY_TOKEN | ||||||
|  |       repo: git.koval.net/cyclane/game-algorithms/frontend | ||||||
|  |       tags: latest | ||||||
|  |       context: frontend | ||||||
|  |       dockerfile: frontend/Dockerfile | ||||||
|  |     when: | ||||||
|  |       branch: main | ||||||
|  |       path: | ||||||
|  |         include: | ||||||
|  |           - frontend/* | ||||||
|  |           - .woodpecker/frontend.yml | ||||||
							
								
								
									
										13
									
								
								frontend/.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/.eslintignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | .DS_Store | ||||||
|  | node_modules | ||||||
|  | /build | ||||||
|  | /.svelte-kit | ||||||
|  | /package | ||||||
|  | .env | ||||||
|  | .env.* | ||||||
|  | !.env.example | ||||||
|  |  | ||||||
|  | # Ignore files for PNPM, NPM and YARN | ||||||
|  | pnpm-lock.yaml | ||||||
|  | package-lock.json | ||||||
|  | yarn.lock | ||||||
							
								
								
									
										44
									
								
								frontend/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								frontend/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | module.exports = { | ||||||
|  | 	root: true, | ||||||
|  | 	parser: "@typescript-eslint/parser", | ||||||
|  | 	extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jsdoc/recommended"], | ||||||
|  | 	plugins: ["svelte3", "@typescript-eslint", "jsdoc"], | ||||||
|  | 	ignorePatterns: ["*.cjs"], | ||||||
|  | 	overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }], | ||||||
|  | 	settings: { | ||||||
|  | 		"svelte3/typescript": () => require("typescript") | ||||||
|  | 	}, | ||||||
|  | 	parserOptions: { | ||||||
|  | 		sourceType: "module", | ||||||
|  | 		ecmaVersion: 2020 | ||||||
|  | 	}, | ||||||
|  | 	env: { | ||||||
|  | 		browser: true, | ||||||
|  | 		es2017: true, | ||||||
|  | 		node: true | ||||||
|  | 	}, | ||||||
|  | 	rules: { | ||||||
|  | 		semi: 2, | ||||||
|  | 		"semi-spacing": ["error", {before: false, after: false}], | ||||||
|  | 		"object-curly-spacing": ["error", "always"], | ||||||
|  | 		"sort-imports": 2, | ||||||
|  | 		indent: ["error", "tab"], | ||||||
|  | 		"eol-last": 2, | ||||||
|  | 		"max-len": ["error", {code: 120}], | ||||||
|  | 		"prefer-const": 2, | ||||||
|  | 		quotes: ["error", "double"], | ||||||
|  |  | ||||||
|  | 		"@typescript-eslint/no-explicit-any": 2, | ||||||
|  | 		"@typescript-eslint/no-unused-vars": 2, | ||||||
|  |  | ||||||
|  | 		"jsdoc/require-description": 2, | ||||||
|  | 		"jsdoc/require-jsdoc": 2, | ||||||
|  | 		"jsdoc/require-param-description": 2, | ||||||
|  | 		"jsdoc/require-param-name": 2, | ||||||
|  | 		"jsdoc/require-param-type": 0, | ||||||
|  | 		"jsdoc/require-returns": ["error", {forceReturnsWithAsync: true}], | ||||||
|  | 		"jsdoc/require-returns-check": 2, | ||||||
|  | 		"jsdoc/require-returns-description": 2, | ||||||
|  | 		"jsdoc/require-returns-type": 0 | ||||||
|  | 	} | ||||||
|  | }; | ||||||
							
								
								
									
										8
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | .DS_Store | ||||||
|  | node_modules | ||||||
|  | /build | ||||||
|  | /.svelte-kit | ||||||
|  | /package | ||||||
|  | .env | ||||||
|  | .env.* | ||||||
|  | !.env.example | ||||||
							
								
								
									
										1
									
								
								frontend/.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/.npmrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | engine-strict=true | ||||||
							
								
								
									
										12
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | FROM node:alpine | ||||||
|  |  | ||||||
|  | COPY build /app/build | ||||||
|  | COPY package.json /app/package.json | ||||||
|  |  | ||||||
|  | HEALTHCHECK --interval=30s --retries=3 --start-period=10s --timeout=1s \ | ||||||
|  |     CMD wget -q --tries=1 --spider http://localhost:3000/ || exit 1 | ||||||
|  |  | ||||||
|  | EXPOSE 3000 | ||||||
|  |  | ||||||
|  | WORKDIR /app | ||||||
|  | ENTRYPOINT [ "node", "build" ] | ||||||
							
								
								
									
										38
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | # create-svelte | ||||||
|  |  | ||||||
|  | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). | ||||||
|  |  | ||||||
|  | ## Creating a project | ||||||
|  |  | ||||||
|  | If you're seeing this, you've probably already done this step. Congrats! | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # create a new project in the current directory | ||||||
|  | npm create svelte@latest | ||||||
|  |  | ||||||
|  | # create a new project in my-app | ||||||
|  | npm create svelte@latest my-app | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Developing | ||||||
|  |  | ||||||
|  | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | npm run dev | ||||||
|  |  | ||||||
|  | # or start the server and open the app in a new browser tab | ||||||
|  | npm run dev -- --open | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Building | ||||||
|  |  | ||||||
|  | To create a production version of your app: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | You can preview the production build with `npm run preview`. | ||||||
|  |  | ||||||
|  | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. | ||||||
							
								
								
									
										4820
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4820
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										37
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | { | ||||||
|  | 	"name": "frontend", | ||||||
|  | 	"version": "0.0.1", | ||||||
|  | 	"scripts": { | ||||||
|  | 		"dev": "vite dev", | ||||||
|  | 		"build": "vite build", | ||||||
|  | 		"package": "svelte-kit package", | ||||||
|  | 		"preview": "vite preview", | ||||||
|  | 		"check": "svelte-check --tsconfig ./tsconfig.json", | ||||||
|  | 		"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", | ||||||
|  | 		"lint": "eslint .", | ||||||
|  | 		"lint-fix": "eslint . --fix" | ||||||
|  | 	}, | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"@sveltejs/adapter-node": "next", | ||||||
|  | 		"@sveltejs/kit": "next", | ||||||
|  | 		"@typescript-eslint/eslint-plugin": "^5.27.0", | ||||||
|  | 		"@typescript-eslint/parser": "^5.27.0", | ||||||
|  | 		"carbon-components-svelte": "^0.67.7", | ||||||
|  | 		"carbon-icons-svelte": "^11.2.0", | ||||||
|  | 		"carbon-preprocess-svelte": "^0.9.1", | ||||||
|  | 		"eslint": "^8.16.0", | ||||||
|  | 		"eslint-plugin-jsdoc": "^39.3.4", | ||||||
|  | 		"eslint-plugin-svelte3": "^4.0.0", | ||||||
|  | 		"svelte": "^3.44.0", | ||||||
|  | 		"svelte-check": "^2.7.1", | ||||||
|  | 		"svelte-preprocess": "^4.10.6", | ||||||
|  | 		"tslib": "^2.3.1", | ||||||
|  | 		"typescript": "^4.7.4", | ||||||
|  | 		"vite": "^3.0.0", | ||||||
|  | 		"vite-plugin-wasm-pack": "0.1.11" | ||||||
|  | 	}, | ||||||
|  | 	"type": "module", | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"@game-algorithms/o-x-rust": "^0.0.9" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								frontend/src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // See https://kit.svelte.dev/docs/types#app | ||||||
|  | // for information about these interfaces | ||||||
|  | // and what to do when importing types | ||||||
|  | declare namespace App { | ||||||
|  | 	// interface Locals {} | ||||||
|  | 	// interface Platform {} | ||||||
|  | 	// interface PrivateEnv {} | ||||||
|  | 	// interface PublicEnv {} | ||||||
|  | 	// interface Session {} | ||||||
|  | 	// interface Stuff {} | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								frontend/src/app.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/app.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html theme="g100" lang="en"> | ||||||
|  | 	<head> | ||||||
|  | 		<meta charset="utf-8" /> | ||||||
|  | 		<link rel="icon" href="%sveltekit.assets%/favicon.png" /> | ||||||
|  | 		<meta name="viewport" content="width=device-width" /> | ||||||
|  | 		%sveltekit.head% | ||||||
|  | 	</head> | ||||||
|  | 	<body> | ||||||
|  | 		%sveltekit.body% | ||||||
|  | 	</body> | ||||||
|  | </html> | ||||||
							
								
								
									
										8
									
								
								frontend/src/lib/components/OXNoneIcon.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/lib/components/OXNoneIcon.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <div></div> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  | 	div { | ||||||
|  | 		width: 5rem; | ||||||
|  | 		height: 5rem; | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
							
								
								
									
										10
									
								
								frontend/src/lib/components/OXOIcon.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/lib/components/OXOIcon.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <h1>○</h1> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  | 	h1 { | ||||||
|  | 		width: 5rem; | ||||||
|  | 		height: 5rem; | ||||||
|  | 		text-align: center; | ||||||
|  | 		font-size: 4rem; | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
							
								
								
									
										10
									
								
								frontend/src/lib/components/OXXIcon.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/lib/components/OXXIcon.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <h1>⨯</h1> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  | 	h1 { | ||||||
|  | 		width: 5rem; | ||||||
|  | 		height: 5rem; | ||||||
|  | 		text-align: center; | ||||||
|  | 		font-size: 4rem; | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
							
								
								
									
										38
									
								
								frontend/src/routes/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/src/routes/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import "carbon-components-svelte/css/all.css"; | ||||||
|  | 	import { | ||||||
|  | 		Content, | ||||||
|  | 		Header, | ||||||
|  | 		HeaderGlobalAction, | ||||||
|  | 		HeaderNav, | ||||||
|  | 		HeaderNavItem, | ||||||
|  | 		HeaderUtilities, | ||||||
|  | 		SkipToContent, | ||||||
|  | 		Theme | ||||||
|  | 	} from "carbon-components-svelte"; | ||||||
|  | 	import Moon from "carbon-icons-svelte/lib/Moon.svelte"; | ||||||
|  | 	import Sun from "carbon-icons-svelte/lib/Sun.svelte"; | ||||||
|  |  | ||||||
|  | 	let dark = true; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <Theme | ||||||
|  | 	theme={dark ? "g100" : "white"} | ||||||
|  | 	on:update={update => dark = update.detail.theme === "g100"} | ||||||
|  | 	persist persistKey="__carbon-theme" | ||||||
|  | /> | ||||||
|  |  | ||||||
|  | <Header company="Cyclane" platformName="Game Algorithms"> | ||||||
|  | 	<svelte:fragment slot="skip-to-content"> | ||||||
|  | 		<SkipToContent /> | ||||||
|  | 	</svelte:fragment> | ||||||
|  | 	<HeaderNav> | ||||||
|  | 		<HeaderNavItem href="/" text="About"/> | ||||||
|  | 		<HeaderNavItem href="/o-x" text="Noughts & Crosses"/> | ||||||
|  | 	</HeaderNav> | ||||||
|  | 	<HeaderUtilities> | ||||||
|  | 		<HeaderGlobalAction icon={dark ? Moon : Sun} on:click={() => dark = !dark}/> | ||||||
|  | 	</HeaderUtilities> | ||||||
|  | </Header> | ||||||
|  |  | ||||||
|  | <Content><slot/></Content> | ||||||
							
								
								
									
										37
									
								
								frontend/src/routes/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/routes/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import { Column, Grid, Link, Row } from "carbon-components-svelte"; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <Grid> | ||||||
|  | 	<Row> | ||||||
|  | 		<Column> | ||||||
|  | 			<h1>Game Algorithms</h1> | ||||||
|  | 			<img | ||||||
|  | 				class="link" | ||||||
|  | 				alt="CI badge" | ||||||
|  | 				src="https://woodpecker.koval.net/api/badges/cyclane/game-algorithms/status.svg" | ||||||
|  | 				on:click={() => window.location.href = "https://woodpecker.koval.net/cyclane/game-algorithms"} | ||||||
|  | 			/> | ||||||
|  |  | ||||||
|  | 			<p> | ||||||
|  | 				This is a learning project for various board-game alogrithms. | ||||||
|  | 			</p> | ||||||
|  | 			<p> | ||||||
|  | 				Visit the <Link inline href="https://git.koval.net/cyclane/game-algorithms">Git repo</Link> for source code. | ||||||
|  | 			</p> | ||||||
|  | 		</Column> | ||||||
|  | 	</Row> | ||||||
|  | </Grid> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  | 	h1 { | ||||||
|  | 		margin-bottom: var(--cds-layout-01); | ||||||
|  | 	} | ||||||
|  | 	p { | ||||||
|  | 		margin-top: var(--cds-layout-01); | ||||||
|  | 		margin-bottom: var(--cds-layout-01); | ||||||
|  | 	} | ||||||
|  | 	.link { | ||||||
|  | 		cursor: pointer; | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
							
								
								
									
										280
									
								
								frontend/src/routes/o-x.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								frontend/src/routes/o-x.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,280 @@ | |||||||
|  | <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 OXNoneIcon from "$lib/components/OXNoneIcon.svelte"; | ||||||
|  | 	import OXOIcon from "$lib/components/OXOIcon.svelte"; | ||||||
|  | 	import OXXIcon from "$lib/components/OXXIcon.svelte"; | ||||||
|  | 	import { onMount } from "svelte"; | ||||||
|  |  | ||||||
|  | 	let board: number[][] = []; | ||||||
|  | 	for (let i = 0;i < 3;i++) { | ||||||
|  | 		board.push([0, 0, 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.flat()); | ||||||
|  | 		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[Math.floor(p / 3)][p % 3] = 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.flat().map((v, i) => { | ||||||
|  | 			if (v === 0) { | ||||||
|  | 				const humanCopy = board.flat(); | ||||||
|  | 				humanCopy[i] = human; | ||||||
|  | 				const hUint8Array = Uint8Array.from(humanCopy); | ||||||
|  | 				const botCopy = board.flat(); | ||||||
|  | 				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 < 9;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> | ||||||
|  |  | ||||||
|  | <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="Algorithms" 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 = []; | ||||||
|  | 				scores = []; | ||||||
|  | 				for (let i = 0;i < 3;i++) { | ||||||
|  | 					board.push([0, 0, 0]); | ||||||
|  | 				} | ||||||
|  | 				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} | ||||||
|  | 			<Grid> | ||||||
|  | 				{#each board as row, r} | ||||||
|  | 				<Row> | ||||||
|  | 					{#each row as col, c} | ||||||
|  | 					<Column sm={.1} style="border: 1px solid white;"> | ||||||
|  | 						<Button | ||||||
|  | 							kind="tertiary" | ||||||
|  | 							iconDescription={col === 0 ? "Available" : col === 1 ? "Cross" : "Nought"} | ||||||
|  | 							icon={col === 0 ? OXNoneIcon : col === 1 ? OXXIcon : OXOIcon} | ||||||
|  | 							disabled={board[r][c] !== 0 || turn !== human} | ||||||
|  | 							on:click={() => { | ||||||
|  | 								board[r][c] = human; | ||||||
|  | 								completeTurn(); | ||||||
|  | 								console.log(scores); | ||||||
|  | 							}} | ||||||
|  | 						/> | ||||||
|  | 					</Column> | ||||||
|  | 					{/each} | ||||||
|  | 				</Row> | ||||||
|  | 				{/each} | ||||||
|  | 			</Grid> | ||||||
|  | 			{: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.flat().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> | ||||||
							
								
								
									
										
											BIN
										
									
								
								frontend/static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										19
									
								
								frontend/svelte.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/svelte.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | import adapter from "@sveltejs/adapter-node"; | ||||||
|  | import { optimizeImports } from "carbon-preprocess-svelte"; | ||||||
|  | import preprocess from "svelte-preprocess"; | ||||||
|  |  | ||||||
|  | /** @type {import("@sveltejs/kit").Config} */ | ||||||
|  | const config = { | ||||||
|  | 	// Consult https://github.com/sveltejs/svelte-preprocess | ||||||
|  | 	// for more information about preprocessors | ||||||
|  | 	preprocess: [ | ||||||
|  | 		preprocess(), | ||||||
|  | 		optimizeImports(), | ||||||
|  | 	], | ||||||
|  |  | ||||||
|  | 	kit: { | ||||||
|  | 		adapter: adapter() | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default config; | ||||||
							
								
								
									
										14
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | { | ||||||
|  | 	"extends": "./.svelte-kit/tsconfig.json", | ||||||
|  | 	"compilerOptions": { | ||||||
|  | 		"allowJs": true, | ||||||
|  | 		"checkJs": true, | ||||||
|  | 		"esModuleInterop": true, | ||||||
|  | 		"forceConsistentCasingInFileNames": true, | ||||||
|  | 		"resolveJsonModule": true, | ||||||
|  | 		"skipLibCheck": true, | ||||||
|  | 		"sourceMap": true, | ||||||
|  | 		"strict": true, | ||||||
|  | 		"moduleResolution": "Node" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import { sveltekit } from "@sveltejs/kit/vite"; | ||||||
|  | import wasmPack from "vite-plugin-wasm-pack"; | ||||||
|  |  | ||||||
|  | const config = { | ||||||
|  | 	plugins: [ | ||||||
|  | 		sveltekit(), | ||||||
|  | 		wasmPack([], ["@game-algorithms/o-x-rust"]) | ||||||
|  | 	] | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default config; | ||||||
		Reference in New Issue
	
	Block a user