@hackersheet/next-document-content-kifu 0.1.0-alpha.16 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/shogi-player/adapters/kifu-adapter.d.ts +11 -0
- package/dist/cjs/components/shogi-player/adapters/kifu-adapter.js +104 -0
- package/dist/cjs/components/shogi-player/board-renderer.d.ts +60 -0
- package/dist/cjs/components/shogi-player/board-renderer.js +137 -0
- package/dist/cjs/components/shogi-player/button.d.ts +22 -1
- package/dist/cjs/components/shogi-player/button.js +2 -1
- package/dist/cjs/components/shogi-player/canvas-utils.d.ts +29 -0
- package/dist/cjs/components/shogi-player/canvas-utils.js +48 -0
- package/dist/cjs/components/shogi-player/hands-renderer.d.ts +42 -0
- package/dist/cjs/components/shogi-player/hands-renderer.js +86 -0
- package/dist/cjs/components/shogi-player/moves-area.d.ts +23 -2
- package/dist/cjs/components/shogi-player/moves-area.js +14 -15
- package/dist/cjs/components/shogi-player/shogi-board-canvas.d.ts +20 -13
- package/dist/cjs/components/shogi-player/shogi-board-canvas.js +9 -119
- package/dist/cjs/components/shogi-player/shogi-hands-canvas.d.ts +20 -12
- package/dist/cjs/components/shogi-player/shogi-hands-canvas.js +7 -79
- package/dist/cjs/components/shogi-player/shogi-player.d.ts +22 -0
- package/dist/cjs/components/shogi-player/shogi-player.js +50 -40
- package/dist/cjs/components/shogi-player/types.d.ts +169 -0
- package/dist/cjs/components/shogi-player/types.js +16 -0
- package/dist/esm/components/shogi-player/adapters/kifu-adapter.d.mts +11 -0
- package/dist/esm/components/shogi-player/adapters/kifu-adapter.mjs +80 -0
- package/dist/esm/components/shogi-player/board-renderer.d.mts +60 -0
- package/dist/esm/components/shogi-player/board-renderer.mjs +109 -0
- package/dist/esm/components/shogi-player/button.d.mts +22 -1
- package/dist/esm/components/shogi-player/button.mjs +2 -1
- package/dist/esm/components/shogi-player/canvas-utils.d.mts +29 -0
- package/dist/esm/components/shogi-player/canvas-utils.mjs +22 -0
- package/dist/esm/components/shogi-player/hands-renderer.d.mts +42 -0
- package/dist/esm/components/shogi-player/hands-renderer.mjs +60 -0
- package/dist/esm/components/shogi-player/moves-area.d.mts +23 -2
- package/dist/esm/components/shogi-player/moves-area.mjs +15 -16
- package/dist/esm/components/shogi-player/shogi-board-canvas.d.mts +20 -13
- package/dist/esm/components/shogi-player/shogi-board-canvas.mjs +12 -116
- package/dist/esm/components/shogi-player/shogi-hands-canvas.d.mts +20 -12
- package/dist/esm/components/shogi-player/shogi-hands-canvas.mjs +4 -76
- package/dist/esm/components/shogi-player/shogi-player.d.mts +22 -0
- package/dist/esm/components/shogi-player/shogi-player.mjs +51 -41
- package/dist/esm/components/shogi-player/types.d.mts +169 -0
- package/dist/esm/components/shogi-player/types.mjs +0 -0
- package/package.json +10 -10
|
@@ -1,117 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { JKFPlayer } from "json-kifu-format";
|
|
3
2
|
import React from "react";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const cell = boardSize / 9;
|
|
13
|
-
return { margin, boardSize, cell };
|
|
14
|
-
};
|
|
15
|
-
const drawBackground = (ctx, size, boardColor) => {
|
|
16
|
-
ctx.fillStyle = boardColor;
|
|
17
|
-
ctx.fillRect(0, 0, size, size);
|
|
18
|
-
};
|
|
19
|
-
const drawBoard = (ctx, margin, boardSize, lineColor) => {
|
|
20
|
-
ctx.strokeStyle = lineColor;
|
|
21
|
-
ctx.lineWidth = 2;
|
|
22
|
-
ctx.strokeRect(margin, margin, boardSize, boardSize);
|
|
23
|
-
ctx.lineWidth = 1;
|
|
24
|
-
Array.from({ length: 8 }).forEach((_, i) => {
|
|
25
|
-
const offset = (i + 1) * (boardSize / 9);
|
|
26
|
-
ctx.beginPath();
|
|
27
|
-
ctx.moveTo(margin + offset, margin);
|
|
28
|
-
ctx.lineTo(margin + offset, margin + boardSize);
|
|
29
|
-
ctx.stroke();
|
|
30
|
-
ctx.beginPath();
|
|
31
|
-
ctx.moveTo(margin, margin + offset);
|
|
32
|
-
ctx.lineTo(margin + boardSize, margin + offset);
|
|
33
|
-
ctx.stroke();
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
const drawPieces = (ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente) => {
|
|
37
|
-
ctx.textAlign = "center";
|
|
38
|
-
ctx.textBaseline = "middle";
|
|
39
|
-
pieces.forEach((row, rowIndex) => {
|
|
40
|
-
row.forEach((piece, colIndex) => {
|
|
41
|
-
if (!piece) return;
|
|
42
|
-
const x = isSente ? 8 - rowIndex : rowIndex;
|
|
43
|
-
const y = isSente ? colIndex : 8 - colIndex;
|
|
44
|
-
const px = margin + x * cell + cell / 2;
|
|
45
|
-
const py = margin + y * cell + cell / 2;
|
|
46
|
-
ctx.save();
|
|
47
|
-
ctx.translate(px, py);
|
|
48
|
-
if (isSente && piece.color === Color.White) {
|
|
49
|
-
ctx.rotate(Math.PI);
|
|
50
|
-
} else if (!isSente && piece.color === Color.Black) {
|
|
51
|
-
ctx.rotate(Math.PI);
|
|
52
|
-
}
|
|
53
|
-
const kan = JKFPlayer.kindToKan(piece.kind);
|
|
54
|
-
ctx.fillStyle = "#000";
|
|
55
|
-
if (kan.length === 2) {
|
|
56
|
-
const baseFontSize = cell * fontSizeRatio * 0.5;
|
|
57
|
-
ctx.font = `${baseFontSize}px ${fontFamily}`;
|
|
58
|
-
const scaleX = 2;
|
|
59
|
-
const scaleY = 1;
|
|
60
|
-
const offsetY = cell * 0.18;
|
|
61
|
-
ctx.save();
|
|
62
|
-
ctx.scale(scaleX, scaleY);
|
|
63
|
-
ctx.fillText(kan[0], 0 / scaleX, -offsetY / scaleY);
|
|
64
|
-
ctx.restore();
|
|
65
|
-
ctx.save();
|
|
66
|
-
ctx.scale(scaleX, scaleY);
|
|
67
|
-
ctx.fillText(kan[1], 0 / scaleX, offsetY / scaleY);
|
|
68
|
-
ctx.restore();
|
|
69
|
-
} else {
|
|
70
|
-
const fontSize = cell * fontSizeRatio;
|
|
71
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
72
|
-
ctx.fillText(kan, 0, 0);
|
|
73
|
-
}
|
|
74
|
-
ctx.restore();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const drawCoordinates = (ctx, margin, boardSize, cell, fontFamily, isSente) => {
|
|
79
|
-
ctx.fillStyle = "#000";
|
|
80
|
-
ctx.font = `${cell * 0.35}px ${fontFamily}`;
|
|
81
|
-
ctx.textAlign = "center";
|
|
82
|
-
ctx.textBaseline = "middle";
|
|
83
|
-
Array.from({ length: 9 }).forEach((_, i) => {
|
|
84
|
-
const x = margin + i * cell + cell / 2;
|
|
85
|
-
const y = margin / 2;
|
|
86
|
-
const label = isSente ? JKFPlayer.numToZen(9 - i) : JKFPlayer.numToZen(i + 1);
|
|
87
|
-
ctx.fillText(label, x, y);
|
|
88
|
-
});
|
|
89
|
-
Array.from({ length: 9 }).forEach((_, i) => {
|
|
90
|
-
const x = margin + boardSize + margin / 2;
|
|
91
|
-
const y = margin + i * cell + cell / 2;
|
|
92
|
-
const label = isSente ? JKFPlayer.numToKan(i + 1) : JKFPlayer.numToKan(9 - i);
|
|
93
|
-
ctx.fillText(label, x, y);
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
const drawHighlightedCell = (ctx, margin, cell, isSente, currentMove) => {
|
|
97
|
-
if (!currentMove) return;
|
|
98
|
-
if (currentMove.to) {
|
|
99
|
-
const toRow = currentMove.to.x - 1;
|
|
100
|
-
const toCol = currentMove.to.y - 1;
|
|
101
|
-
const toX = isSente ? 8 - toRow : toRow;
|
|
102
|
-
const toY = isSente ? toCol : 8 - toCol;
|
|
103
|
-
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
104
|
-
ctx.fillRect(margin + toX * cell, margin + toY * cell, cell, cell);
|
|
105
|
-
}
|
|
106
|
-
if (currentMove.from) {
|
|
107
|
-
const fromRow = currentMove.from.x - 1;
|
|
108
|
-
const fromCol = currentMove.from.y - 1;
|
|
109
|
-
const fromX = isSente ? 8 - fromRow : fromRow;
|
|
110
|
-
const fromY = isSente ? fromCol : 8 - fromCol;
|
|
111
|
-
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
112
|
-
ctx.fillRect(margin + fromX * cell, margin + fromY * cell, cell, cell);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
3
|
+
import {
|
|
4
|
+
drawBoardBackground,
|
|
5
|
+
drawBoardGrid,
|
|
6
|
+
drawBoardPieces,
|
|
7
|
+
drawBoardCoordinates,
|
|
8
|
+
drawHighlightedCell
|
|
9
|
+
} from "./board-renderer.mjs";
|
|
10
|
+
import { getCanvasDimensions, getBoardLayout } from "./canvas-utils.mjs";
|
|
115
11
|
const ShogiBoardCanvas = ({
|
|
116
12
|
size = 360,
|
|
117
13
|
boardColor = "#f9d27a",
|
|
@@ -136,11 +32,11 @@ const ShogiBoardCanvas = ({
|
|
|
136
32
|
ctx.scale(dpr, dpr);
|
|
137
33
|
ctx.clearRect(0, 0, size, size);
|
|
138
34
|
const { margin, boardSize, cell } = getBoardLayout(size);
|
|
139
|
-
|
|
140
|
-
|
|
35
|
+
drawBoardBackground(ctx, size, boardColor);
|
|
36
|
+
drawBoardGrid(ctx, margin, boardSize, lineColor);
|
|
141
37
|
drawHighlightedCell(ctx, margin, cell, isSente, currentMove);
|
|
142
|
-
|
|
143
|
-
|
|
38
|
+
drawBoardPieces(ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente);
|
|
39
|
+
drawBoardCoordinates(ctx, margin, boardSize, cell, fontFamily, isSente);
|
|
144
40
|
}, [size, boardColor, lineColor, fontFamily, fontSizeRatio, pieces, isSente, currentMove]);
|
|
145
41
|
return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef, width: size, height: size });
|
|
146
42
|
};
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ShogiHandsCanvasProps } from './types.mjs';
|
|
3
|
+
import 'shogi.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Canvas component for rendering captured pieces (hands)
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @param props - Component props
|
|
10
|
+
* @returns Canvas element displaying the hands (captured pieces area)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <ShogiHandsCanvas
|
|
15
|
+
* size={360}
|
|
16
|
+
* hands={capturedPieces}
|
|
17
|
+
* isSente={true}
|
|
18
|
+
* isTop={true}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare const ShogiHandsCanvas: React.FC<ShogiHandsCanvasProps>;
|
|
15
23
|
|
|
16
24
|
export { ShogiHandsCanvas as default };
|
|
@@ -1,78 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { JKFPlayer } from "json-kifu-format";
|
|
3
2
|
import React from "react";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const dpr = window.devicePixelRatio || 1;
|
|
7
|
-
return { width: size * dpr, height: size * dpr, dpr };
|
|
8
|
-
};
|
|
9
|
-
const getHandsLayout = (size) => {
|
|
10
|
-
const margin = size * 0.06;
|
|
11
|
-
const boardSize = size - margin * 2;
|
|
12
|
-
const cellSize = boardSize / 9;
|
|
13
|
-
const handsHeight = cellSize + margin + 2;
|
|
14
|
-
return { margin, handsHeight, boardSize, cellSize };
|
|
15
|
-
};
|
|
16
|
-
const drawBackground = (ctx, width, height, color) => {
|
|
17
|
-
ctx.fillStyle = color;
|
|
18
|
-
ctx.fillRect(0, 0, width, height);
|
|
19
|
-
};
|
|
20
|
-
const drawHandsFrame = (ctx, x, y, width, height, lineColor, isTop) => {
|
|
21
|
-
ctx.strokeStyle = lineColor;
|
|
22
|
-
ctx.lineWidth = 2;
|
|
23
|
-
if (isTop) {
|
|
24
|
-
ctx.strokeRect(x, y, width, height);
|
|
25
|
-
} else {
|
|
26
|
-
ctx.strokeRect(x, 2, width, height);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const drawCoordinates = (ctx, fontSize, fontFamily) => {
|
|
30
|
-
ctx.fillStyle = "#000";
|
|
31
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
32
|
-
ctx.textAlign = "center";
|
|
33
|
-
ctx.textBaseline = "middle";
|
|
34
|
-
};
|
|
35
|
-
const drawPieces = (ctx, hands, margin, boardSize, cell, fontFamily, fontSizeRatio, isSente, isTop) => {
|
|
36
|
-
ctx.textAlign = "center";
|
|
37
|
-
ctx.textBaseline = "middle";
|
|
38
|
-
const pieces = isSente && isTop || !isSente && !isTop ? hands[Color.White] : hands[Color.Black];
|
|
39
|
-
if (!pieces || pieces.length === 0) return;
|
|
40
|
-
const grouped = pieces.reduce(
|
|
41
|
-
(acc, piece) => {
|
|
42
|
-
if (!piece) return acc;
|
|
43
|
-
const key = piece.kind;
|
|
44
|
-
if (!acc[key]) acc[key] = { count: 0, color: piece.color };
|
|
45
|
-
acc[key].count += 1;
|
|
46
|
-
return acc;
|
|
47
|
-
},
|
|
48
|
-
{}
|
|
49
|
-
);
|
|
50
|
-
const order = ["OU", "HI", "KA", "KI", "GI", "KE", "KY", "FU"];
|
|
51
|
-
const kinds = order.filter((kind) => grouped[kind]);
|
|
52
|
-
kinds.forEach((kind, index) => {
|
|
53
|
-
const { count, color } = grouped[kind];
|
|
54
|
-
const px = isTop ? margin + boardSize - (index * cell + cell / 2) : margin + index * cell + cell / 2;
|
|
55
|
-
const py = isTop ? margin + cell / 2 : cell / 2 + 2;
|
|
56
|
-
ctx.save();
|
|
57
|
-
ctx.translate(px, py);
|
|
58
|
-
if (isSente && color === Color.White) {
|
|
59
|
-
ctx.rotate(Math.PI);
|
|
60
|
-
} else if (!isSente && color === Color.Black) {
|
|
61
|
-
ctx.rotate(Math.PI);
|
|
62
|
-
}
|
|
63
|
-
const fontSize = cell * fontSizeRatio;
|
|
64
|
-
const kan = JKFPlayer.kindToKan(kind);
|
|
65
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
66
|
-
ctx.fillText(kan, 0, 0);
|
|
67
|
-
if (count > 1) {
|
|
68
|
-
ctx.font = `${fontSize * 0.5}px ${fontFamily}`;
|
|
69
|
-
const countOffsetX = cell * 0;
|
|
70
|
-
const countOffsetY = cell * 0.8;
|
|
71
|
-
ctx.fillText(String(count), countOffsetX, countOffsetY);
|
|
72
|
-
}
|
|
73
|
-
ctx.restore();
|
|
74
|
-
});
|
|
75
|
-
};
|
|
3
|
+
import { getCanvasDimensions, getHandsLayout } from "./canvas-utils.mjs";
|
|
4
|
+
import { drawHandsBackground, drawHandsFrame, drawHandsPieces } from "./hands-renderer.mjs";
|
|
76
5
|
const ShogiHandsCanvas = ({
|
|
77
6
|
size = 360,
|
|
78
7
|
boardColor = "#f9d27a",
|
|
@@ -97,10 +26,9 @@ const ShogiHandsCanvas = ({
|
|
|
97
26
|
canvas.style.height = `${handsHeight}px`;
|
|
98
27
|
ctx.scale(dpr, dpr);
|
|
99
28
|
ctx.clearRect(0, 0, size, handsHeight);
|
|
100
|
-
|
|
29
|
+
drawHandsBackground(ctx, size, handsHeight, boardColor);
|
|
101
30
|
drawHandsFrame(ctx, margin, margin, boardSize, cellSize, lineColor, isTop);
|
|
102
|
-
|
|
103
|
-
drawPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
|
|
31
|
+
drawHandsPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
|
|
104
32
|
}, [size, boardColor, lineColor, fontFamily, fontSizeRatio, isSente, hands, isTop]);
|
|
105
33
|
return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef });
|
|
106
34
|
};
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { KifuAdapterFactory } from './types.mjs';
|
|
3
|
+
import 'shogi.js';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for the ShogiPlayer component
|
|
7
|
+
* @property kifuText - KIF format kifu (game record) text
|
|
8
|
+
* @property size - Canvas size in CSS pixels (default: 360)
|
|
9
|
+
* @property tesuu - Initial move number to display
|
|
10
|
+
* @property adapterFactory - Factory function for creating KifuAdapter (for DI/testing)
|
|
11
|
+
*/
|
|
3
12
|
type ShogiPlayerProps = {
|
|
4
13
|
kifuText: string;
|
|
5
14
|
size?: number;
|
|
6
15
|
tesuu?: number;
|
|
16
|
+
adapterFactory?: KifuAdapterFactory;
|
|
7
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Shogi game viewer component with board, hands, and move navigation
|
|
20
|
+
*
|
|
21
|
+
* @component
|
|
22
|
+
* @param props - Component props
|
|
23
|
+
* @returns Interactive shogi player with keyboard and button navigation
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <ShogiPlayer kifuText={kifString} tesuu={10} />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
8
30
|
declare function ShogiPlayer(props: ShogiPlayerProps): React.JSX.Element;
|
|
9
31
|
|
|
10
32
|
export { type ShogiPlayerProps, ShogiPlayer as default };
|
|
@@ -1,46 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createKifuAdapter } from "./adapters/kifu-adapter.mjs";
|
|
4
4
|
import Button from "./button.mjs";
|
|
5
5
|
import MovesArea from "./moves-area.mjs";
|
|
6
6
|
import ShogiBoardCanvas from "./shogi-board-canvas.mjs";
|
|
7
7
|
import ShogiHandsCanvas from "./shogi-hands-canvas.mjs";
|
|
8
8
|
function ShogiPlayer(props) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const [
|
|
12
|
-
const [moves, setMoves] = useState(player.kifu.moves);
|
|
13
|
-
const [tesuu, setTesuu] = useState(player.tesuu);
|
|
14
|
-
const [currentMove, setCurrentMove] = useState(player.getMove());
|
|
9
|
+
const factory = props.adapterFactory ?? createKifuAdapter;
|
|
10
|
+
const adapterRef = useRef(factory(props.kifuText));
|
|
11
|
+
const [gameState, setGameState] = useState(() => adapterRef.current.getState());
|
|
15
12
|
const [isSente, setIsSente] = useState(true);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
32
|
-
const handleBackward = () => {
|
|
33
|
-
player.backward();
|
|
34
|
-
updateState();
|
|
35
|
-
};
|
|
36
|
-
const handleGoto = (tesuu2) => {
|
|
37
|
-
player.goto(tesuu2);
|
|
38
|
-
updateState();
|
|
39
|
-
};
|
|
40
|
-
const handeleToggle = () => {
|
|
41
|
-
setIsSente(!isSente);
|
|
42
|
-
updateState();
|
|
43
|
-
};
|
|
13
|
+
const size = props.size ?? 360;
|
|
14
|
+
const handleForward = useCallback(() => {
|
|
15
|
+
const newState = adapterRef.current.forward();
|
|
16
|
+
setGameState(newState);
|
|
17
|
+
}, []);
|
|
18
|
+
const handleBackward = useCallback(() => {
|
|
19
|
+
const newState = adapterRef.current.backward();
|
|
20
|
+
setGameState(newState);
|
|
21
|
+
}, []);
|
|
22
|
+
const handleGoto = useCallback((tesuu) => {
|
|
23
|
+
const newState = adapterRef.current.goto(tesuu);
|
|
24
|
+
setGameState(newState);
|
|
25
|
+
}, []);
|
|
26
|
+
const handleToggle = useCallback(() => {
|
|
27
|
+
setIsSente((prev) => !prev);
|
|
28
|
+
}, []);
|
|
44
29
|
const handleKeydown = useCallback(
|
|
45
30
|
(event) => {
|
|
46
31
|
event.preventDefault();
|
|
@@ -52,7 +37,7 @@ function ShogiPlayer(props) {
|
|
|
52
37
|
break;
|
|
53
38
|
case "Down":
|
|
54
39
|
case "ArrowDown":
|
|
55
|
-
handleGoto(
|
|
40
|
+
handleGoto(gameState.maxMoveIndex);
|
|
56
41
|
break;
|
|
57
42
|
case "Left":
|
|
58
43
|
case "ArrowLeft":
|
|
@@ -64,18 +49,43 @@ function ShogiPlayer(props) {
|
|
|
64
49
|
handleForward();
|
|
65
50
|
break;
|
|
66
51
|
case "r":
|
|
67
|
-
|
|
52
|
+
handleToggle();
|
|
68
53
|
break;
|
|
69
54
|
}
|
|
70
55
|
},
|
|
71
|
-
[
|
|
56
|
+
[gameState.maxMoveIndex, handleGoto, handleBackward, handleForward, handleToggle]
|
|
72
57
|
);
|
|
73
58
|
useEffect(() => {
|
|
74
59
|
if (props.tesuu !== void 0) {
|
|
75
60
|
handleGoto(props.tesuu);
|
|
76
61
|
}
|
|
77
|
-
}, [props.tesuu]);
|
|
78
|
-
|
|
62
|
+
}, [props.tesuu, handleGoto]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
return () => {
|
|
65
|
+
adapterRef.current.dispose();
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
const topPlayerName = isSente ? `\u2616 ${gameState.header.goteName}` : `\u2617 ${gameState.header.senteName}`;
|
|
69
|
+
const bottomPlayerName = isSente ? `\u2617 ${gameState.header.senteName}` : `\u2616 ${gameState.header.goteName}`;
|
|
70
|
+
return /* @__PURE__ */ React.createElement(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
className: "flex flex-col sm:flex-row w-fit",
|
|
74
|
+
tabIndex: 0,
|
|
75
|
+
role: "application",
|
|
76
|
+
"aria-label": "Shogi player - use arrow keys to navigate, space or right arrow to move forward, left arrow to move backward, 'r' to flip board",
|
|
77
|
+
onKeyDown: handleKeydown
|
|
78
|
+
},
|
|
79
|
+
/* @__PURE__ */ React.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs text-right p-1" }, topPlayerName), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: true }), /* @__PURE__ */ React.createElement(ShogiBoardCanvas, { size, pieces: gameState.board, isSente, currentMove: gameState.currentMove }), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: false }), /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs p-1" }, bottomPlayerName)),
|
|
80
|
+
/* @__PURE__ */ React.createElement("div", { className: "flex flex-col sm:w-fit bg-[#f9d27a] p-4 gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block flex-1 relative w-full" }, /* @__PURE__ */ React.createElement(
|
|
81
|
+
MovesArea,
|
|
82
|
+
{
|
|
83
|
+
readableMoves: gameState.readableMoves,
|
|
84
|
+
tesuu: gameState.currentMoveIndex,
|
|
85
|
+
onTesuuChange: handleGoto
|
|
86
|
+
}
|
|
87
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block text-black border-black border-2 p-1 text-xs max-h-40 w-0 min-w-full overflow-auto" }, gameState.comments.map((comment, index) => /* @__PURE__ */ React.createElement("div", { key: index }, comment)), gameState.comments.length === 0 && gameState.currentMoveIndex !== 0 && /* @__PURE__ */ React.createElement("div", null, "\xA0"), gameState.currentMoveIndex === 0 && Object.entries(gameState.header).map(([key, value], i) => /* @__PURE__ */ React.createElement("div", { key: i, className: "whitespace-nowrap" }, key, ": ", value))), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(0) }, "\u6700\u521D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleBackward }, "\u524D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleForward }, "\u6B21"), /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(gameState.maxMoveIndex) }, "\u6700\u5F8C"), /* @__PURE__ */ React.createElement(Button, { onClick: handleToggle }, "\u53CD\u8EE2")))
|
|
88
|
+
);
|
|
79
89
|
}
|
|
80
90
|
export {
|
|
81
91
|
ShogiPlayer as default
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Piece } from 'shogi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Library-independent move representation
|
|
5
|
+
* @property from - Source position (optional for drops)
|
|
6
|
+
* @property to - Destination position
|
|
7
|
+
* @property color - Player color (0: sente/black, 1: gote/white)
|
|
8
|
+
* @property piece - Piece type being moved
|
|
9
|
+
* @property same - Whether the destination is the same as the previous move
|
|
10
|
+
* @property promote - Whether the piece promotes
|
|
11
|
+
* @property capture - Captured piece type (if any)
|
|
12
|
+
* @property relative - Relative movement direction (for disambiguation)
|
|
13
|
+
*/
|
|
14
|
+
interface Move {
|
|
15
|
+
from?: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
to?: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
color?: number;
|
|
24
|
+
piece?: string;
|
|
25
|
+
same?: boolean;
|
|
26
|
+
promote?: boolean;
|
|
27
|
+
capture?: string;
|
|
28
|
+
relative?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Complete game state representation independent of external libraries
|
|
32
|
+
* @property board - 9x9 board with pieces (or null for empty squares)
|
|
33
|
+
* @property hands - Captured pieces for each player [sente, gote]
|
|
34
|
+
* @property currentMoveIndex - Current position in the move sequence
|
|
35
|
+
* @property maxMoveIndex - Total number of moves in the game
|
|
36
|
+
* @property currentMove - The move at the current position
|
|
37
|
+
* @property header - Game metadata (player names, date, etc.)
|
|
38
|
+
* @property comments - Comments for the current position
|
|
39
|
+
* @property readableMoves - Human-readable move strings (e.g., "☗7六歩")
|
|
40
|
+
*/
|
|
41
|
+
interface GameState {
|
|
42
|
+
board: (Piece | null)[][];
|
|
43
|
+
hands: [Piece[], Piece[]];
|
|
44
|
+
currentMoveIndex: number;
|
|
45
|
+
maxMoveIndex: number;
|
|
46
|
+
currentMove?: Move;
|
|
47
|
+
header: {
|
|
48
|
+
senteName: string;
|
|
49
|
+
goteName: string;
|
|
50
|
+
[key: string]: string;
|
|
51
|
+
};
|
|
52
|
+
comments: string[];
|
|
53
|
+
readableMoves: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Adapter interface for kifu parsing libraries
|
|
57
|
+
* Wraps the underlying library and provides a clean interface for game navigation
|
|
58
|
+
*/
|
|
59
|
+
interface KifuAdapter {
|
|
60
|
+
/**
|
|
61
|
+
* Move forward one step in the game
|
|
62
|
+
* @returns Updated game state after the move
|
|
63
|
+
*/
|
|
64
|
+
forward(): GameState;
|
|
65
|
+
/**
|
|
66
|
+
* Move backward one step in the game
|
|
67
|
+
* @returns Updated game state after the move
|
|
68
|
+
*/
|
|
69
|
+
backward(): GameState;
|
|
70
|
+
/**
|
|
71
|
+
* Jump to a specific move index
|
|
72
|
+
* @param moveIndex - Target move index (0 = initial position)
|
|
73
|
+
* @returns Updated game state after navigation
|
|
74
|
+
*/
|
|
75
|
+
goto(moveIndex: number): GameState;
|
|
76
|
+
/**
|
|
77
|
+
* Get the current game state without navigation
|
|
78
|
+
* @returns Current game state
|
|
79
|
+
*/
|
|
80
|
+
getState(): GameState;
|
|
81
|
+
/**
|
|
82
|
+
* Clean up resources held by the adapter
|
|
83
|
+
*/
|
|
84
|
+
dispose(): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Factory function type for creating KifuAdapter instances
|
|
88
|
+
* Used for dependency injection in tests
|
|
89
|
+
*/
|
|
90
|
+
type KifuAdapterFactory = (kifuText: string) => KifuAdapter;
|
|
91
|
+
/**
|
|
92
|
+
* Props for ShogiBoardCanvas component
|
|
93
|
+
* @property size - Canvas size in CSS pixels (default: 360)
|
|
94
|
+
* @property boardColor - Background color of the board (default: '#f9d27a')
|
|
95
|
+
* @property lineColor - Color of board lines (default: '#000')
|
|
96
|
+
* @property fontFamily - Font family for coordinates and pieces (default: 'serif')
|
|
97
|
+
* @property fontSizeRatio - Font size ratio relative to cell size (default: 0.7)
|
|
98
|
+
* @property pieces - 2D array of pieces on the board (null for empty squares)
|
|
99
|
+
* @property isSente - Whether to display from black player's perspective (default: true)
|
|
100
|
+
* @property currentMove - Current move to highlight on the board
|
|
101
|
+
*/
|
|
102
|
+
type ShogiBoardCanvasProps = {
|
|
103
|
+
size?: number;
|
|
104
|
+
boardColor?: string;
|
|
105
|
+
lineColor?: string;
|
|
106
|
+
fontFamily?: string;
|
|
107
|
+
fontSizeRatio?: number;
|
|
108
|
+
pieces: (Piece | null)[][];
|
|
109
|
+
isSente?: boolean;
|
|
110
|
+
currentMove?: Move;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Props for ShogiHandsCanvas component
|
|
114
|
+
* @property size - Canvas size in CSS pixels (default: 360)
|
|
115
|
+
* @property boardColor - Background color (default: '#f9d27a')
|
|
116
|
+
* @property lineColor - Color of frame lines (default: '#000')
|
|
117
|
+
* @property fontFamily - Font family for piece names (default: 'serif')
|
|
118
|
+
* @property fontSizeRatio - Font size ratio relative to cell size (default: 0.7)
|
|
119
|
+
* @property hands - 2D array of captured pieces
|
|
120
|
+
* @property isSente - Whether to display from black player's perspective (default: true)
|
|
121
|
+
* @property isTop - Whether this is the top hands area (default: false)
|
|
122
|
+
*/
|
|
123
|
+
type ShogiHandsCanvasProps = {
|
|
124
|
+
size?: number;
|
|
125
|
+
boardColor?: string;
|
|
126
|
+
lineColor?: string;
|
|
127
|
+
fontFamily?: string;
|
|
128
|
+
fontSizeRatio?: number;
|
|
129
|
+
hands: Piece[][];
|
|
130
|
+
isSente?: boolean;
|
|
131
|
+
isTop?: boolean;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Canvas rendering dimensions including device pixel ratio
|
|
135
|
+
* @property width - Actual canvas width in device pixels
|
|
136
|
+
* @property height - Actual canvas height in device pixels
|
|
137
|
+
* @property dpr - Device pixel ratio for HiDPI displays
|
|
138
|
+
*/
|
|
139
|
+
type CanvasDimensions = {
|
|
140
|
+
width: number;
|
|
141
|
+
height: number;
|
|
142
|
+
dpr: number;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Layout information for the shogi board
|
|
146
|
+
* @property margin - Margin from canvas edge
|
|
147
|
+
* @property boardSize - Total size of the 9x9 board area
|
|
148
|
+
* @property cell - Size of a single cell in the board
|
|
149
|
+
*/
|
|
150
|
+
type BoardLayout = {
|
|
151
|
+
margin: number;
|
|
152
|
+
boardSize: number;
|
|
153
|
+
cell: number;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Layout information for the hands (captured pieces) area
|
|
157
|
+
* @property margin - Margin from canvas edge
|
|
158
|
+
* @property handsHeight - Total height of the hands canvas
|
|
159
|
+
* @property boardSize - Width of the hands area (same as board width)
|
|
160
|
+
* @property cellSize - Size of a single cell in the hands area
|
|
161
|
+
*/
|
|
162
|
+
type HandsLayout = {
|
|
163
|
+
margin: number;
|
|
164
|
+
handsHeight: number;
|
|
165
|
+
boardSize: number;
|
|
166
|
+
cellSize: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export type { BoardLayout, CanvasDimensions, GameState, HandsLayout, KifuAdapter, KifuAdapterFactory, Move, ShogiBoardCanvasProps, ShogiHandsCanvasProps };
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackersheet/next-document-content-kifu",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.18",
|
|
4
4
|
"description": "Hacker Sheet document content kifu components for Next.js",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -24,17 +24,17 @@
|
|
|
24
24
|
"dist"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"json-kifu-format": "
|
|
28
|
-
"shogi.js": "
|
|
29
|
-
"@hackersheet/
|
|
30
|
-
"@hackersheet/
|
|
27
|
+
"json-kifu-format": "5.4.1",
|
|
28
|
+
"shogi.js": "5.4.1",
|
|
29
|
+
"@hackersheet/react-document-content": "0.1.0-alpha.15",
|
|
30
|
+
"@hackersheet/core": "0.1.0-alpha.13"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/react": "^19.2.
|
|
34
|
-
"@types/react-dom": "^19.2.
|
|
35
|
-
"next": "^
|
|
36
|
-
"react": "^19.2.
|
|
37
|
-
"react-dom": "^19.2.
|
|
33
|
+
"@types/react": "^19.2.10",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"next": "^16.1.6",
|
|
36
|
+
"react": "^19.2.4",
|
|
37
|
+
"react-dom": "^19.2.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"next": "^15.0.0 || ^16.0.0",
|