Render the Go board and connect it to the core #42

Merged
savanni merged 5 commits from kifu/render-goban into main 2023-05-16 14:22:20 +00:00
7 changed files with 286 additions and 32 deletions

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1 @@
export const assertNever = (_: never) => {};

View File

@ -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;

View File

@ -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);
}

View File

@ -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();

1
package-lock.json generated
View File

@ -36,6 +36,7 @@
}
},
"kifu/pwa": {
"name": "kifu-pwa",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {