diff --git a/.cursorignore b/.cursorignore
new file mode 100644
index 0000000..fbeb128
--- /dev/null
+++ b/.cursorignore
@@ -0,0 +1,4 @@
+public/
+third_party/
+assets/
+dist/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0eb8a1e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 iFwu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5a26e29
--- /dev/null
+++ b/README.md
@@ -0,0 +1,89 @@
+# 象棋棋盘识别与分析系统
+
+这是一个在线象棋棋盘识别与分析系统,可以从上传的图片中识别出象棋棋局,并提供最佳走法分析。
+
+## 在线演示
+
+您可以在这里体验本系统:[象棋棋盘识别与分析](https://ifwu.github.io/xiangqi-analysis/)
+
+## 功能特点
+
+- 棋盘识别:从上传的图片中自动识别象棋棋盘和棋子位置
+- FEN 生成:根据识别结果生成 FEN(Forsyth–Edwards Notation)字符串
+- 最佳走法分析:使用象棋引擎分析当前局面,给出最佳走法建议
+- 走棋模拟:可以按照建议的最佳走法进行模拟,查看局面变化
+- 中文走法表示:将走法转换为中文表示,方便理解
+- 响应式设计:适配不同尺寸的设备屏幕
+
+## 技术栈
+
+- 前端框架:Preact
+- 图像处理:OpenCV.js
+- 象棋引擎:Pikafish(WebAssembly 版本)
+- 构建工具:Vite
+- 包管理器:pnpm
+- 部署:GitHub Pages
+
+## 本地开发
+
+1. 克隆仓库:
+
+```
+git clone https://github.com/your-username/xiangqi-analysis.git
+cd xiangqi-analysis
+```
+
+2. 安装依赖:
+
+```
+pnpm install
+```
+
+3. 启动开发服务器:
+
+```
+pnpm dev
+```
+
+4. 在浏览器中打开 `http://localhost:5173` 查看应用
+
+## 构建和部署
+
+1. 构建项目:
+
+```
+pnpm build
+```
+
+2. 部署到 GitHub Pages:
+
+```
+pnpm deploy
+```
+
+注意:本项目使用 GitHub Actions 进行自动部署。每次推送到主分支时,都会触发构建和部署流程。
+
+## 项目结构
+
+- `src/`: 源代码目录
+ - `components/`: React 组件
+ - `chessboard/`: 棋盘识别和分析相关的模块
+- `public/`: 静态资源目录
+- `vite.config.ts`: Vite 配置文件
+- `.github/workflows/`: GitHub Actions 工作流配置
+
+## 贡献
+
+欢迎提交 Issues 和 Pull Requests 来改进这个项目!
+
+## 许可证声明
+
+本项目使用 MIT 许可证。
+
+本项目使用了 [Pikafish](https://github.com/official-pikafish/Pikafish) 象棋引擎,该引擎采用 GNU General Public License v3.0 (GPL-3.0) 许可证。Pikafish 的版权归其原作者所有。
+
+本项目仅调用 Pikafish 引擎,并未对其进行修改。使用本项目不要求您的应用程序遵守 GPL-3.0 许可证。但是,如果您计划重新分发包含 Pikafish 的完整应用程序,请确保遵守 GPL-3.0 许可证的相关规定,包括提供源代码访问和适当的许可声明。
+
+完整的 GPL-3.0 许可证文本可在 [此处](https://www.gnu.org/licenses/gpl-3.0.en.html) 查看。
+
+使用本项目时,请确保理解并遵守相关的许可条款。如有疑问,建议咨询法律专业人士。
diff --git a/index.html b/index.html
index 9a5f6e8..d7ee4e1 100644
--- a/index.html
+++ b/index.html
@@ -1,11 +1,18 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
%VITE_UMAMI_SCRIPT%
- 象棋分析
+ 象棋棋盘识别与分析系统 | 在线象棋分析工具
diff --git a/package.json b/package.json
index b8b322e..988ab53 100644
--- a/package.json
+++ b/package.json
@@ -22,5 +22,6 @@
"vite": "^5.4.8"
},
"packageManager": "pnpm@9.12.0",
- "homepage": "https://ifwu.github.io/xiangqi-analysis/"
+ "homepage": "https://ifwu.github.io/xiangqi-analysis/",
+ "license": "MIT"
}
diff --git a/src/App.tsx b/src/App.tsx
index 0d14d57..db3b7f7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -48,7 +48,7 @@ export function App() {
height: number;
}>();
// 从 localStorage 加载初始深度值
- const initialDepth = Number(localStorage.getItem('depth')) || 15;
+ const initialDepth = Number(localStorage.getItem('depth')) || 14;
const [depth, setDepth] = useState(initialDepth);
// Initialize OpenCV and Chess Engine
@@ -295,7 +295,11 @@ export function App() {
>
);
diff --git a/src/app.css b/src/app.css
index 23adb82..7968c01 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,72 +1,41 @@
+/* 全局样式 */
:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
+ --font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ --text-color: #333;
+ --background-color: #f4f4f4;
+ --primary-color: #3498db;
+ --primary-hover-color: #2980b9;
+ --header-footer-bg: #2c3e50;
+ --header-footer-color: #ecf0f1;
}
body {
+ font-family: var(--font-family);
+ line-height: 1.6;
+ color: var(--text-color);
+ background-color: var(--background-color);
margin: 0;
padding: 0;
min-width: 100%;
min-height: 100vh;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- line-height: 1.6;
- color: #333;
- background-color: #f4f4f4;
- margin: 0;
- padding: 0;
display: flex;
flex-direction: column;
- width: 100%;
overflow-x: hidden;
}
+h1 {
+ font-size: 2.5rem;
+ line-height: 1.1;
+ margin: 0;
+}
+
+h2 {
+ color: #2c3e50;
+ margin-top: 0;
+ font-size: 1.5rem;
+}
+
+/* 布局 */
.app-container {
flex: 1;
display: flex;
@@ -78,10 +47,27 @@ body {
box-sizing: border-box;
}
+.content-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2rem;
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 1rem;
+ box-sizing: border-box;
+}
+
+.left-column,
+.right-column {
+ width: 100%;
+}
+
+/* 头部和底部 */
header, footer {
width: 100%;
- background-color: #2c3e50;
- color: #ecf0f1;
+ background-color: var(--header-footer-bg);
+ color: var(--header-footer-color);
text-align: center;
padding: 1rem 0;
}
@@ -90,11 +76,17 @@ header {
margin-bottom: 1rem;
}
-h1 {
- margin: 0;
- font-size: 2.5rem;
+footer a {
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+footer a:hover {
+ color: var(--primary-hover-color);
}
+/* 主要内容区 */
main {
flex: 1;
display: flex;
@@ -106,22 +98,7 @@ main {
box-sizing: border-box;
}
-.content-wrapper {
- display: flex;
- flex-wrap: wrap;
- gap: 2rem;
- width: 100%;
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 1rem;
- box-sizing: border-box;
-}
-
-.left-column,
-.right-column {
- width: 100%;
-}
-
+/* 组件样式 */
.solution-section,
.upload-section,
.board-result-section,
@@ -134,42 +111,59 @@ main {
margin-bottom: 1rem;
}
-.solution-column,
-.image-column {
- flex: 1;
+/* 标题左对齐 */
+.solution-section h2,
+.upload-section h2,
+.board-result-section h2,
+.fen-section h2,
+.depth-control-section h2 {
+ text-align: left;
+}
+
+/* 内容居中 */
+.solution-section > *:not(h2),
+.upload-section > *:not(h2),
+.board-result-section > *:not(h2),
+.fen-section > *:not(h2),
+.depth-control-section > *:not(h2) {
+ text-align: center;
+}
+
+/* 解法控制按钮样式 */
+.solution-controls {
display: flex;
- flex-direction: column;
+ justify-content: center;
gap: 1rem;
+ margin-top: 1rem;
}
-.solution-section,
-.upload-section,
-.board-result-section,
-.depth-control-section,
-.fen-section {
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- margin-bottom: 1rem;
+/* 按钮样式 */
+button {
+ border-radius: 4px;
+ border: none;
+ padding: 0.5rem 1rem;
+ font-size: 1em;
+ font-weight: 500;
+ background-color: var(--primary-color);
+ color: #fff;
+ cursor: pointer;
+ transition: background-color 0.3s, opacity 0.3s;
+ display: inline-block; /* 确保按钮可以被居中 */
}
-.solution-section {
- flex: 1;
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- box-sizing: border-box;
+button:hover {
+ background-color: var(--primary-hover-color);
}
-h2 {
- color: #2c3e50;
- margin-top: 0;
- font-size: 1.5rem;
+button:disabled {
+ background-color: #95a5a6;
+ cursor: not-allowed;
+ opacity: 0.7;
}
-input[type="file"] {
+/* 输入框样式 */
+input[type="file"],
+.fen-container input {
display: block;
margin: 1rem 0;
padding: 0.5rem;
@@ -180,53 +174,119 @@ input[type="file"] {
box-sizing: border-box;
}
+/* FEN容器 */
.fen-container {
display: flex;
gap: 1rem;
+ justify-content: center;
+ align-items: center; /* 添加这行以垂直居中对齐项目 */
}
+/* 输入框样式 */
.fen-container input {
- flex: 1;
- padding: 0.5rem;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 1rem;
+ flex: 1; /* 让输入框占据剩余空间 */
+ margin: 0; /* 移除之前的 margin */
}
-.fen-container button, .solution-controls button {
- padding: 0.5rem 1rem;
- background-color: #3498db;
- color: #fff;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.3s, opacity 0.3s;
+/* FEN 复制按钮样式 */
+.fen-container button {
+ flex-shrink: 0; /* 防止按钮被压缩 */
+ white-space: nowrap; /* 防止文本换行 */
}
-.fen-container button:hover, .solution-controls button:hover {
- background-color: #2980b9;
+/* 深度滑块 */
+.depth-slider-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
}
-#solutionCanvas {
- display: block;
- margin: 0 auto;
- border: 1px solid #ddd;
+.depth-slider-container label {
+ font-weight: bold;
}
-.solution-controls {
+#depth-slider {
+ width: 100%;
+}
+
+/* 图像容器 */
+.image-container {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ margin-top: 1rem;
+}
+
+.overlay-image {
+ position: absolute;
+ top: 0;
+}
+
+/* 棋盘样式 */
+.chessboard {
+ display: inline-block;
+ border: 2px solid #000;
+ background-color: #f0d9b5;
+}
+
+.board-row {
+ display: flex;
+}
+
+.board-cell {
+ width: 40px;
+ height: 40px;
display: flex;
justify-content: center;
- gap: 1rem;
- margin-top: 1rem;
+ align-items: center;
+ border: 1px solid #000;
}
-footer {
- text-align: center;
- padding: 1rem 0;
- background-color: #2c3e50;
- color: #ecf0f1;
+.piece {
+ font-size: 24px;
+ font-weight: bold;
+}
+
+.piece.red {
+ color: #ff0000;
+}
+
+.piece.black {
+ color: #000000;
+}
+
+/* 棋盘和按钮之间的间距 */
+.chessboard-display {
+ margin-bottom: 1rem;
+}
+
+/* 走棋历史样式 */
+.move-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: flex-start;
+}
+
+.move-item {
+ background-color: #f0f0f0;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.9em;
+ display: inline-block;
+ margin-bottom: 0.25rem;
}
+/* 为红方和黑方的移动添加不同的样式 */
+.move-item:nth-child(odd) {
+ background-color: #ffebee; /* 浅红色背景 */
+}
+
+.move-item:nth-child(even) {
+ background-color: #e3f2fd; /* 浅蓝色背景 */
+}
+
+/* 媒体查询 */
@media (max-width: 768px) {
.content-wrapper {
flex-direction: column;
@@ -238,25 +298,11 @@ footer {
width: 100%;
}
- .solution-section {
- order: 1;
- }
-
- .upload-section {
- order: 0;
- }
-
- .board-result-section {
- order: 2;
- }
-
- .fen-section {
- order: 3;
- }
-
- .depth-control-section {
- order: 4;
- }
+ .solution-section { order: 1; }
+ .upload-section { order: 0; }
+ .board-result-section { order: 2; }
+ .fen-section { order: 3; }
+ .depth-control-section { order: 4; }
}
@media (min-width: 769px) {
@@ -279,6 +325,7 @@ footer {
.solution-section {
height: calc(100vh - 2rem);
overflow-y: auto;
+ order: -1;
}
.solution-column,
@@ -286,60 +333,17 @@ footer {
width: 50%;
max-width: 600px;
}
- .solution-section {
- order: -1;
- }
-
-}
-
-.chessboard {
- display: inline-block;
- border: 2px solid #000;
- background-color: #f0d9b5;
-}
-
-.board-row {
- display: flex;
-}
-
-.board-cell {
- width: 40px;
- height: 40px;
- display: flex;
- justify-content: center;
- align-items: center;
- border: 1px solid #000;
-}
-
-.piece {
- font-size: 24px;
- font-weight: bold;
-}
-
-.piece.red {
- color: #ff0000;
-}
-
-.piece.black {
- color: #000000;
}
-.solution-controls button:disabled {
- background-color: #95a5a6;
- cursor: not-allowed;
- opacity: 0.7;
-}
+/* ... 其他样式保持不变 ... */
-.depth-slider-container {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
+.solution-debug {
+ margin: 1rem 0;
+ font-size: 1.1em;
}
-.depth-slider-container label {
- font-weight: bold;
+.solution-debug p {
+ margin: 0.5rem 0;
}
-#depth-slider {
- width: 100%;
-}
\ No newline at end of file
+/* ... 其他样式保持不变 ... */
\ No newline at end of file
diff --git a/src/components/BoardResult.tsx b/src/components/BoardResult.tsx
index 0611f7a..0b96dbd 100644
--- a/src/components/BoardResult.tsx
+++ b/src/components/BoardResult.tsx
@@ -1,5 +1,3 @@
-import './ChessboardOverlay.css';
-
interface BoardResultProps {
overlayImageSrc: string;
chessboardRect?: { x: number; y: number; width: number; height: number };
diff --git a/src/components/ChessboardOverlay.css b/src/components/ChessboardOverlay.css
deleted file mode 100644
index 27c9ce0..0000000
--- a/src/components/ChessboardOverlay.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.image-container {
- position: relative;
- width: 100%;
- overflow: hidden;
- margin-top: 1rem;
-}
-
-.overlay-image {
- position: absolute;
- top: 0;
-}
\ No newline at end of file
diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx
index c772d05..e021237 100644
--- a/src/components/ImageUploader.tsx
+++ b/src/components/ImageUploader.tsx
@@ -1,8 +1,12 @@
+import { useRef } from 'preact/hooks';
+
interface ImageUploaderProps {
onImageUpload: (img: HTMLImageElement) => void;
}
export function ImageUploader({ onImageUpload }: ImageUploaderProps) {
+ const fileInputRef = useRef(null);
+
const handleImageUpload = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
@@ -16,10 +20,21 @@ export function ImageUploader({ onImageUpload }: ImageUploaderProps) {
}
};
+ const handleButtonClick = () => {
+ fileInputRef.current?.click();
+ };
+
return (
);
}
\ No newline at end of file
diff --git a/src/components/SolutionDisplay.tsx b/src/components/SolutionDisplay.tsx
index 913fbde..89a085e 100644
--- a/src/components/SolutionDisplay.tsx
+++ b/src/components/SolutionDisplay.tsx
@@ -23,8 +23,9 @@ export function SolutionDisplay({
fenHistory,
}: SolutionDisplayProps) {
const currentMoveColor = moveHistory.length % 2 === 0 ? '红方' : '黑方';
+ const currentMoveNumber = moveHistory.length + 1;
- // Map over moveHistory and use the correct FEN before each move
+ // Create move history items
const moveItems = moveHistory.map((move, index) => {
const fenBeforeMove = fenHistory[index]; // FEN before the move
let notation = '';
@@ -35,9 +36,9 @@ export function SolutionDisplay({
console.error(`Error converting move to notation: ${err}`);
}
return (
-
- {index % 2 === 0 ? '红方' : '黑方'}: {notation}
-
+
+ {notation}
+
);
});
@@ -49,14 +50,15 @@ export function SolutionDisplay({
? '黑方胜'
: '';
- // Calculate the number of moves for each side
- const redMoves = Math.ceil(moveHistory.length / 2);
- const blackMoves = Math.floor(moveHistory.length / 2);
-
return (
解法展示
-
+
+
+
上一步
@@ -72,7 +74,11 @@ export function SolutionDisplay({
{loading && 正在计算最佳走法...
}
{error && {error}
}
{!isGameOver && bestMove && (
- 最佳走法: {moveToChineseNotation(fenCode, bestMove)}
+
+ {currentMoveColor}最佳走法:
+ {moveToChineseNotation(fenCode, bestMove)} (第 {currentMoveNumber}{' '}
+ 步)
+
)}
{isGameOver && (
@@ -80,15 +86,7 @@ export function SolutionDisplay({
)}
-
-
- 走棋历史 (红方: {redMoves}步, 黑方: {blackMoves}步)
-
-
-
-
-
当前步: {currentMoveColor}
-
+
{moveItems}
);
}