diff --git a/index.html b/index.html index 2af8acc..7df951b 100644 --- a/index.html +++ b/index.html @@ -6,8 +6,12 @@ Vite App - -
+ +
+
+
+
+
diff --git a/package-lock.json b/package-lock.json index d67cf04..33abd18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/chess.js": "^0.11.2", "@types/gif.js": "^0.2.2", "chess.js": "^0.12.0", + "common-tags": "^1.8.2", "gif.js": "^0.2.0", "h264-mp4-encoder": "^1.0.12", "hammerjs": "^2.0.8", @@ -21,6 +22,7 @@ "webm-writer": "^1.0.0" }, "devDependencies": { + "@types/common-tags": "^1.8.1", "@types/hammerjs": "^2.0.41", "@types/node": "^17.0.8", "@types/webfontloader": "^1.6.34", @@ -46,6 +48,12 @@ "resolved": "https://registry.npmjs.org/@types/chess.js/-/chess.js-0.11.2.tgz", "integrity": "sha512-ZgT0f5Dm1x+kjA/RhCQpBM2NoUGkdk9JFdwncW7/GdqzHCOkLsOTq4k/EKRiRTCFLXR4fTKk8j7KNOmzzHQVgg==" }, + "node_modules/@types/common-tags": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.1.tgz", + "integrity": "sha512-20R/mDpKSPWdJs5TOpz3e7zqbeCNuMCPhV7Yndk9KU2Rbij2r5W4RzwDPkzC+2lzUqXYu9rFzTktCBnDjHuNQg==", + "dev": true + }, "node_modules/@types/gif.js": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@types/gif.js/-/gif.js-0.2.2.tgz", @@ -74,6 +82,14 @@ "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-0.12.0.tgz", "integrity": "sha512-eF1xf4j88r/AwIRxqV1FXZbVgMt4yUx05xrL+ZMqUrY+snFrUxGVHs0VgEd3AvngujDE9th7XZqqCwEKEQ7mWQ==" }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/esbuild": { "version": "0.13.15", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", @@ -2889,6 +2905,12 @@ "resolved": "https://registry.npmjs.org/@types/chess.js/-/chess.js-0.11.2.tgz", "integrity": "sha512-ZgT0f5Dm1x+kjA/RhCQpBM2NoUGkdk9JFdwncW7/GdqzHCOkLsOTq4k/EKRiRTCFLXR4fTKk8j7KNOmzzHQVgg==" }, + "@types/common-tags": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.1.tgz", + "integrity": "sha512-20R/mDpKSPWdJs5TOpz3e7zqbeCNuMCPhV7Yndk9KU2Rbij2r5W4RzwDPkzC+2lzUqXYu9rFzTktCBnDjHuNQg==", + "dev": true + }, "@types/gif.js": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@types/gif.js/-/gif.js-0.2.2.tgz", @@ -2917,6 +2939,11 @@ "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-0.12.0.tgz", "integrity": "sha512-eF1xf4j88r/AwIRxqV1FXZbVgMt4yUx05xrL+ZMqUrY+snFrUxGVHs0VgEd3AvngujDE9th7XZqqCwEKEQ7mWQ==" }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" + }, "esbuild": { "version": "0.13.15", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", diff --git a/package.json b/package.json index 6e55bb4..2f39ac9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "preview": "vite preview" }, "devDependencies": { + "@types/common-tags": "^1.8.1", "@types/hammerjs": "^2.0.41", "@types/node": "^17.0.8", "@types/webfontloader": "^1.6.34", @@ -18,6 +19,7 @@ "@types/chess.js": "^0.11.2", "@types/gif.js": "^0.2.2", "chess.js": "^0.12.0", + "common-tags": "^1.8.2", "gif.js": "^0.2.0", "h264-mp4-encoder": "^1.0.12", "hammerjs": "^2.0.8", diff --git a/src/board/Board.ts b/src/board/Board.ts index eb9eec3..6190fc1 100644 --- a/src/board/Board.ts +++ b/src/board/Board.ts @@ -1,6 +1,5 @@ -import { BoardConfig, PiecesStyle } from "./../types"; -import { Material, MoveWithDetails } from "../game/Game_x"; -import { Style, BoardData } from "../types"; +import { BoardConfig, PiecesStyle, Position } from "./../types"; +import { Style } from "../types"; import drawRectangle from "./layers/drawRectangle"; import drawCoords from "./layers/drawCoords"; import drawMoveIndicators from "./layers/drawMoveIndicators"; @@ -37,14 +36,10 @@ class Board { private style: Style = boards.standard; private flipped: boolean = false; private header: { [key: string]: string | undefined } = {}; - private boardData: BoardData | null = null; - private ctx: CanvasRenderingContext2D; - private tempCtx: CanvasRenderingContext2D; + // private boardData: BoardData | null = null; private borderVisible: boolean = true; - private lastMove: MoveWithDetails | null = null; - private lastMaterial: Material | undefined = undefined; - public canvas: HTMLCanvasElement = document.createElement("canvas"); - private tempCanvas: HTMLCanvasElement = document.createElement("canvas"); + private lastPosition: Position | null = null; + // private lastMaterial: Material | undefined = undefined; private background: HTMLCanvasElement | null = null; private extraInfo: boolean = true; private piecesStyle: PiecesStyle = "tatiana"; @@ -54,6 +49,12 @@ class Board { private showChecks: boolean = true; private currentScreen: "title" | "move" = "move"; + private ctx: CanvasRenderingContext2D; + private tempCtx: CanvasRenderingContext2D; + + private tempCanvas: HTMLCanvasElement = document.createElement("canvas"); + public canvas: HTMLCanvasElement = document.createElement("canvas"); + constructor(config: Partial = {}) { const ctx = this.canvas.getContext("2d"); const tempCtx = this.tempCanvas.getContext("2d"); @@ -96,13 +97,8 @@ class Board { if (this.currentScreen === "title") { await this.titleFrame(this.header); - } else { - await this.frame( - this.boardData, - this.header, - this.lastMove, - this.lastMaterial - ); + } else if (this.lastPosition !== null) { + await this.frame(this.lastPosition, this.header); } this.render(); @@ -174,30 +170,6 @@ class Board { return this; } - isCheck(move: MoveWithDetails | null) { - if (!move) { - return false; - } - - return move.san.includes("+"); - } - - isMate(move: MoveWithDetails | null) { - if (!move) { - return false; - } - - return move.san.includes("#"); - } - - getOppositeColor(color?: "w" | "b") { - if (!color) { - return; - } - - return color === "w" ? "b" : "w"; - } - async titleFrame(header: { [key: string]: string | undefined }) { this.currentScreen = "title"; this.header = header; @@ -265,22 +237,12 @@ class Board { } async frame( - boardData: BoardData | null, - header: { [key: string]: string | undefined }, - move: MoveWithDetails | null = null, - material?: Material + position: Position | null, + header: { [key: string]: string | undefined } ) { this.currentScreen = "move"; - this.lastMove = move; - this.boardData = boardData; - this.lastMaterial = material; - - const check = this.isCheck(move) - ? this.getOppositeColor(move?.color) - : undefined; - const mate = this.isMate(move) - ? this.getOppositeColor(move?.color) - : undefined; + this.lastPosition = position; + this.header = header; this.tempCtx.clearRect(0, 0, this.size, this.size); @@ -290,64 +252,59 @@ class Board { this.tempCtx.drawImage((await this.background) as HTMLCanvasElement, 0, 0); - if (boardData !== null) { - if (this.lastMove && this.showMoveIndicator) { - await drawMoveIndicators( - this.tempCtx, - this.lastMove, - this.squareSize, - this.style, - this.borderWidth, - this.tiles, - this.flipped, - this.margin - ); - } - - if (!this.borderVisible && this.showCoords) { - drawCoords( - this.tempCtx, - this.style.coords, - this.squareSize, - this.tiles, - this.flipped, - this.borderWidth, - this.size, - this.borderVisible, - this.margin - ); - } - - const piecesShadow = false; - - await drawPieces( + if (this.lastPosition?.move && this.showMoveIndicator) { + await drawMoveIndicators( this.tempCtx, - boardData, + this.lastPosition.move, this.squareSize, + this.style, this.borderWidth, this.tiles, this.flipped, - check && this.showChecks ? check : undefined, - mate && this.showChecks ? mate : undefined, - piecesShadow, - this.margin, - this.piecesStyle + this.margin ); + } - if (this.extraInfo && header) { - await drawExtraInfo( - this.tempCtx, - this.width, - this.height, - this.scale, - this.margin, - this.style, - header, - this.flipped, - move?.end === 0, - material && this.showMaterial ? material : undefined - ); - } + if (!this.borderVisible && this.showCoords) { + drawCoords( + this.tempCtx, + this.style.coords, + this.squareSize, + this.tiles, + this.flipped, + this.borderWidth, + this.size, + this.borderVisible, + this.margin + ); + } + + if (!this.lastPosition) { + return; + } + + await drawPieces( + this.tempCtx, + this.lastPosition, + this.squareSize, + this.borderWidth, + this.flipped, + this.margin, + this.piecesStyle + ); + + if (this.extraInfo && header) { + await drawExtraInfo( + this.tempCtx, + this.width, + this.height, + this.scale, + this.margin, + this.style, + this.header, + this.flipped, + this.lastPosition + ); } } diff --git a/src/board/layers/drawExtraInfo.ts b/src/board/layers/drawExtraInfo.ts index da3e080..99de462 100644 --- a/src/board/layers/drawExtraInfo.ts +++ b/src/board/layers/drawExtraInfo.ts @@ -1,5 +1,4 @@ -import { Material } from "../../game/Game_x"; -import { Style } from "./../../types"; +import { Style, Position } from "./../../types"; import drawText from "./drawText"; const chessFontMapping: { [key: string]: string } = { @@ -20,8 +19,7 @@ const drawExtraInfo = async ( style: Style, data: { [key: string]: string | undefined }, flipped: boolean, - lastMove: boolean, - material?: Material + position: Position ) => { const fontSize = Math.round(20 * scale); let offsetX = (margin - fontSize) / 2; @@ -88,7 +86,7 @@ const drawExtraInfo = async ( let rightMarginWhite = 0; let rightMarginBlack = 0; - if (lastMove && data.Result) { + if (position.last && data.Result) { const [resultWhite, resultBlack] = data.Result.split("-"); const textWhite = @@ -125,76 +123,75 @@ const drawExtraInfo = async ( rightMarginBlack = w + 20 * scale; } - if (material) { - const textWhite = material.diff > 0 ? `+${Math.abs(material.diff)}` : ""; + const { diff, imbalance } = position.material; - rightMarginWhite += drawText( - ctx, - textWhite, - "Fira Mono", - fontSize, - 500, - width - offsetX - rightMarginWhite, - flipped ? offsetY : height - offsetY, - "right" - ); + const textWhite = diff > 0 ? `+${Math.abs(diff)}` : ""; - const textBlack = material.diff < 0 ? `+${Math.abs(material.diff)}` : ""; + rightMarginWhite += drawText( + ctx, + textWhite, + "Fira Mono", + fontSize, + 500, + width - offsetX - rightMarginWhite, + flipped ? offsetY : height - offsetY, + "right" + ); - rightMarginBlack += drawText( - ctx, - textBlack, - "Fira Mono", - fontSize, - 500, - width - offsetX - rightMarginBlack, - flipped ? height - offsetY : offsetY, - "right" - ); + const textBlack = diff < 0 ? `+${Math.abs(diff)}` : ""; - for (const [piece, count] of Object.entries(material.imbalance.w)) { - for (let i = 0; i < count; i++) { - const textWidth = drawText( - ctx, - chessFontMapping[piece], - "Chess", - fontSize, - 500, - width - offsetX - rightMarginWhite, - (flipped ? offsetY : height - offsetY) - 2 * scale, - "right" - ); + rightMarginBlack += drawText( + ctx, + textBlack, + "Fira Mono", + fontSize, + 500, + width - offsetX - rightMarginBlack, + flipped ? height - offsetY : offsetY, + "right" + ); - rightMarginWhite += - i === count - 1 - ? textWidth * 0.85 - : piece === "p" - ? textWidth * 0.4 - : textWidth * 0.6; - } + for (const [piece, count] of Object.entries(imbalance.w)) { + for (let i = 0; i < count; i++) { + const textWidth = drawText( + ctx, + chessFontMapping[piece], + "Chess", + fontSize, + 500, + width - offsetX - rightMarginWhite, + (flipped ? offsetY : height - offsetY) - 2 * scale, + "right" + ); + + rightMarginWhite += + i === count - 1 + ? textWidth * 0.85 + : piece === "p" + ? textWidth * 0.4 + : textWidth * 0.6; } + } - for (const [piece, count] of Object.entries(material.imbalance.b)) { - for (let i = 0; i < count; i++) { - const textWidth = drawText( - ctx, - chessFontMapping[piece], - "Chess", - fontSize, - 500, - width - offsetX - rightMarginBlack, - (flipped ? height - offsetY : offsetY) - 2 * scale, - "right" - ); + for (const [piece, count] of Object.entries(imbalance.b)) { + for (let i = 0; i < count; i++) { + const textWidth = drawText( + ctx, + chessFontMapping[piece], + "Chess", + fontSize, + 500, + width - offsetX - rightMarginBlack, + (flipped ? height - offsetY : offsetY) - 2 * scale, + "right" + ); - rightMarginBlack += - i === count - 1 - ? textWidth * 0.85 - : piece === "p" - ? textWidth * 0.4 - : textWidth * 0.6; - // i === count - 1 || piece !== "p" ? textWidth * 0.8 : textWidth * 0.4; - } + rightMarginBlack += + i === count - 1 + ? textWidth * 0.85 + : piece === "p" + ? textWidth * 0.4 + : textWidth * 0.6; } } }; diff --git a/src/board/layers/drawPieces.ts b/src/board/layers/drawPieces.ts index e92d2f5..9647cb0 100644 --- a/src/board/layers/drawPieces.ts +++ b/src/board/layers/drawPieces.ts @@ -1,74 +1,56 @@ -import { BoardData, PieceType, PieceColor, PiecesStyle } from "../../types"; +import { Position, PieceType, PieceColor, PiecesStyle } from "../../types"; import ImagesCache from "../loaders/PiecesCache"; // import drawCircle from "./drawCircle"; const drawPieces = async ( ctx: CanvasRenderingContext2D, - board: BoardData, + position: Position, squareSize: number, borderWidth: number, - tiles: number, flipped: boolean, - check: "b" | "w" | undefined, - mate: "b" | "w" | undefined, - shadow: boolean, margin: number, piecesStyle: PiecesStyle ) => { - for (let y = 0; y < 8; y++) { - for (let x = 0; x < 8; x++) { - if (board[y][x] !== null) { - const { type, color } = board[y][x] as { - type: PieceType; - color: PieceColor; - }; - const img = await ImagesCache.get(piecesStyle, type, color); - const rank = flipped ? tiles - 1 - y : y; - const file = flipped ? tiles - 1 - x : x; + const { placement, check, mate, turn } = position; - const filters = []; + for (const { x, y, type, color } of placement) { + const img = await ImagesCache.get(piecesStyle, type, color); + const rank = flipped ? 8 - 1 - y : y; + const file = flipped ? 8 - 1 - x : x; - if (shadow) { - filters.push( - `drop-shadow(${squareSize * 0.05}px ${squareSize * 0.05}px ${ - squareSize * 0.05 - }px rgba(0, 0, 0, 0.6))` - ); - } + const filters = []; - // if ((color === check || color === mate) && type === "k") { - // const hex = check ? "#ffa600" : "#ff002f"; - // drawCircle( - // ctx, - // squareSize / 2, - // borderWidth + file * squareSize, - // borderWidth + rank * squareSize + margin, - // 0, - // hex, - // true - // ); - // } + // if ((color === check || color === mate) && type === "k") { + // const hex = check ? "#ffa600" : "#ff002f"; + // drawCircle( + // ctx, + // squareSize / 2, + // borderWidth + file * squareSize, + // borderWidth + rank * squareSize + margin, + // 0, + // hex, + // true + // ); + // } - if ((color === check || color === mate) && type === "k") { - const hex = check ? "#ffa600" : "#ff002f"; - filters.push(`drop-shadow(0 0 ${squareSize * 0.07}px ${hex})`); - filters.push(`drop-shadow(0 0 ${squareSize * 0.07}px ${hex})`); - } - - ctx.filter = filters.length > 0 ? filters.join(" ") : "none"; - - ctx.drawImage( - img, - borderWidth + file * squareSize, - borderWidth + rank * squareSize + margin, - squareSize, - squareSize - ); - } + if ((check || mate) && type === "k" && color === turn) { + const hex = mate ? "#ff002f" : "#ffa600"; + filters.push(`drop-shadow(0 0 ${squareSize * 0.07}px ${hex})`); + filters.push(`drop-shadow(0 0 ${squareSize * 0.07}px ${hex})`); } - } - ctx.filter = "none"; + ctx.filter = filters.join(" "); + + ctx.drawImage( + img, + borderWidth + file * squareSize, + borderWidth + rank * squareSize + margin, + squareSize, + squareSize + ); + + ctx.filter = "none"; + } }; export default drawPieces; diff --git a/src/board/styles-board/mono/chesscom.ts b/src/board/styles-board/mono/chesscom.ts index c55ed74..4f868d9 100644 --- a/src/board/styles-board/mono/chesscom.ts +++ b/src/board/styles-board/mono/chesscom.ts @@ -28,7 +28,7 @@ const style: Style = { border: { type: "solid", data: { - color: "#40522f", + color: "#312e2b", }, }, coords: { diff --git a/src/encoders/createAnimation.ts b/src/encoders/createAnimation.ts index a08ee3f..c58a6e5 100644 --- a/src/encoders/createAnimation.ts +++ b/src/encoders/createAnimation.ts @@ -27,7 +27,7 @@ const createAnimation = async ( ? new MP4(board.width, board.height) : new WebM(); - const header = game.getHeader(); + const header = game.header; await board.titleFrame(header); board.render(); @@ -35,25 +35,12 @@ const createAnimation = async ( // @ts-ignore await encoder.add(getData(board, encoder), 4); - await board.frame(game.getBoardData(), header, null, game.materialInfo()); - board.render(); - // @ts-ignore - await encoder.add(getData(board, encoder), 1); - - while (true) { - const move = game.next(); - - // console.log(move); - - if (!move) { - break; - } - - await board.frame(game.getBoardData(), header, move, game.materialInfo()); + for (let ply = 0; ply < game.length; ply++) { + const position = game.getPosition(ply); + await board.frame(position, header); board.render(); - // @ts-ignore - await encoder.add(getData(board, encoder), move.end === 0 ? 5 : 1); + await encoder.add(getData(board, encoder), position.end === 0 ? 5 : 1); } return await encoder.render(); diff --git a/src/game/Game.ts b/src/game/Game.ts index ae170af..b9aa100 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -1,9 +1,7 @@ -import { PieceType } from "../types"; -import { Chess, ChessInstance, Move } from "chess.js"; +import { PieceType, PieceColor, BoardData, Position } from "../types"; +import { Chess, ChessInstance } from "chess.js"; import { cleanPGN } from "./PGNHelpers"; -export type MoveWithDetails = Move & { ply: number; end: number; fen: string }; - const MATERIAL_VALUE: Map = new Map([ ["q", 9], ["r", 5], @@ -13,110 +11,96 @@ const MATERIAL_VALUE: Map = new Map([ ]); class Game { - private game: ChessInstance; - private replay: ChessInstance; - private moves: MoveWithDetails[]; - private currentPly: number = 0; + private positions: Position[] = []; + private game: ChessInstance = new Chess(); - constructor() { - this.game = new Chess(); - this.replay = new Chess(); - this.moves = []; - } - - getMoves() { - return this.moves.map((move) => move.san); - } - - getFEN() { - return this.replay.fen(); - } + constructor() {} loadPGN(pgn: string) { - this.game.load_pgn(cleanPGN(pgn)); - this.game.delete_comments(); + const game = new Chess(); + const replay = new Chess(); - const moves = this.game.history({ verbose: true }); - const tempGame = new Chess(); - const fen = this.game.header().FEN; + game.load_pgn(cleanPGN(pgn)); + game.delete_comments(); + + const moves = game.history({ verbose: true }); + const fen = game.header().FEN; if (fen) { - tempGame.load(fen); - this.replay.load(fen); + replay.load(fen); } - this.moves = moves.map((item, i) => { - tempGame.move(item); + this.positions = [ + { + ply: 0, + move: null, + end: moves.length, + fen: replay.fen(), + check: replay.in_check(), + mate: replay.in_checkmate(), + turn: replay.turn(), + material: this.materialInfo(replay.board()), + placement: this.getPlacement(replay.fen()), + last: moves.length === 0, + }, + ]; - return { - ...item, + moves.forEach((item, i) => { + replay.move(item); + + const currentFEN = replay.fen(); + + this.positions.push({ ply: i + 1, + move: item, end: moves.length - 1 - i, - fen: tempGame.fen(), - }; + fen: currentFEN, + check: replay.in_check(), + mate: replay.in_checkmate(), + turn: replay.turn(), + material: this.materialInfo(replay.board()), + placement: this.getPlacement(currentFEN), + last: i === moves.length - 1, + }); }); - this.currentPly = 0; - + this.game = game; return this; } loadFEN(fen: string) { - this.game.load(fen); - return this; + this.game = new Chess(fen); + this.positions = []; } - next() { - const move = this.moves[this.currentPly]; + private getPlacement(fen: string) { + const board = new Chess(fen).board(); - if (!move) { - return null; + const placement: { + x: number; + y: number; + type: PieceType; + color: PieceColor; + }[] = []; + + for (let y = 0; y < 8; y++) { + for (let x = 0; x < 8; x++) { + if (board[y][x] !== null) { + const { type, color } = board[y][x] as { + type: PieceType; + color: PieceColor; + }; + + placement.push({ x, y, type, color }); + } + } } - this.currentPly++; - this.replay.move(move); - - return move; + return placement; } - prev() { - const undo = Boolean(this.replay.undo()); - - if (undo) { - this.currentPly--; - } else { - return null; - } - - const move = this.moves[this.currentPly - 1]; - - return move; - } - - last() { - while (this.next()) {} - return this.moves[this.moves.length - 1]; - } - - first() { - while (this.prev()) {} - return this.moves[0]; - } - - goto(ply: number) { - if (ply === this.currentPly || ply < 0 || ply > this.moves.length - 1) { - return null; - } - - while (this.currentPly !== ply) { - ply > this.currentPly ? this.next() : this.prev(); - } - - return this.moves[ply - 1]; - } - - materialInfo() { - const pieces = this.replay.board().flat().filter(Boolean); + private materialInfo(boardData: BoardData) { + const pieces = boardData.flat().filter(Boolean); const sum = { w: 0, b: 0 }; const imbalance = { @@ -146,20 +130,23 @@ class Game { return { sum, imbalance, count, diff: sum.w - sum.b }; } - getBoardData() { - // console.log(this.replay.ascii()); - return this.replay.board(); + get length() { + return this.positions.length; } - getHeader() { + get header() { return this.game.header(); } - pgn() { - return this.game.pgn(); + getPosition(ply: number) { + const position = this.positions[ply]; + + return position ?? null; + } + + getMoves() { + return this.positions.slice(1).map(({ move }) => move?.san) as string[]; } } -export type Material = ReturnType; - export default Game; diff --git a/src/game/Game_x.ts b/src/game/Game_x.ts deleted file mode 100644 index 5135679..0000000 --- a/src/game/Game_x.ts +++ /dev/null @@ -1,150 +0,0 @@ -// import { PieceType, BoardData } from "../types"; -// import { Chess, ChessInstance, Move } from "chess.js"; -// import { cleanPGN } from "./PGNHelpers"; - -// export type MoveWithDetails = Move & { ply: number; end: number; fen: string }; - -// const MATERIAL_VALUE: Map = new Map([ -// ["q", 9], -// ["r", 5], -// ["b", 3], -// ["n", 3], -// ["p", 1], -// ]); - -// class Game { -// private moves: MoveWithDetails[]; -// private currentPly: number = 0; - -// constructor() { -// this.moves = []; -// } - -// getMoves() { -// return this.moves.map((move) => move.san); -// } - -// loadPGN(pgn: string) { -// const game = new Chess(); -// const replay = new Chess(); - -// game.load_pgn(cleanPGN(pgn)); -// game.delete_comments(); - -// const moves = game.history({ verbose: true }); -// const fen = game.header().FEN; - -// if (fen) { -// replay.load(fen); -// } - -// this.moves = moves.map((item, i) => { -// replay.move(item); - -// const currentFEN = replay.fen(); - -// return { -// ...item, -// ply: i + 1, -// end: moves.length - 1 - i, -// fen: currentFEN, -// material: this.materialInfo(replay.board()), -// }; -// }); - -// this.currentPly = 0; - -// return this; -// } - -// next() { -// // const move = this.moves[this.currentPly]; -// // if (!move) { -// // return null; -// // } -// // this.currentPly++; -// // this.replay.move(move); -// // return move; -// } - -// prev() { -// // const undo = Boolean(this.replay.undo()); -// // if (undo) { -// // this.currentPly--; -// // } else { -// // return null; -// // } -// // const move = this.moves[this.currentPly - 1]; -// // return move; -// } - -// last() { -// while (this.next()) {} -// return this.moves[this.moves.length - 1]; -// } - -// first() { -// while (this.prev()) {} -// return this.moves[0]; -// } - -// goto(ply: number) { -// if (ply === this.currentPly || ply < 0 || ply > this.moves.length - 1) { -// return null; -// } - -// while (this.currentPly !== ply) { -// ply > this.currentPly ? this.next() : this.prev(); -// } - -// return this.moves[ply - 1]; -// } - -// materialInfo(boardData: BoardData) { -// const pieces = boardData.flat().filter(Boolean); - -// const sum = { w: 0, b: 0 }; -// const imbalance = { -// w: { p: 0, n: 0, b: 0, r: 0, q: 0 }, -// b: { p: 0, n: 0, b: 0, r: 0, q: 0 }, -// }; -// const count = { -// w: { p: 0, n: 0, b: 0, r: 0, q: 0 }, -// b: { p: 0, n: 0, b: 0, r: 0, q: 0 }, -// }; - -// for (const piece of pieces) { -// if (piece !== null && piece.type !== "k") { -// sum[piece.color] += MATERIAL_VALUE.get(piece.type) ?? 0; -// count[piece.color][piece.type] += 1; - -// const oppositeColor = piece.color === "b" ? "w" : "b"; - -// if (imbalance[oppositeColor][piece.type] === 0) { -// imbalance[piece.color][piece.type] += 1; -// } else { -// imbalance[oppositeColor][piece.type] -= 1; -// } -// } -// } - -// return { sum, imbalance, count, diff: sum.w - sum.b }; -// } - -// getBoardData() { -// // return this.replay.board(); -// return new Chess().board(); -// } - -// getHeader() { -// return this.game.header(); -// } - -// pgn() { -// return this.game.pgn(); -// } -// } - -// export type Material = ReturnType; - -// export default Game; diff --git a/src/main.ts b/src/main.ts index 64cd98d..b629df6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,20 +1,22 @@ import { BoardConfig, GameConfig } from "./types"; import "./style.css"; import Board from "./board/Board"; -import styles from "./board/styles-board"; import Game from "./game/Game"; +import styles from "./board/styles-board"; import pgns from "./test-data/pgns"; import createAnimation from "./encoders/createAnimation"; // import { decompressPGN } from "./game/PGNHelpers"; import WebFont from "webfontloader"; import Player from "./player/Player"; import * as Hammer from "hammerjs"; +import Moves from "./ui/moves/Moves"; -const $app = document.querySelector("#app"); +const $board = document.querySelector("#board"); +const $moves = document.querySelector("#moves"); const boardConfig: BoardConfig = { size: 1024, - boardStyle: styles.chesscom, + boardStyle: styles.lila, piecesStyle: "tatiana", showBorder: true, showExtraInfo: true, @@ -33,7 +35,7 @@ const gameConfig: GameConfig = { }; const createDownloadLink = async (pgn: string, boardConfig: BoardConfig) => { - const file = await createAnimation(pgn, boardConfig, "GIF"); + const file = await createAnimation(pgn, { ...boardConfig, size: 720 }, "GIF"); const link = document.createElement("a"); link.innerText = "DOWNLOAD"; link.setAttribute("href", URL.createObjectURL(file)); @@ -53,7 +55,7 @@ const main = async () => { // const pgn = pgns[2]; const board = new Board(boardConfig); - $app?.appendChild(board.canvas); + $board?.appendChild(board.canvas); // const interval = 1000; // play(board, gameConfig, pgn, interval); @@ -61,12 +63,15 @@ const main = async () => { const player = new Player(board, gameConfig); const game = new Game().loadPGN(pgn); + const moves = new Moves($moves as HTMLElement, player).load(game.getMoves()); + + // @ts-ignore + window.game = game; + await player.load(game); // @ts-ignore window.player = player; - // @ts-ignore - window.game = game; // @ts-ignore window.load = async (pgn: string) => { diff --git a/src/pattern-light.png b/src/pattern-light.png new file mode 100644 index 0000000..6a5f0d7 Binary files /dev/null and b/src/pattern-light.png differ diff --git a/src/pattern-light.png~ b/src/pattern-light.png~ new file mode 100644 index 0000000..3e47619 Binary files /dev/null and b/src/pattern-light.png~ differ diff --git a/src/player/Player.ts b/src/player/Player.ts index 39fe8c6..21ccdd2 100644 --- a/src/player/Player.ts +++ b/src/player/Player.ts @@ -7,15 +7,14 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); class Player { private interval = 1000; private game: Game = new Game(); - private header: { [key: string]: string | undefined } = {}; - private start: boolean = true; - private end: boolean = false; + private ply: number = 0; public playing: boolean = false; + private firstRender: Promise; constructor(private board: Board, private config: GameConfig) { this.firstRender = this.board - .frame(this.game.getBoardData(), {}, null) + .frame(this.game.getPosition(0), this.game.header) .then((_) => this.board.render()); } @@ -28,9 +27,9 @@ class Player { await this.firstRender; this.game = game; - this.header = game.getHeader(); + this.ply = -1; - await this.board.titleFrame(this.game.getHeader()); + await this.board.titleFrame(this.game.header); this.board.render(); } @@ -52,103 +51,56 @@ class Player { } async prev() { - if (this.start) { + const ply = this.ply - 1; + + if (ply < -1) { return; } - this.end = false; + this.ply = ply; - const move = this.game.prev(); - - if (!move) { - await this.board.titleFrame(this.header); + if (ply === -1) { + await this.board.titleFrame(this.game.header); this.board.render(); - this.start = true; - return; } - await this.board.frame( - this.game.getBoardData(), - this.header, - move, - this.game.materialInfo() - ); + await this.board.frame(this.game.getPosition(ply), this.game.header); this.board.render(); } async next() { - if (this.end) { + const ply = this.ply + 1; + + if (ply >= this.game.length) { return; } - if (this.start) { - await this.board.frame( - this.game.getBoardData(), - this.header, - null, - this.game.materialInfo() - ); - this.board.render(); - this.start = false; - return; - } + this.ply = ply; - const move = this.game.next(); - - this.end = move?.end === 0; - - if (!move) { - return; - } - - await this.board.frame( - this.game.getBoardData(), - this.header, - move, - this.game.materialInfo() - ); + await this.board.frame(this.game.getPosition(ply), this.game.header); this.board.render(); } async first() { - this.game.first(); + this.ply = 0; - await this.board.titleFrame(this.header); + await this.board.frame(this.game.getPosition(this.ply), this.game.header); this.board.render(); - - this.end = false; - this.start = true; } async last() { - const move = this.game.last(); + this.ply = this.game.length - 1; - await this.board.frame( - this.game.getBoardData(), - this.header, - move, - this.game.materialInfo() - ); + await this.board.frame(this.game.getPosition(this.ply), this.game.header); this.board.render(); - - this.end = true; - this.start = false; } async goto(ply: number) { - const move = this.game.goto(ply); + this.ply = ply; - await this.board.frame( - this.game.getBoardData(), - this.header, - move, - this.game.materialInfo() - ); + await this.board.frame(this.game.getPosition(this.ply), this.game.header); this.board.render(); - - this.start = false; - this.end = move?.end === 0; } } diff --git a/src/style.css b/src/style.css index 292ec57..a03b01d 100644 --- a/src/style.css +++ b/src/style.css @@ -10,12 +10,19 @@ } body { - background-color: #191d24; - background-image: url(pattern.png); background-repeat: repeat; background-position: center; text-align: center; - padding-top: 2.5vh; +} + +.dark { + background-color: #191d24; + background-image: url(pattern.png); +} + +.light { + background-color: #cfcfcf; + background-image: url(pattern-light.png); } .board { @@ -30,3 +37,23 @@ body { max-width: 100%; max-height: 95vh; } + +.layout { + display: grid; + grid-template-columns: 1fr 2fr 1fr; +} + +.moves-box { + background: rgba(255, 192, 203, 0.5); + height: 100vh; +} + +.board-box { + background: rgba(255, 166, 0, 0.5); + height: 100vh; +} + +.setup-box { + background: rgba(135, 207, 235, 0.5); + height: 100vh; +} diff --git a/src/types.ts b/src/types.ts index 6f51768..be2f7ea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { Move } from "chess.js"; + export type GradientDir = | "horizontal" | "vertical" @@ -112,3 +114,50 @@ export type GameConfig = { toPly: number | null; loop: boolean; }; + +export type MaterialCount = { + w: { + p: number; + n: number; + b: number; + r: number; + q: number; + }; + b: { + p: number; + n: number; + b: number; + r: number; + q: number; + }; +}; + +export type Material = { + sum: { + w: number; + b: number; + }; + imbalance: MaterialCount; + count: MaterialCount; + diff: number; +}; + +export type Placement = { + x: number; + y: number; + type: PieceType; + color: PieceColor; +}[]; + +export type Position = { + move: Move | null; + ply: number; + end: number; + fen: string; + check: boolean; + mate: boolean; + turn: PieceColor; + material: Material; + placement: Placement; + last: boolean; +}; diff --git a/src/ui/moves/Moves.ts b/src/ui/moves/Moves.ts new file mode 100644 index 0000000..3e14f99 --- /dev/null +++ b/src/ui/moves/Moves.ts @@ -0,0 +1,36 @@ +import { html } from "common-tags"; +import Player from "../../player/Player"; +import chunk_ from "@arrows/array/chunk_"; + +class Moves { + constructor(private element: HTMLElement, private player: Player) { + this.element.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + + if (target?.dataset?.type === "ply") { + const ply = Number(target?.dataset?.ply); + this.player.goto(ply); + } + }); + } + + load(moves: string[]) { + const items = chunk_(2, moves).map( + ([white, black], i) => + html`

+ ${i + 1}. ${white} + ${black} +

` + ); + + const content = html`
+ ${items.join("\n")} + `; + + this.element.innerHTML = content; + + return this; + } +} + +export default Moves;