diff --git a/kifu/core-types/Makefile b/kifu/core-types/Makefile index d42b5a0..9806904 100644 --- a/kifu/core-types/Makefile +++ b/kifu/core-types/Makefile @@ -1,6 +1,6 @@ SOURCES = $(shell find ../core -name "*.rs") -dist/core.d.ts: $(SOURCES) +dist/index.ts: $(SOURCES) mkdir -p dist - typeshare ../core --lang=typescript --output-file=dist/core.d.ts + typeshare ../core --lang=typescript --output-file=dist/index.ts diff --git a/kifu/core-types/package.json b/kifu/core-types/package.json index 4691e70..19766f8 100644 --- a/kifu/core-types/package.json +++ b/kifu/core-types/package.json @@ -2,7 +2,8 @@ "name": "core-types", "version": "0.0.1", "description": "", - "types": "dist/core.d.ts", + "types": "dist/index.ts", + "main": "dist/index.ts", "scripts": { "build": "make", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/kifu/pwa/src/assertNever.ts b/kifu/pwa/src/assertNever.ts new file mode 100644 index 0000000..6e622d1 --- /dev/null +++ b/kifu/pwa/src/assertNever.ts @@ -0,0 +1 @@ +export const assertNever = (_: never) => {}; diff --git a/kifu/pwa/src/components/Board.ts b/kifu/pwa/src/components/Board.ts index b7065ed..8349c36 100644 --- a/kifu/pwa/src/components/Board.ts +++ b/kifu/pwa/src/components/Board.ts @@ -1,25 +1,239 @@ -export class GoBoard extends HTMLCanvasElement { - static get observedAttributes() { - return []; +import { BoardElement, Color, Size, CoreRequest } from "core-types"; +import { assertNever } from "../assertNever"; + +const MARGIN = 20; +const BOARD_WIDTH = 800; +const BOARD_HEIGHT = 800; + +type Pixel = { x: number; y: number }; +type Coordinate = { column: number; row: number }; + +export interface GoBoardProps { + board: BoardElement; + onClick: (_: CoreRequest) => void; +} + +export class GoBoard { + private board: BoardElement; + private pen: Pen; + private cursorLocation: Coordinate | null; + canvas: HTMLCanvasElement; + + constructor({ board, onClick }: GoBoardProps) { + this.board = board; + this.canvas = document.createElement("canvas"); + this.canvas.classList.add("board"); + this.canvas.width = BOARD_WIDTH; + this.canvas.height = BOARD_HEIGHT; + + this.pen = new Pen( + this.canvas.width, + this.canvas.height, + MARGIN, + this.board.size.width, + this.board.size.height + ); + + this.cursorLocation = null; + + this.canvas.onmousemove = (event) => { + const bounds = this.canvas.getBoundingClientRect(); + const coordinate = { + x: event.clientX - bounds.x, + y: event.clientY - bounds.y, + }; + + let address = this.pen.address(coordinate); + + if (this.cursorLocation != address) { + this.cursorLocation = this.pen.address(coordinate); + this.renderBoard(); + } + }; + + this.canvas.onclick = (_) => { + if (this.cursorLocation) { + const intersection = + this.board.spaces[boardAddress(this.board.size, this.cursorLocation)]; + switch (intersection.type) { + case "Unplayable": + break; + case "Empty": + onClick(intersection.content); + break; + case "Filled": + break; + default: + assertNever(intersection); + } + } + }; } - constructor() { - super(); + setBoard(board: BoardElement) { + this.board = board; + + this.pen = new Pen( + this.canvas.width, + this.canvas.height, + MARGIN, + this.board.size.width, + this.board.size.height + ); + this.renderBoard(); } - connectedCallback() { - this.setAttribute("width", "500"); - this.setAttribute("height", "500"); - this.classList.add("go-board"); - const ctx = this.getContext("2d"); + renderBoard() { + const ctx = this.canvas.getContext("2d"); if (!ctx) { alert("could not get the canvas context"); - return; + return null; } - ctx.fillStyle = "rgb(128, 128, 128)"; - ctx.fillRect(10, 10, 50, 50); - ctx.fillStyle = "rgba(0, 0, 200, 0.5)"; - ctx.fillRect(30, 30, 50, 50); + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + ctx.lineWidth = 2; + ctx.strokeStyle = "black"; + ctx.beginPath(); + for (var col = 0; col < this.board.size.width; col++) { + ctx.moveTo(MARGIN + col * this.pen.hspaceBetween, MARGIN); + ctx.lineTo( + MARGIN + col * this.pen.hspaceBetween, + MARGIN + (this.board.size.height - 1) * this.pen.vspaceBetween + ); + } + for (var row = 0; row < this.board.size.height; row++) { + ctx.moveTo(MARGIN, MARGIN + row * this.pen.vspaceBetween); + ctx.lineTo( + MARGIN + (this.board.size.width - 1) * this.pen.hspaceBetween, + MARGIN + row * this.pen.vspaceBetween + ); + } + ctx.closePath(); + ctx.stroke(); + + this.pen.starPoint(ctx, { column: 3, row: 3 }); + this.pen.starPoint(ctx, { column: 3, row: 9 }); + this.pen.starPoint(ctx, { column: 3, row: 15 }); + this.pen.starPoint(ctx, { column: 9, row: 3 }); + this.pen.starPoint(ctx, { column: 9, row: 9 }); + this.pen.starPoint(ctx, { column: 9, row: 15 }); + this.pen.starPoint(ctx, { column: 15, row: 3 }); + this.pen.starPoint(ctx, { column: 15, row: 9 }); + this.pen.starPoint(ctx, { column: 15, row: 15 }); + + col = 0; + row = 0; + for (let idx = 0; idx < this.board.spaces.length; idx++) { + const space = this.board.spaces[idx]; + switch (space.type) { + case "Filled": + this.pen.stone(ctx, { column: col, row: row }, space.content.color); + break; + default: + break; + } + col = col + 1; + if (col == this.board.size.width) { + col = 0; + row = row + 1; + } + } + + if (this.cursorLocation) { + this.pen.ghostStone(ctx, this.cursorLocation, Color.White); + } } } + +class Pen { + margin: number; + hspaceBetween: number; + vspaceBetween: number; + + constructor( + width: number, + height: number, + margin: number, + columns: number, + rows: number + ) { + this.margin = margin; + this.hspaceBetween = (width - margin * 2) / (columns - 1); + this.vspaceBetween = (height - margin * 2) / (rows - 1); + } + + starPoint(ctx: CanvasRenderingContext2D, addr: Coordinate) { + ctx.fillStyle = "rgba(0, 0, 0, 1.0);"; + ctx.beginPath(); + const pixel = this.position(addr); + ctx.moveTo(pixel.x, pixel.y); + ctx.arc(pixel.x, pixel.y, 5, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fill(); + } + + ghostStone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) { + switch (color) { + case Color.White: + ctx.fillStyle = "rgba(230, 230, 230, 0.5)"; + break; + case Color.Black: + ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + break; + } + + this.drawStone(ctx, addr); + } + + stone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) { + switch (color) { + case Color.White: + ctx.fillStyle = "rgb(230, 230, 230)"; + break; + case Color.Black: + ctx.fillStyle = "rgb(0, 0, 0)"; + break; + } + + this.drawStone(ctx, addr); + } + + drawStone(ctx: CanvasRenderingContext2D, addr: Coordinate) { + ctx.beginPath(); + const radius = this.hspaceBetween / 2 - 2; + const pixel = this.position(addr); + ctx.moveTo(pixel.x, pixel.y); + ctx.arc(pixel.x, pixel.y, radius, 0, 2.0 * Math.PI); + ctx.closePath(); + ctx.fill(); + } + + position(addr: Coordinate): Pixel { + return { + x: this.margin + addr.column * this.hspaceBetween, + y: this.margin + addr.row * this.vspaceBetween, + }; + } + + address(pixel: Pixel): Coordinate | null { + if ( + Math.round(pixel.x) < this.margin || + Math.round(pixel.y) < this.margin + ) { + return null; + } else { + return { + column: Math.round( + (Math.round(pixel.x) - this.margin) / this.hspaceBetween + ), + row: Math.round( + (Math.round(pixel.y) - this.margin) / this.vspaceBetween + ), + }; + } + } +} + +const boardAddress = (size: Size, coordinate: Coordinate): number => + coordinate.column + size.width * coordinate.row; diff --git a/kifu/pwa/src/kifu.css b/kifu/pwa/src/kifu.css index cff95ab..ea92600 100644 --- a/kifu/pwa/src/kifu.css +++ b/kifu/pwa/src/kifu.css @@ -13,7 +13,10 @@ body { display: grid; } -.go-board { - width: 100%; - height: 100%; +canvas { + border: 1px solid black; +} + +.board { + background-color: rgb(150, 150, 150); } diff --git a/kifu/pwa/src/main.ts b/kifu/pwa/src/main.ts index 26fb1c7..182eeb6 100644 --- a/kifu/pwa/src/main.ts +++ b/kifu/pwa/src/main.ts @@ -1,28 +1,62 @@ import { GoBoard } from "./components/Board"; +import { CoreRequest, CoreResponse } from "core-types"; import { CoreApi, initCore } from "./coreApi"; +// import { assertNever } from "./assertNever"; -window.customElements.define("go-board", GoBoard, { extends: "canvas" }); +class UIState { + private currentView: GoBoard | null; + private rootElement: HTMLElement; + coreApi: CoreApi; -declare global { - interface HTMLElementTagNameMap { - "go-board": GoBoard; + constructor(coreApi: CoreApi, root: HTMLElement) { + this.currentView = null; + this.rootElement = root; + this.coreApi = coreApi; + + if (!root) { + console.log("root element not found"); + return; + } + } + + processResponse(response: CoreResponse) { + switch (response.type) { + case "PlayingFieldView": + if (this.currentView) { + this.currentView.setBoard(response.content.board); + } else { + this.currentView = new GoBoard({ + board: response.content.board, + onClick: async (request: CoreRequest) => { + const response = await this.coreApi.dispatch(request); + this.processResponse(response); + }, + }); + this.rootElement?.appendChild(this.currentView.canvas); + this.currentView.renderBoard(); + } + break; + default: + console.log("impossible branch: ", response); + alert("impossible branch"); + // assertNever(response); + break; + } } } const main = async () => { let coreApi = await initCore(); - let response = await coreApi.playingField(); - console.log("playing field response: ", response); - + let response = await coreApi.dispatch({ type: "PlayingField" }); const root = document.getElementById("root"); + if (!root) { - alert("could not retrieve the app root container"); + console.log("root element not present"); return; } - const board = document.createElement("canvas", { is: "go-board" }); - console.log("constructed board: ", board); - root.appendChild(board); + const uiState = new UIState(coreApi, root); + uiState.processResponse(response); }; main(); diff --git a/package-lock.json b/package-lock.json index 753687b..32d874d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ } }, "kifu/pwa": { + "name": "kifu-pwa", "version": "1.0.0", "license": "GPL-3.0-or-later", "dependencies": {