Maximus Powers commited on
Commit
3568151
·
1 Parent(s): 3787eeb
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +96 -0
  2. Dockerfile +25 -0
  3. README.md +8 -10
  4. index.html +26 -0
  5. nginx.conf +31 -0
  6. package-lock.json +0 -0
  7. package.json +31 -0
  8. public/assets/pieces/dark/rotated/Chess_Bdt45.svg +78 -0
  9. public/assets/pieces/dark/rotated/Chess_Ndt45.svg +22 -0
  10. public/assets/pieces/dark/rotated/Chess_fdt45.svg +25 -0
  11. public/assets/pieces/dark/rotated/Chess_gdt45.svg +157 -0
  12. public/assets/pieces/dark/rotated/Chess_hdt45.svg +66 -0
  13. public/assets/pieces/dark/rotated/Chess_mdt45.svg +133 -0
  14. public/assets/pieces/dark/upright/Chess_bdt45.svg +12 -0
  15. public/assets/pieces/dark/upright/Chess_kdt45.svg +12 -0
  16. public/assets/pieces/dark/upright/Chess_ndt45.svg +22 -0
  17. public/assets/pieces/dark/upright/Chess_pdt45.svg +5 -0
  18. public/assets/pieces/dark/upright/Chess_qdt45.svg +27 -0
  19. public/assets/pieces/dark/upright/Chess_rdt45.svg +39 -0
  20. public/assets/pieces/white/rotated/Chess_Blt45.svg +79 -0
  21. public/assets/pieces/white/rotated/Chess_Nlt45.svg +117 -0
  22. public/assets/pieces/white/rotated/Chess_flt45.svg +115 -0
  23. public/assets/pieces/white/rotated/Chess_glt45.svg +157 -0
  24. public/assets/pieces/white/rotated/Chess_hlt45.svg +87 -0
  25. public/assets/pieces/white/rotated/Chess_mlt45.svg +117 -0
  26. public/assets/pieces/white/upright/Chess_blt45.svg +12 -0
  27. public/assets/pieces/white/upright/Chess_klt45.svg +9 -0
  28. public/assets/pieces/white/upright/Chess_nlt45.svg +19 -0
  29. public/assets/pieces/white/upright/Chess_plt45.svg +5 -0
  30. public/assets/pieces/white/upright/Chess_qlt45.svg +15 -0
  31. public/assets/pieces/white/upright/Chess_rlt45.svg +25 -0
  32. src/App.tsx +178 -0
  33. src/ErrorBoundary.tsx +69 -0
  34. src/components/AudioInfoPopup.tsx +56 -0
  35. src/components/ChessBoard.tsx +169 -0
  36. src/components/ChessPiece.tsx +48 -0
  37. src/components/GameControls.tsx +152 -0
  38. src/components/PromotionDialog.tsx +47 -0
  39. src/components/VolumeSlider.tsx +43 -0
  40. src/engines/AudioEngine.ts +311 -0
  41. src/engines/ChessAI.ts +129 -0
  42. src/engines/PositionEvaluator.ts +443 -0
  43. src/hooks/useChessGame.ts +404 -0
  44. src/main.tsx +15 -0
  45. src/styles/App.css +117 -0
  46. src/styles/AudioInfoPopup.css +146 -0
  47. src/styles/ChessBoard.css +99 -0
  48. src/styles/ChessPiece.css +30 -0
  49. src/styles/ChessSquare.css +60 -0
  50. src/styles/GameControls.css +92 -0
.gitignore ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # Production
7
+ /dist
8
+ /build
9
+
10
+ # Development
11
+ .env.local
12
+ .env.development.local
13
+ .env.test.local
14
+ .env.production.local
15
+
16
+ # Logs
17
+ npm-debug.log*
18
+ yarn-debug.log*
19
+ yarn-error.log*
20
+ pnpm-debug.log*
21
+ lerna-debug.log*
22
+
23
+ # Runtime data
24
+ pids
25
+ *.pid
26
+ *.seed
27
+ *.pid.lock
28
+
29
+ # Coverage directory used by tools like istanbul
30
+ coverage/
31
+ *.lcov
32
+
33
+ # nyc test coverage
34
+ .nyc_output
35
+
36
+ # ESLint cache
37
+ .eslintcache
38
+
39
+ # Optional npm cache directory
40
+ .npm
41
+
42
+ # Optional eslint cache
43
+ .eslintcache
44
+
45
+ # Microbundle cache
46
+ .rpt2_cache/
47
+ .rts2_cache_cjs/
48
+ .rts2_cache_es/
49
+ .rts2_cache_umd/
50
+
51
+ # Optional REPL history
52
+ .node_repl_history
53
+
54
+ # Output of 'npm pack'
55
+ *.tgz
56
+
57
+ # Yarn Integrity file
58
+ .yarn-integrity
59
+
60
+ # parcel-bundler cache (https://parceljs.org/)
61
+ .cache
62
+ .parcel-cache
63
+
64
+ # Vite cache
65
+ .vite
66
+
67
+ # Next.js build output
68
+ .next
69
+
70
+ # Nuxt.js build / generate output
71
+ .nuxt
72
+ dist
73
+
74
+ # Storybook build outputs
75
+ .out
76
+ .storybook-out
77
+
78
+ # Temporary folders
79
+ tmp/
80
+ temp/
81
+
82
+ # IDE
83
+ .vscode/
84
+ .idea/
85
+ *.swp
86
+ *.swo
87
+ *~
88
+
89
+ # OS
90
+ .DS_Store
91
+ .DS_Store?
92
+ ._*
93
+ .Spotlight-V100
94
+ .Trashes
95
+ ehthumbs.db
96
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+
5
+ RUN apt-get update && apt-get install -y curl && \
6
+ curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
7
+ apt-get install -y nodejs && \
8
+ apt-get clean && rm -rf /var/lib/apt/lists/*
9
+
10
+ USER user
11
+ ENV PATH="/home/user/.local/bin:$PATH"
12
+
13
+ WORKDIR /app
14
+
15
+ COPY --chown=user package*.json ./
16
+
17
+ RUN npm ci
18
+
19
+ COPY --chown=user . .
20
+
21
+ RUN npm run build
22
+
23
+ EXPOSE 7860
24
+
25
+ CMD ["python", "-m", "http.server", "7860", "--directory", "dist"]
README.md CHANGED
@@ -1,12 +1,10 @@
1
  ---
2
- title: Musical Chess
3
- emoji: 🏆
4
- colorFrom: green
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Train chess with audio cues for moves and initiative.
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Musical Chess Arena
3
+ emoji: 🎵♟️
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
+ sdk_version: "4.40.0"
8
+ app_file: app.py
9
+ pinned: true
10
+ ---
 
 
index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>🎵♟️ Musical Chess Arena</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background-color: #323232;
17
+ color: white;
18
+ overflow: hidden;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div id="root"></div>
24
+ <script type="module" src="/src/main.tsx"></script>
25
+ </body>
26
+ </html>
nginx.conf ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ events {
2
+ worker_connections 1024;
3
+ }
4
+
5
+ http {
6
+ include /etc/nginx/mime.types;
7
+ default_type application/octet-stream;
8
+
9
+ server {
10
+ listen 7860;
11
+ server_name localhost;
12
+ root /usr/share/nginx/html;
13
+ index index.html;
14
+
15
+ gzip on;
16
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
17
+
18
+ location / {
19
+ try_files $uri $uri/ /index.html;
20
+ }
21
+
22
+ location /assets/ {
23
+ expires 1y;
24
+ add_header Cache-Control "public, immutable";
25
+ }
26
+
27
+ add_header X-Frame-Options "SAMEORIGIN" always;
28
+ add_header X-Content-Type-Options "nosniff" always;
29
+ add_header X-XSS-Protection "1; mode=block" always;
30
+ }
31
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "musical-chess",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "start": "python3 -m http.server 3000 --directory dist"
11
+ },
12
+ "dependencies": {
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0",
15
+ "chess.js": "^1.0.0-beta.8",
16
+ "@xenova/transformers": "^2.17.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.10.0",
20
+ "@types/react": "^18.2.43",
21
+ "@types/react-dom": "^18.2.17",
22
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
23
+ "@typescript-eslint/parser": "^6.14.0",
24
+ "@vitejs/plugin-react": "^4.2.1",
25
+ "eslint": "^8.55.0",
26
+ "eslint-plugin-react-hooks": "^4.6.0",
27
+ "eslint-plugin-react-refresh": "^0.4.5",
28
+ "typescript": "^5.2.2",
29
+ "vite": "^5.0.8"
30
+ }
31
+ }
public/assets/pieces/dark/rotated/Chess_Bdt45.svg ADDED
public/assets/pieces/dark/rotated/Chess_Ndt45.svg ADDED
public/assets/pieces/dark/rotated/Chess_fdt45.svg ADDED
public/assets/pieces/dark/rotated/Chess_gdt45.svg ADDED
public/assets/pieces/dark/rotated/Chess_hdt45.svg ADDED
public/assets/pieces/dark/rotated/Chess_mdt45.svg ADDED
public/assets/pieces/dark/upright/Chess_bdt45.svg ADDED
public/assets/pieces/dark/upright/Chess_kdt45.svg ADDED
public/assets/pieces/dark/upright/Chess_ndt45.svg ADDED
public/assets/pieces/dark/upright/Chess_pdt45.svg ADDED
public/assets/pieces/dark/upright/Chess_qdt45.svg ADDED
public/assets/pieces/dark/upright/Chess_rdt45.svg ADDED
public/assets/pieces/white/rotated/Chess_Blt45.svg ADDED
public/assets/pieces/white/rotated/Chess_Nlt45.svg ADDED
public/assets/pieces/white/rotated/Chess_flt45.svg ADDED
public/assets/pieces/white/rotated/Chess_glt45.svg ADDED
public/assets/pieces/white/rotated/Chess_hlt45.svg ADDED
public/assets/pieces/white/rotated/Chess_mlt45.svg ADDED
public/assets/pieces/white/upright/Chess_blt45.svg ADDED
public/assets/pieces/white/upright/Chess_klt45.svg ADDED
public/assets/pieces/white/upright/Chess_nlt45.svg ADDED
public/assets/pieces/white/upright/Chess_plt45.svg ADDED
public/assets/pieces/white/upright/Chess_qlt45.svg ADDED
public/assets/pieces/white/upright/Chess_rlt45.svg ADDED
src/App.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { ChessBoard } from './components/ChessBoard'
3
+ import { GameControls } from './components/GameControls'
4
+ import { PromotionDialog } from './components/PromotionDialog'
5
+ import { AudioInfoPopup } from './components/AudioInfoPopup'
6
+ import { useChessGame } from './hooks/useChessGame'
7
+ import { AudioEngine } from './engines/AudioEngine'
8
+ import './styles/App.css'
9
+
10
+ function App() {
11
+ const {
12
+ gameState,
13
+ draggedPiece,
14
+ selectedModel,
15
+ startNewGame,
16
+ resignGame,
17
+ togglePlayerColor,
18
+ selectSquare,
19
+ attemptMove,
20
+ completePromotion,
21
+ startDrag,
22
+ endDrag,
23
+ changeModel
24
+ } = useChessGame()
25
+
26
+ const [audioEnabled, setAudioEnabled] = useState(false)
27
+ const [showAudioInfo, setShowAudioInfo] = useState(false)
28
+ const [volumeSettings, setVolumeSettings] = useState<{ ambient: number, game: number}>({
29
+ ambient: 0.8, // Default even louder for ambient beats
30
+ game: 0.08 // 8% default for game sounds (scaled)
31
+ })
32
+ const audioEngineRef = useRef<AudioEngine | null>(null)
33
+
34
+ // Initialize audio engine
35
+ useEffect(() => {
36
+ audioEngineRef.current = new AudioEngine()
37
+
38
+ return () => {
39
+ if (audioEngineRef.current) {
40
+ audioEngineRef.current.cleanup()
41
+ }
42
+ }
43
+ }, [])
44
+
45
+ // Handle audio state changes
46
+ useEffect(() => {
47
+ if (audioEngineRef.current) {
48
+ if (audioEnabled) {
49
+ audioEngineRef.current.setVolume(0.7)
50
+ audioEngineRef.current.setBoardFlipped(gameState.playerColor === 'b')
51
+ if (gameState.gameActive) {
52
+ audioEngineRef.current.updatePositionAudio(gameState.board, gameState.playerColor)
53
+ }
54
+ } else {
55
+ audioEngineRef.current.setVolume(0)
56
+ audioEngineRef.current.stopAllAudio()
57
+ }
58
+ }
59
+ }, [audioEnabled, gameState.gameActive, gameState.playerColor])
60
+
61
+ // Handle move audio
62
+ useEffect(() => {
63
+ if (audioEnabled && audioEngineRef.current && gameState.gameHistory.length > 0) {
64
+ const lastMove = gameState.gameHistory[gameState.gameHistory.length - 1]
65
+ audioEngineRef.current.playMoveSound(lastMove.moveData, gameState.board, lastMove.capturedPiece)
66
+ }
67
+ }, [gameState.gameHistory.length, audioEnabled])
68
+
69
+ // Handle position audio updates
70
+ useEffect(() => {
71
+ if (audioEnabled && audioEngineRef.current && gameState.gameActive) {
72
+ audioEngineRef.current.updateInitiativeVolumes(gameState.board, gameState.playerColor)
73
+ }
74
+ }, [gameState.board.fen(), audioEnabled, gameState.gameActive, gameState.playerColor])
75
+
76
+ // Stop audio when game ends
77
+ useEffect(() => {
78
+ if (audioEngineRef.current && gameState.gameOver) {
79
+ audioEngineRef.current.stopPositionAudio()
80
+ }
81
+ }, [gameState.gameOver])
82
+
83
+ const handleStartGame = () => {
84
+ startNewGame()
85
+
86
+ // Enable audio context on user interaction
87
+ if (audioEnabled && audioEngineRef.current) {
88
+ audioEngineRef.current.ensureAudioContext()
89
+ }
90
+ }
91
+
92
+ const handleToggleAudio = () => {
93
+ setAudioEnabled(!audioEnabled)
94
+
95
+ // Enable audio context on user interaction if turning on
96
+ if (!audioEnabled && audioEngineRef.current) {
97
+ audioEngineRef.current.ensureAudioContext()
98
+ }
99
+ }
100
+
101
+ const handleVolumeChange = (type: "ambient" | "game", value: number) => {
102
+ setVolumeSettings(prev => ({
103
+ ...prev,
104
+ [type]: value
105
+ }))
106
+
107
+ // Update audio engine with new volume
108
+ if (audioEngineRef.current) {
109
+ switch (type) {
110
+ case 'ambient':
111
+ audioEngineRef.current.setAmbientVolume(value)
112
+ break
113
+ case 'game':
114
+ audioEngineRef.current.setGameVolume(value)
115
+ break
116
+ }
117
+ }
118
+ }
119
+
120
+ return (
121
+ <div className="app-container">
122
+ <div className="main-content">
123
+ <div className="header">
124
+ <h1 className="title">🎵♟️ Musical Chess</h1>
125
+ <button
126
+ className="audio-info-button"
127
+ onClick={() => setShowAudioInfo(true)}
128
+ title="Audio Guide"
129
+ >
130
+ ℹ️
131
+ </button>
132
+ </div>
133
+
134
+ <div className="game-area">
135
+ <div className="board-container">
136
+ <ChessBoard
137
+ key={gameState.board.fen()}
138
+ gameState={gameState}
139
+ draggedPiece={draggedPiece}
140
+ audioEngine={audioEngineRef.current}
141
+ onSquareClick={selectSquare}
142
+ onPieceDragStart={startDrag}
143
+ onPieceDrop={endDrag}
144
+ />
145
+ </div>
146
+
147
+ <div className="sidebar">
148
+ <GameControls
149
+ gameState={gameState}
150
+ audioEnabled={audioEnabled}
151
+ volumeSettings={volumeSettings}
152
+ selectedModel={selectedModel}
153
+ onStartGame={handleStartGame}
154
+ onResignGame={resignGame}
155
+ onToggleColor={togglePlayerColor}
156
+ onToggleAudio={handleToggleAudio}
157
+ onVolumeChange={handleVolumeChange}
158
+ onModelChange={changeModel}
159
+ />
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <PromotionDialog
165
+ isVisible={gameState.promotionDialogActive}
166
+ color={gameState.playerColor}
167
+ onSelect={completePromotion}
168
+ />
169
+
170
+ <AudioInfoPopup
171
+ isVisible={showAudioInfo}
172
+ onClose={() => setShowAudioInfo(false)}
173
+ />
174
+ </div>
175
+ )
176
+ }
177
+
178
+ export default App
src/ErrorBoundary.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ export class ErrorBoundary extends React.Component<{
4
+ children: React.ReactNode
5
+ },{
6
+ hasError: boolean
7
+ error?: Error
8
+ } > {
9
+ constructor(props: {
10
+ children: React.ReactNode
11
+ }) {
12
+ super(props)
13
+ this.state = { hasError: false }
14
+ }
15
+
16
+ static getDerivedStateFromError(error: Error): {
17
+ hasError: boolean
18
+ error?: Error
19
+ } {
20
+ return { hasError: true, error }
21
+ }
22
+
23
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
24
+ console.error('Error caught by boundary:', error, errorInfo)
25
+ }
26
+
27
+ render() {
28
+ if (this.state.hasError) {
29
+ return (
30
+ <div style={{
31
+ padding: '20px',
32
+ backgroundColor: '#323232',
33
+ color: 'white',
34
+ minHeight: '100vh'
35
+ }}>
36
+ <h1>🎵♟️ Musical Chess Arena</h1>
37
+ <div style={{
38
+ backgroundColor: '#ff4444',
39
+ padding: '20px',
40
+ borderRadius: '8px',
41
+ marginTop: '20px'
42
+ }}>
43
+ <h2>Something went wrong!</h2>
44
+ <p>Error: {this.state.error?.message}</p>
45
+ <pre style={{ marginTop: '10px', fontSize: '12px' }}>
46
+ {this.state.error?.stack}
47
+ </pre>
48
+ <button
49
+ onClick={() => window.location.reload()}
50
+ style={{
51
+ marginTop: '20px',
52
+ padding: '10px 20px',
53
+ backgroundColor: '#323232',
54
+ color: 'white',
55
+ border: '2px solid white',
56
+ borderRadius: '4px',
57
+ cursor: 'pointer'
58
+ }}
59
+ >
60
+ Reload Page
61
+ </button>
62
+ </div>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ return this.props.children
68
+ }
69
+ }
src/components/AudioInfoPopup.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import '../styles/AudioInfoPopup.css'
3
+
4
+ export const AudioInfoPopup: React.FC<{
5
+ isVisible: boolean
6
+ onClose: () => void
7
+ }> = ({
8
+ isVisible,
9
+ onClose
10
+ }) => {
11
+ if (!isVisible) return null
12
+
13
+ return (
14
+ <div className="audio-info-overlay" onClick={onClose}>
15
+ <div className="audio-info-popup" onClick={(e) => e.stopPropagation()}>
16
+ <div className="popup-header">
17
+ <h2>🎵 How muscial chess works</h2>
18
+ <button className="close-button" onClick={onClose}>×</button>
19
+ </div>
20
+
21
+ <div className="popup-content">
22
+ <section>
23
+ <h3>🎯 Initiative Tempo</h3>
24
+ <p>A steady beat represents your position strength:</p>
25
+ <ul>
26
+ <li><strong>Frequency:</strong> A2 (110 Hz)</li>
27
+ <li><strong>Tempo:</strong> Speed increases with your initiative (up to 120 BPM). No beat = no initiative. Faster beat = stronger position.</li>
28
+ </ul>
29
+ </section>
30
+
31
+ <section>
32
+ <h3>🎼 Square-Based Move Notes</h3>
33
+ <p>Each square has a unique musical note based on its position:</p>
34
+ <div className="note-mapping">
35
+ <li><strong>Files (left to right):</strong> A, B, C♭, C, D, D♭, E, F</li>
36
+ <li><strong>Ranks (bottom to top):</strong> 8 octaves (1st-8th)</li>
37
+ </div>
38
+ <p>Every move plays two quick beats:</p>
39
+ <ul>
40
+ <li><strong>First beat:</strong> Origin square note.</li>
41
+ <li><strong>Second beat:</strong> Destination square note.</li>
42
+ </ul>
43
+ </section>
44
+
45
+ <section>
46
+ <h3>⚡ Special Move Notes</h3>
47
+ <ul>
48
+ <li><strong>Capture:</strong> Pop sound.</li>
49
+ <li><strong>Danger:</strong> Sharp tone (1000 Hz) when piece threatens capture.</li>
50
+ </ul>
51
+ </section>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ )
56
+ }
src/components/ChessBoard.tsx ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useState } from 'react'
2
+ import { Square } from 'chess.js'
3
+ import { ChessPiece } from './ChessPiece'
4
+ import { GameState, DraggedPiece } from '../types/chess'
5
+ import { isSquareLight } from '../utils/chessUtils'
6
+ import { AudioEngine } from '../engines/AudioEngine'
7
+ import '../styles/ChessBoard.css'
8
+ import '../styles/ChessSquare.css'
9
+
10
+ export const ChessBoard: React.FC<{
11
+ gameState: GameState
12
+ draggedPiece: DraggedPiece | null
13
+ audioEngine: AudioEngine | null
14
+ onSquareClick: (square: Square) => void
15
+ onPieceDragStart: (square: Square) => void
16
+ onPieceDrop: (targetSquare: Square | null) => void
17
+ }> = ({
18
+ gameState,
19
+ draggedPiece,
20
+ audioEngine,
21
+ onSquareClick,
22
+ onPieceDragStart,
23
+ onPieceDrop
24
+ }) => {
25
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
26
+ const [isDragging, setIsDragging] = useState(false)
27
+
28
+ const handleMouseMove = useCallback((event: React.MouseEvent) => {
29
+ setMousePosition({ x: event.clientX, y: event.clientY })
30
+ }, [])
31
+
32
+ const handleSquareClick = useCallback((square: Square) => {
33
+ if (!isDragging) {
34
+ onSquareClick(square)
35
+ }
36
+ }, [onSquareClick, isDragging])
37
+
38
+ const handleMouseDown = useCallback((square: Square) => {
39
+ const piece = gameState.board.get(square)
40
+ if (piece && piece.color === gameState.playerColor) {
41
+ setIsDragging(true)
42
+ onPieceDragStart(square)
43
+ }
44
+ }, [gameState, onPieceDragStart])
45
+
46
+ const handleMouseUp = useCallback((square: Square) => {
47
+ if (isDragging) {
48
+ onPieceDrop(square)
49
+ setIsDragging(false)
50
+ }
51
+ }, [isDragging, onPieceDrop])
52
+
53
+ const renderSquares = () => {
54
+ const renderedSquares = []
55
+
56
+ for (let rank = 8; rank >= 1; rank--) {
57
+ for (let file = 0; file < 8; file++) {
58
+ const square = (String.fromCharCode(97 + file) + rank) as Square
59
+
60
+ let displayFile = file
61
+ let displayRank = rank
62
+
63
+ if (gameState.playerColor === 'b') {
64
+ displayFile = 7 - file
65
+ displayRank = 9 - rank
66
+ }
67
+
68
+ const piece = gameState.board.get(square) || null
69
+ const isSelected = gameState.selectedSquare === square
70
+ const isLegalMove = gameState.legalMoves.some(move => move.to === square)
71
+ const isDraggedSquare = draggedPiece?.square === square
72
+ const isLight = isSquareLight(square)
73
+
74
+ renderedSquares.push(
75
+ <div
76
+ key={square}
77
+ className={`chess-square ${isLight ? 'light' : 'dark'} ${isSelected ? 'selected' : ''} ${isLegalMove ? 'legal-move' : ''}`}
78
+ style={{
79
+ position: 'absolute',
80
+ left: displayFile * 75,
81
+ top: (8 - displayRank) * 75,
82
+ width: 75,
83
+ height: 75
84
+ }}
85
+ onClick={() => handleSquareClick(square)}
86
+ onMouseDown={() => handleMouseDown(square)}
87
+ onMouseUp={() => handleMouseUp(square)}
88
+ >
89
+ {piece && !isDraggedSquare && (
90
+ <div className="piece-container">
91
+ <ChessPiece piece={piece} size={75} />
92
+ </div>
93
+ )}
94
+
95
+ {isLegalMove && (
96
+ <div className="legal-move-indicator" />
97
+ )}
98
+ </div>
99
+ )
100
+ }
101
+ }
102
+
103
+ return renderedSquares
104
+ }
105
+
106
+ const renderDraggedPiece = () => {
107
+ if (!draggedPiece) return null
108
+
109
+ return (
110
+ <div
111
+ className="dragged-piece"
112
+ style={{
113
+ left: mousePosition.x - 37.5,
114
+ top: mousePosition.y - 37.5,
115
+ position: 'fixed',
116
+ pointerEvents: 'none',
117
+ zIndex: 1000
118
+ }}
119
+ >
120
+ <ChessPiece
121
+ piece={draggedPiece.piece}
122
+ size={75}
123
+ />
124
+ </div>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <div
130
+ className="chess-board"
131
+ onMouseMove={handleMouseMove}
132
+ >
133
+ <div className="board-container">
134
+ <div className="board-squares">
135
+ {renderSquares()}
136
+ </div>
137
+
138
+ <div className="board-border" />
139
+ <div className="file-labels">
140
+ {Array.from({ length: 8 }, (_, i) => {
141
+ const fileIndex = gameState.playerColor === 'w' ? i : 7 - i
142
+ const fileLabel = String.fromCharCode(97 + fileIndex)
143
+ const noteName = audioEngine?.getFileNoteName(fileIndex) || ''
144
+ return (
145
+ <div key={fileLabel} className="file-label">
146
+ <span className="file-letter">{fileLabel}</span>
147
+ <span className="file-note">({noteName})</span>
148
+ </div>
149
+ )
150
+ })}
151
+ </div>
152
+ <div className="rank-labels">
153
+ {Array.from({ length: 8 }, (_, i) => {
154
+ const rankIndex = gameState.playerColor === 'w' ? 8 - i : i + 1
155
+ const octave = gameState.playerColor === 'w' ? 8 - i : i + 1
156
+ return (
157
+ <div key={rankIndex} className="rank-label">
158
+ <span className="rank-number">{rankIndex}</span>
159
+ <span className="rank-octave">(♪{octave})</span>
160
+ </div>
161
+ )
162
+ })}
163
+ </div>
164
+ </div>
165
+
166
+ {renderDraggedPiece()}
167
+ </div>
168
+ )
169
+ }
src/components/ChessPiece.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { ChessPiece as ChessPieceType } from '../types/chess'
3
+ import '../styles/ChessPiece.css'
4
+
5
+ export const ChessPiece: React.FC<{
6
+ piece: ChessPieceType
7
+ size: number
8
+ }> = ({
9
+ piece,
10
+ size,
11
+ }) => {
12
+
13
+ const getPieceImagePath = () => {
14
+ const color = piece.color === 'w' ? 'white' : 'dark'
15
+ const orientation = 'upright'
16
+
17
+ const pieceFileMap: { [key: string]: string } = {
18
+ 'k': piece.color === 'w' ? 'Chess_klt45.svg' : 'Chess_kdt45.svg',
19
+ 'q': piece.color === 'w' ? 'Chess_qlt45.svg' : 'Chess_qdt45.svg',
20
+ 'r': piece.color === 'w' ? 'Chess_rlt45.svg' : 'Chess_rdt45.svg',
21
+ 'b': piece.color === 'w' ? 'Chess_blt45.svg' : 'Chess_bdt45.svg',
22
+ 'n': piece.color === 'w' ? 'Chess_nlt45.svg' : 'Chess_ndt45.svg',
23
+ 'p': piece.color === 'w' ? 'Chess_plt45.svg' : 'Chess_pdt45.svg'
24
+ }
25
+
26
+ const fileName = pieceFileMap[piece.type]
27
+ return `/assets/pieces/${color}/${orientation}/${fileName}`
28
+ }
29
+
30
+ return (
31
+ <div
32
+ className="chess-piece"
33
+ style={{
34
+ width: size,
35
+ height: size
36
+ }}
37
+ >
38
+ <img
39
+ src={getPieceImagePath()}
40
+ alt={`${piece.color === 'w' ? 'White' : 'Black'} ${piece.type}`}
41
+ width={size}
42
+ height={size}
43
+ className="piece-image"
44
+ draggable={false}
45
+ />
46
+ </div>
47
+ )
48
+ }
src/components/GameControls.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { GameState, CHESS_MODELS } from '../types/chess'
3
+ import { VolumeSlider } from './VolumeSlider'
4
+ import '../styles/GameControls.css'
5
+
6
+ export const GameControls: React.FC<{
7
+ gameState: GameState
8
+ audioEnabled: boolean
9
+ volumeSettings: {
10
+ ambient: number
11
+ game: number
12
+ }
13
+ selectedModel: string
14
+ onStartGame: () => void
15
+ onResignGame: () => void
16
+ onToggleColor: () => void
17
+ onToggleAudio: () => void
18
+ onVolumeChange: (type: "ambient" | "game", value: number) => void
19
+ onModelChange: (modelId: string) => void
20
+ }> = ({
21
+ gameState,
22
+ audioEnabled,
23
+ volumeSettings,
24
+ selectedModel,
25
+ onStartGame,
26
+ onResignGame,
27
+ onToggleColor,
28
+ onToggleAudio,
29
+ onVolumeChange,
30
+ onModelChange
31
+ }) => {
32
+ const getMainButtonText = () => {
33
+ if (gameState.gameActive) {
34
+ return '❌ Resign Game'
35
+ }
36
+ return '🎮 Start Game'
37
+ }
38
+
39
+ const getMainButtonAction = () => {
40
+ if (gameState.gameActive) {
41
+ return onResignGame
42
+ }
43
+ return onStartGame
44
+ }
45
+
46
+ const getColorButtonText = () => {
47
+ return `⚪ Play as ${gameState.playerColor === 'w' ? 'White' : 'Black'}`
48
+ }
49
+
50
+ const getAudioButtonText = () => {
51
+ return `🔊 Audio: ${audioEnabled ? 'ON' : 'OFF'}`
52
+ }
53
+
54
+ const getGameStatus = () => {
55
+ if (gameState.aiModelLoading) {
56
+ return (
57
+ <div className="thinking-indicator">
58
+ Loading AI model<span className="thinking-dots"></span>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ if (gameState.gameActive) {
64
+ if (gameState.aiThinking) {
65
+ return (
66
+ <div className="thinking-indicator">
67
+ AI is thinking<span className="thinking-dots"></span>
68
+ </div>
69
+ )
70
+ } else if (gameState.board.turn() === gameState.playerColor) {
71
+ return `Your turn (${gameState.playerColor === 'w' ? 'White' : 'Black'})`
72
+ } else {
73
+ return `AI turn (${gameState.playerColor === 'w' ? 'Black' : 'White'})`
74
+ }
75
+ } else if (gameState.gameResult?.isGameOver) {
76
+ return gameState.gameResult.message
77
+ } else {
78
+ const aiStatusText = gameState.aiModelLoaded ? '' : ' (AI: Fallback mode)'
79
+ return `Click 'Start Game' to begin${aiStatusText}`
80
+ }
81
+ }
82
+
83
+ return (
84
+ <div className="game-controls">
85
+ <div className="game-status">
86
+ {getGameStatus()}
87
+ </div>
88
+
89
+ <div className="control-buttons">
90
+ <button
91
+ className="button main-button"
92
+ onClick={getMainButtonAction()}
93
+ >
94
+ {getMainButtonText()}
95
+ </button>
96
+
97
+ {!gameState.gameActive && (
98
+ <>
99
+ <button
100
+ className="button color-button"
101
+ onClick={onToggleColor}
102
+ >
103
+ {getColorButtonText()}
104
+ </button>
105
+
106
+ <div className="model-selector">
107
+ <label htmlFor="model-select">🤖 Chess Model:</label>
108
+ <select
109
+ id="model-select"
110
+ className="model-dropdown"
111
+ value={selectedModel}
112
+ onChange={(e) => onModelChange(e.target.value)}
113
+ disabled={gameState.aiModelLoading}
114
+ >
115
+ {CHESS_MODELS.map((model) => (
116
+ <option key={model} value={model}>
117
+ {model}
118
+ </option>
119
+ ))}
120
+ </select>
121
+ </div>
122
+ </>
123
+ )}
124
+
125
+ <button
126
+ className="button audio-button"
127
+ onClick={onToggleAudio}
128
+ >
129
+ {getAudioButtonText()}
130
+ </button>
131
+ </div>
132
+
133
+ {audioEnabled && (
134
+ <div className="volume-controls">
135
+ <h3 className="volume-controls-title">Audio Volume</h3>
136
+
137
+ <VolumeSlider
138
+ label="🎵 Initative Tempo"
139
+ value={volumeSettings.ambient}
140
+ onChange={(value) => onVolumeChange('ambient', value)}
141
+ />
142
+
143
+ <VolumeSlider
144
+ label="♟️ Move Notes"
145
+ value={volumeSettings.game}
146
+ onChange={(value) => onVolumeChange('game', value)}
147
+ />
148
+ </div>
149
+ )}
150
+ </div>
151
+ )
152
+ }
src/components/PromotionDialog.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { Color, PieceSymbol } from 'chess.js'
3
+ import { ChessPiece } from './ChessPiece'
4
+ import '../styles/PromotionDialog.css'
5
+
6
+ export const PromotionDialog: React.FC<{
7
+ isVisible: boolean
8
+ color: Color
9
+ onSelect: (piece: 'q' | 'r' | 'b' | 'n') => void
10
+ }> = ({
11
+ isVisible,
12
+ color,
13
+ onSelect
14
+ }) => {
15
+ if (!isVisible) return null
16
+
17
+ const promotionPieces: Array<{ type: 'q' | 'r' | 'b' | 'n'; symbol: string }> = [
18
+ { type: 'q', symbol: color === 'w' ? '♕' : '♛' },
19
+ { type: 'r', symbol: color === 'w' ? '♖' : '♜' },
20
+ { type: 'b', symbol: color === 'w' ? '♗' : '♝' },
21
+ { type: 'n', symbol: color === 'w' ? '♘' : '♞' }
22
+ ]
23
+
24
+ return (
25
+ <div className="promotion-dialog-overlay">
26
+ <div className="promotion-dialog">
27
+ <div className="promotion-dialog-title">
28
+ Choose Promotion Piece
29
+ </div>
30
+ <div className="promotion-buttons">
31
+ {promotionPieces.map(({ type, symbol }) => (
32
+ <button
33
+ key={type}
34
+ className="promotion-button"
35
+ onClick={() => onSelect(type)}
36
+ >
37
+ <ChessPiece
38
+ piece={{ type: type as PieceSymbol, color }}
39
+ size={48}
40
+ />
41
+ </button>
42
+ ))}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ )
47
+ }
src/components/VolumeSlider.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import '../styles/VolumeSlider.css'
3
+
4
+ export const VolumeSlider: React.FC<{
5
+ label: string
6
+ value: number
7
+ onChange: (value: number) => void
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ }> = ({
12
+ label,
13
+ value,
14
+ onChange,
15
+ min = 0,
16
+ max = 1,
17
+ step = 0.01
18
+ }) => {
19
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
20
+ const newValue = parseFloat(event.target.value)
21
+ onChange(newValue)
22
+ }
23
+
24
+ const percentage = Math.round((value / max) * 100)
25
+
26
+ return (
27
+ <div className="volume-slider">
28
+ <div className="volume-slider-header">
29
+ <label className="volume-slider-label">{label}</label>
30
+ <span className="volume-slider-value">{percentage}%</span>
31
+ </div>
32
+ <input
33
+ type="range"
34
+ className="volume-slider-input"
35
+ min={min}
36
+ max={max}
37
+ step={step}
38
+ value={value}
39
+ onChange={handleChange}
40
+ />
41
+ </div>
42
+ )
43
+ }
src/engines/AudioEngine.ts ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Chess, Square, Move, PieceSymbol, Color } from 'chess.js'
2
+ import { PositionEvaluator } from './PositionEvaluator'
3
+
4
+ export class AudioEngine {
5
+ private audioContext: AudioContext | null = null
6
+ private masterGain: GainNode | null = null
7
+ private isActive: boolean = false
8
+ private volume: number = 0.7
9
+ private ambientVolume: number = 0.8
10
+ private gameVolume: number = 0.048
11
+ private userInterval: NodeJS.Timeout | null = null
12
+ private boardFlipped: boolean = false
13
+
14
+ private noteFrequencies = {
15
+ 'A': 27.50, // A0
16
+ 'B': 30.87, // B0
17
+ 'Cb': 32.70, // C1♭
18
+ 'C': 32.70, // C1
19
+ 'D': 36.71, // D1
20
+ 'Db': 34.65, // D1♭
21
+ 'E': 41.20, // E1
22
+ 'F': 43.65 // F1
23
+ }
24
+ private fileNotes = ['A', 'B', 'Cb', 'C', 'D', 'Db', 'E', 'F']
25
+
26
+ private fileFrequencies = [
27
+ this.noteFrequencies.A,
28
+ this.noteFrequencies.B,
29
+ this.noteFrequencies.Cb,
30
+ this.noteFrequencies.C,
31
+ this.noteFrequencies.D,
32
+ this.noteFrequencies.Db,
33
+ this.noteFrequencies.E,
34
+ this.noteFrequencies.F
35
+ ]
36
+ private userInitiativeFreq = 110.0 // A2
37
+
38
+ constructor() {
39
+ this.initialize()
40
+ }
41
+
42
+ private async initialize() {
43
+ try {
44
+ this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
45
+ this.masterGain = this.audioContext.createGain()
46
+ this.masterGain.connect(this.audioContext.destination)
47
+ this.masterGain.gain.value = this.volume
48
+ this.isActive = true
49
+ console.log('Audio engine initialized')
50
+ } catch (error) {
51
+ console.error('Failed to initialize audio engine:', error)
52
+ this.isActive = false
53
+ }
54
+ }
55
+
56
+ setBoardFlipped(flipped: boolean) {
57
+ this.boardFlipped = flipped
58
+ }
59
+
60
+ private getSquareFrequency(square: Square): number {
61
+ const file = square.charCodeAt(0) - 97
62
+ const rank = parseInt(square[1]) - 1
63
+ const actualFile = this.boardFlipped ? 7 - file : file
64
+ const actualRank = this.boardFlipped ? 7 - rank : rank
65
+ const baseFreq = this.fileFrequencies[actualFile]
66
+ const octaveMultiplier = Math.pow(2, actualRank)
67
+ return baseFreq * octaveMultiplier
68
+ }
69
+
70
+ getFileNoteName(fileIndex: number): string {
71
+ const actualFile = this.boardFlipped ? 7 - fileIndex : fileIndex
72
+ return this.fileNotes[actualFile]
73
+ }
74
+
75
+ setVolume(volume: number) {
76
+ this.volume = Math.max(0, Math.min(1, volume))
77
+ if (this.masterGain) {
78
+ this.masterGain.gain.value = this.volume
79
+ }
80
+ if (this.volume === 0) {
81
+ this.stopAllAudio()
82
+ }
83
+ }
84
+
85
+ setAmbientVolume(volume: number) {
86
+ this.ambientVolume = Math.max(0, Math.min(1, volume))
87
+ }
88
+
89
+ setGameVolume(volume: number) {
90
+ this.gameVolume = Math.max(0, Math.min(0.1, volume * 0.1))
91
+ }
92
+
93
+ getAmbientVolume(): number {
94
+ return this.ambientVolume
95
+ }
96
+
97
+ getGameVolume(): number {
98
+ return this.gameVolume / 0.1
99
+ }
100
+
101
+ async ensureAudioContext() {
102
+ if (!this.audioContext) {
103
+ await this.initialize()
104
+ }
105
+
106
+ if (this.audioContext?.state === 'suspended') {
107
+ await this.audioContext.resume()
108
+ }
109
+ }
110
+
111
+ private createTone(frequency: number, duration: number, fadeOut: boolean = true, volume: number = 0.4): Promise<void> {
112
+ return new Promise((resolve) => {
113
+ if (!this.audioContext || !this.masterGain || this.volume === 0) {
114
+ resolve()
115
+ return
116
+ }
117
+
118
+ const oscillator = this.audioContext.createOscillator()
119
+ const gainNode = this.audioContext.createGain()
120
+
121
+ oscillator.connect(gainNode)
122
+ gainNode.connect(this.masterGain)
123
+
124
+ oscillator.frequency.value = frequency
125
+ oscillator.type = 'sine'
126
+
127
+ const now = this.audioContext.currentTime
128
+ gainNode.gain.setValueAtTime(volume, now)
129
+
130
+ if (fadeOut) {
131
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration)
132
+ } else {
133
+ gainNode.gain.setValueAtTime(volume, now + duration - 0.01)
134
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration)
135
+ }
136
+
137
+ oscillator.start(now)
138
+ oscillator.stop(now + duration)
139
+
140
+ oscillator.onended = () => resolve()
141
+ })
142
+ }
143
+
144
+ async playMoveSound(move: Move, board: Chess, capturedPiece?: any) {
145
+ if (!this.isActive || this.volume === 0) return
146
+
147
+ try {
148
+ await this.ensureAudioContext()
149
+ const originFreq = this.getSquareFrequency(move.from as Square)
150
+ const destFreq = this.getSquareFrequency(move.to as Square)
151
+ const beatDuration = 0.2
152
+ const pauseDuration = 0.08
153
+ await this.createTone(originFreq, beatDuration, false, this.gameVolume)
154
+ await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000))
155
+ await this.createTone(destFreq, beatDuration, false, this.gameVolume)
156
+ if (capturedPiece) {
157
+ await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000))
158
+ await this.createCaptureSound()
159
+ } else {
160
+ const canCapture = this.checkForDanger(move.to as Square, board)
161
+ if (canCapture) {
162
+ await new Promise(resolve => setTimeout(resolve, pauseDuration * 1000))
163
+ await this.createDangerSound()
164
+ }
165
+ }
166
+ } catch (error) {
167
+ console.error('Error playing move sound:', error)
168
+ }
169
+ }
170
+
171
+ private async createCaptureSound() {
172
+ if (!this.audioContext || !this.masterGain) return
173
+ const duration = 0.2
174
+ const now = this.audioContext.currentTime
175
+ const frequencies = [400, 800, 1200]
176
+ const oscillators: OscillatorNode[] = []
177
+ const gainNodes: GainNode[] = []
178
+
179
+ frequencies.forEach((freq, index) => {
180
+ const oscillator = this.audioContext!.createOscillator()
181
+ const gainNode = this.audioContext!.createGain()
182
+ oscillator.connect(gainNode)
183
+ gainNode.connect(this.masterGain!)
184
+ oscillator.frequency.value = freq
185
+ oscillator.type = 'sine'
186
+ const harmonic_volume = this.gameVolume * (1 / (index + 1))
187
+ gainNode.gain.setValueAtTime(harmonic_volume, now)
188
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration)
189
+ oscillator.start(now)
190
+ oscillator.stop(now + duration)
191
+ oscillators.push(oscillator)
192
+ gainNodes.push(gainNode)
193
+ })
194
+
195
+ return new Promise<void>(resolve => {
196
+ oscillators[oscillators.length - 1].onended = () => resolve()
197
+ })
198
+ }
199
+
200
+ private async createDangerSound() {
201
+ if (!this.audioContext || !this.masterGain) return
202
+ const duration = 0.15
203
+ const now = this.audioContext.currentTime
204
+ const oscillator = this.audioContext.createOscillator()
205
+ const gainNode = this.audioContext.createGain()
206
+ oscillator.connect(gainNode)
207
+ gainNode.connect(this.masterGain)
208
+ oscillator.frequency.value = 1000
209
+ oscillator.type = 'sawtooth'
210
+ gainNode.gain.setValueAtTime(this.gameVolume, now)
211
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration)
212
+ oscillator.start(now)
213
+ oscillator.stop(now + duration)
214
+
215
+ return new Promise<void>(resolve => {
216
+ oscillator.onended = () => resolve()
217
+ })
218
+ }
219
+
220
+ private checkForDanger(square: Square, board: Chess): boolean {
221
+ try {
222
+ const moves = board.moves({ square, verbose: true })
223
+ return moves.some((move: any) => move.captured)
224
+ } catch {
225
+ return false
226
+ }
227
+ }
228
+
229
+
230
+ async updatePositionAudio(board: Chess, userColor: Color = 'w') {
231
+ if (!this.isActive || this.volume === 0) return
232
+
233
+ try {
234
+ await this.ensureAudioContext()
235
+ this.startContinuousInitiativeAudio(board, userColor)
236
+ } catch (error) {
237
+ console.error('Error updating position audio:', error)
238
+ }
239
+ }
240
+
241
+ private startContinuousInitiativeAudio(board: Chess, userColor: Color = 'w') {
242
+ if (!this.audioContext || !this.masterGain) return
243
+ this.stopPositionAudio()
244
+ const userInitiative = PositionEvaluator.getInitiative(board, userColor)
245
+ const userBeatsPerMinute = userInitiative * 120
246
+ if (userBeatsPerMinute > 0) {
247
+ const userInterval = (60 / userBeatsPerMinute) * 1000
248
+ this.userInterval = setInterval(() => {
249
+ this.playInitiativeBeat(this.userInitiativeFreq)
250
+ }, userInterval)
251
+ }
252
+ }
253
+
254
+ private async playInitiativeBeat(frequency: number) {
255
+ if (!this.audioContext || !this.masterGain || this.volume === 0) return
256
+ const oscillator = this.audioContext.createOscillator()
257
+ const gainNode = this.audioContext.createGain()
258
+ oscillator.connect(gainNode)
259
+ gainNode.connect(this.masterGain)
260
+ oscillator.frequency.value = frequency
261
+ oscillator.type = 'sine'
262
+ const now = this.audioContext.currentTime
263
+ const duration = 0.1
264
+ const volume = this.ambientVolume
265
+ gainNode.gain.setValueAtTime(volume, now)
266
+ gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration)
267
+ oscillator.start(now)
268
+ oscillator.stop(now + duration)
269
+ }
270
+
271
+ updateInitiativeVolumes(board: Chess, userColor: Color = 'w') {
272
+ this.startContinuousInitiativeAudio(board, userColor)
273
+ }
274
+
275
+ stopPositionAudio() {
276
+ if (this.userInterval) {
277
+ clearInterval(this.userInterval)
278
+ this.userInterval = null
279
+ }
280
+ }
281
+
282
+ stopAllAudio() {
283
+ this.stopPositionAudio()
284
+ if (this.audioContext) {
285
+ try {
286
+ if (this.masterGain) {
287
+ this.masterGain.disconnect()
288
+ this.masterGain = this.audioContext.createGain()
289
+ this.masterGain.connect(this.audioContext.destination)
290
+ this.masterGain.gain.value = this.volume
291
+ }
292
+ } catch (error) {
293
+ console.error('Error stopping audio:', error)
294
+ }
295
+ }
296
+ }
297
+
298
+ isPlaying(): boolean {
299
+ return this.userInterval !== null
300
+ }
301
+
302
+ cleanup() {
303
+ this.stopAllAudio()
304
+ this.isActive = false
305
+
306
+ if (this.audioContext) {
307
+ this.audioContext.close()
308
+ this.audioContext = null
309
+ }
310
+ }
311
+ }
src/engines/ChessAI.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Chess, Move } from 'chess.js'
2
+ import { pipeline } from '@xenova/transformers'
3
+
4
+ export class ChessAI {
5
+ private model: any = null
6
+ private isLoading: boolean = false
7
+ private modelId: string
8
+ constructor(modelId: string = 'mlabonne/chesspythia-70m') {
9
+ this.modelId = modelId
10
+ }
11
+
12
+ async initialize(): Promise<void> {
13
+ if (this.model || this.isLoading) return
14
+
15
+ this.isLoading = true
16
+ try {
17
+ this.model = await pipeline('text-generation', this.modelId)
18
+ } catch (error) {
19
+ console.error('Failed to load chess model:', error)
20
+ this.model = null
21
+ } finally {
22
+ this.isLoading = false
23
+ }
24
+ }
25
+
26
+ async getMove(chess: Chess, timeLimit: number = 10000): Promise<Move | null> {
27
+ if (!this.model) {
28
+ console.log('Using fallback AI (model not loaded)')
29
+ return this.getFallbackMove(chess)
30
+ }
31
+
32
+ try {
33
+ const legalMoves = chess.moves({ verbose: true })
34
+ if (legalMoves.length === 0) return null
35
+ const prompt = this.createChessPrompt(chess)
36
+ const startTime = Date.now()
37
+ const result = await Promise.race([
38
+ this.generateMove(prompt, legalMoves),
39
+ new Promise<null>((_, reject) =>
40
+ setTimeout(() => reject(new Error('Timeout')), timeLimit)
41
+ )
42
+ ])
43
+ const elapsedTime = Date.now() - startTime
44
+ console.log(`AI move generated in ${elapsedTime}ms`)
45
+ return result || this.getFallbackMove(chess)
46
+ } catch (error) {
47
+ console.error('Error generating AI move:', error)
48
+ return this.getFallbackMove(chess)
49
+ }
50
+ }
51
+
52
+ private async generateMove(prompt: string, legalMoves: Move[]): Promise<Move | null> {
53
+ if (!this.model) return null
54
+
55
+ try {
56
+ const output = await this.model(prompt, {
57
+ max_new_tokens: 10,
58
+ temperature: 0.7
59
+ })
60
+ const generatedText = Array.isArray(output) ? output[0]?.generated_text : output.generated_text
61
+ console.log('Model output:', generatedText)
62
+ const move = this.parseMove(generatedText, legalMoves)
63
+ return move
64
+ } catch (error) {
65
+ console.error('Error in model generation:', error)
66
+ return null
67
+ }
68
+ }
69
+
70
+ private createChessPrompt(chess: Chess): string {
71
+ const turn = chess.turn()
72
+ const moveNumber = chess.moveNumber()
73
+ if (turn === 'w') {
74
+ return `${moveNumber}.`
75
+ } else {
76
+ return `${moveNumber}...`
77
+ }
78
+ }
79
+
80
+ private parseMove(generatedText: string, legalMoves: Move[]): Move | null {
81
+ if (!generatedText) return null
82
+ const cleanText = generatedText.trim().replace(/[+#]$/, '')
83
+ for (const move of legalMoves) {
84
+ if (move.san === cleanText || move.lan === cleanText) {
85
+ return move
86
+ }
87
+ }
88
+ for (const move of legalMoves) {
89
+ if (move.san.startsWith(cleanText) || cleanText.includes(move.san)) {
90
+ return move
91
+ }
92
+ }
93
+ console.log(`Could not parse move "${cleanText}" from legal moves:`, legalMoves.map(m => m.san))
94
+ return null
95
+ }
96
+
97
+ private getFallbackMove(chess: Chess): Move | null {
98
+ const legalMoves = chess.moves({ verbose: true })
99
+ if (legalMoves.length === 0) return null
100
+ let candidateMoves = legalMoves.filter(move => move.captured)
101
+ if (candidateMoves.length === 0) {
102
+ candidateMoves = legalMoves.filter(move => {
103
+ chess.move(move)
104
+ const isCheck = chess.inCheck()
105
+ chess.undo()
106
+ return isCheck
107
+ })
108
+ }
109
+ if (candidateMoves.length === 0) {
110
+ candidateMoves = legalMoves
111
+ }
112
+ const randomIndex = Math.floor(Math.random() * candidateMoves.length)
113
+ return candidateMoves[randomIndex]
114
+ }
115
+
116
+ isModelLoaded(): boolean {
117
+ return this.model !== null
118
+ }
119
+
120
+ isModelLoading(): boolean {
121
+ return this.isLoading
122
+ }
123
+
124
+ getModelInfo(): string {
125
+ if (this.isLoading) return 'Loading...'
126
+ if (this.model) return `${this.modelId} (Loaded)`
127
+ return `${this.modelId} (Not loaded)`
128
+ }
129
+ }
src/engines/PositionEvaluator.ts ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Chess, Square, PieceSymbol, Color } from 'chess.js'
2
+
3
+ export class PositionEvaluator {
4
+ private static readonly PIECE_VALUES: Record<PieceSymbol, number> = {
5
+ 'p': 100,
6
+ 'n': 320,
7
+ 'b': 330,
8
+ 'r': 500,
9
+ 'q': 900,
10
+ 'k': 20000
11
+ }
12
+
13
+ private static readonly PAWN_TABLE = [
14
+ 0, 0, 0, 0, 0, 0, 0, 0,
15
+ 50, 50, 50, 50, 50, 50, 50, 50,
16
+ 10, 10, 20, 30, 30, 20, 10, 10,
17
+ 5, 5, 10, 25, 25, 10, 5, 5,
18
+ 0, 0, 0, 20, 20, 0, 0, 0,
19
+ 5, -5,-10, 0, 0,-10, -5, 5,
20
+ 5, 10, 10,-20,-20, 10, 10, 5,
21
+ 0, 0, 0, 0, 0, 0, 0, 0
22
+ ]
23
+
24
+ private static readonly KNIGHT_TABLE = [
25
+ -50,-40,-30,-30,-30,-30,-40,-50,
26
+ -40,-20, 0, 0, 0, 0,-20,-40,
27
+ -30, 0, 10, 15, 15, 10, 0,-30,
28
+ -30, 5, 15, 20, 20, 15, 5,-30,
29
+ -30, 0, 15, 20, 20, 15, 0,-30,
30
+ -30, 5, 10, 15, 15, 10, 5,-30,
31
+ -40,-20, 0, 5, 5, 0,-20,-40,
32
+ -50,-40,-30,-30,-30,-30,-40,-50
33
+ ]
34
+
35
+ private static readonly BISHOP_TABLE = [
36
+ -20,-10,-10,-10,-10,-10,-10,-20,
37
+ -10, 0, 0, 0, 0, 0, 0,-10,
38
+ -10, 0, 5, 10, 10, 5, 0,-10,
39
+ -10, 5, 5, 10, 10, 5, 5,-10,
40
+ -10, 0, 10, 10, 10, 10, 0,-10,
41
+ -10, 10, 10, 10, 10, 10, 10,-10,
42
+ -10, 5, 0, 0, 0, 0, 5,-10,
43
+ -20,-10,-10,-10,-10,-10,-10,-20
44
+ ]
45
+
46
+ private static readonly ROOK_TABLE = [
47
+ 0, 0, 0, 0, 0, 0, 0, 0,
48
+ 5, 10, 10, 10, 10, 10, 10, 5,
49
+ -5, 0, 0, 0, 0, 0, 0, -5,
50
+ -5, 0, 0, 0, 0, 0, 0, -5,
51
+ -5, 0, 0, 0, 0, 0, 0, -5,
52
+ -5, 0, 0, 0, 0, 0, 0, -5,
53
+ -5, 0, 0, 0, 0, 0, 0, -5,
54
+ 0, 0, 0, 5, 5, 0, 0, 0
55
+ ]
56
+
57
+ private static readonly QUEEN_TABLE = [
58
+ -20,-10,-10, -5, -5,-10,-10,-20,
59
+ -10, 0, 0, 0, 0, 0, 0,-10,
60
+ -10, 0, 5, 5, 5, 5, 0,-10,
61
+ -5, 0, 5, 5, 5, 5, 0, -5,
62
+ 0, 0, 5, 5, 5, 5, 0, -5,
63
+ -10, 5, 5, 5, 5, 5, 0,-10,
64
+ -10, 0, 5, 0, 0, 0, 0,-10,
65
+ -20,-10,-10, -5, -5,-10,-10,-20
66
+ ]
67
+
68
+ private static readonly KING_MIDDLE_GAME_TABLE = [
69
+ -30,-40,-40,-50,-50,-40,-40,-30,
70
+ -30,-40,-40,-50,-50,-40,-40,-30,
71
+ -30,-40,-40,-50,-50,-40,-40,-30,
72
+ -30,-40,-40,-50,-50,-40,-40,-30,
73
+ -20,-30,-30,-40,-40,-30,-30,-20,
74
+ -10,-20,-20,-20,-20,-20,-20,-10,
75
+ 20, 20, 0, 0, 0, 0, 20, 20,
76
+ 20, 30, 10, 0, 0, 10, 30, 20
77
+ ]
78
+
79
+ private static readonly KING_END_GAME_TABLE = [
80
+ -50,-40,-30,-20,-20,-30,-40,-50,
81
+ -30,-20,-10, 0, 0,-10,-20,-30,
82
+ -30,-10, 20, 30, 30, 20,-10,-30,
83
+ -30,-10, 30, 40, 40, 30,-10,-30,
84
+ -30,-10, 30, 40, 40, 30,-10,-30,
85
+ -30,-10, 20, 30, 30, 20,-10,-30,
86
+ -30,-30, 0, 0, 0, 0,-30,-30,
87
+ -50,-30,-30,-30,-30,-30,-30,-50
88
+ ]
89
+
90
+ /**
91
+ * Evaluates the current position from White's perspective
92
+ * Positive values favor White, negative values favor Black
93
+ */
94
+ public static evaluatePosition(board: Chess): number {
95
+ if (board.isGameOver()) {
96
+ if (board.isCheckmate()) {
97
+ return board.turn() === 'w' ? -30000 : 30000
98
+ }
99
+ return 0 // draw
100
+ }
101
+
102
+ let score = 0
103
+ const isEndGame = this.isEndGame(board)
104
+
105
+ // material and positional evaluation
106
+ for (let rank = 0; rank < 8; rank++) {
107
+ for (let file = 0; file < 8; file++) {
108
+ const square = (String.fromCharCode(97 + file) + (rank + 1)) as Square
109
+ const piece = board.get(square)
110
+
111
+ if (piece) {
112
+ const pieceValue = this.evaluatePiece(piece.type, piece.color, rank, file, isEndGame)
113
+ score += piece.color === 'w' ? pieceValue : -pieceValue
114
+ }
115
+ }
116
+ }
117
+
118
+ // mobility bonus
119
+ const { whiteMobility, blackMobility } = this.calculateMobility(board)
120
+ score += (whiteMobility - blackMobility) * 10
121
+
122
+ // king safety in middle game
123
+ if (!isEndGame) {
124
+ score += this.evaluateKingSafety(board, 'w')
125
+ score -= this.evaluateKingSafety(board, 'b')
126
+ }
127
+
128
+ // pawn structure
129
+ score += this.evaluatePawnStructure(board)
130
+
131
+ // center control
132
+ score += this.evaluateCenterControl(board)
133
+
134
+ return score
135
+ }
136
+
137
+ private static evaluatePiece(
138
+ pieceType: PieceSymbol,
139
+ color: Color,
140
+ rank: number,
141
+ file: number,
142
+ isEndGame: boolean
143
+ ): number {
144
+ const index = rank * 8 + file
145
+ const flippedIndex = color === 'w' ? (7 - rank) * 8 + file : index
146
+
147
+ let value = this.PIECE_VALUES[pieceType]
148
+
149
+ switch (pieceType) {
150
+ case 'p':
151
+ value += this.PAWN_TABLE[flippedIndex]
152
+ break
153
+ case 'n':
154
+ value += this.KNIGHT_TABLE[flippedIndex]
155
+ break
156
+ case 'b':
157
+ value += this.BISHOP_TABLE[flippedIndex]
158
+ break
159
+ case 'r':
160
+ value += this.ROOK_TABLE[flippedIndex]
161
+ break
162
+ case 'q':
163
+ value += this.QUEEN_TABLE[flippedIndex]
164
+ break
165
+ case 'k':
166
+ if (isEndGame) {
167
+ value += this.KING_END_GAME_TABLE[flippedIndex]
168
+ } else {
169
+ value += this.KING_MIDDLE_GAME_TABLE[flippedIndex]
170
+ }
171
+ break
172
+ }
173
+
174
+ return value
175
+ }
176
+
177
+ private static isEndGame(board: Chess): boolean {
178
+ let materialCount = 0
179
+ const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
180
+
181
+ for (const file of squares) {
182
+ for (let rank = 1; rank <= 8; rank++) {
183
+ const square = (file + rank) as Square
184
+ const piece = board.get(square)
185
+ if (piece && piece.type !== 'k' && piece.type !== 'p') {
186
+ materialCount += this.PIECE_VALUES[piece.type]
187
+ }
188
+ }
189
+ }
190
+
191
+ return materialCount < 2500
192
+ }
193
+
194
+ private static evaluateKingSafety(board: Chess, color: Color): number {
195
+ const kingSquare = this.findKing(board, color)
196
+ if (!kingSquare) return 0
197
+
198
+ let safety = 0
199
+ const kingFile = kingSquare.charCodeAt(0) - 97
200
+ const kingRank = parseInt(kingSquare[1]) - 1
201
+
202
+ // check for pawn shield
203
+ const pawnShieldRank = color === 'w' ? kingRank + 1 : kingRank - 1
204
+ if (pawnShieldRank >= 0 && pawnShieldRank < 8) {
205
+ for (let file = Math.max(0, kingFile - 1); file <= Math.min(7, kingFile + 1); file++) {
206
+ const shieldSquare = (String.fromCharCode(97 + file) + (pawnShieldRank + 1)) as Square
207
+ const piece = board.get(shieldSquare)
208
+ if (piece && piece.type === 'p' && piece.color === color) {
209
+ safety += 30
210
+ }
211
+ }
212
+ }
213
+
214
+ return safety
215
+ }
216
+
217
+ private static evaluatePawnStructure(board: Chess): number {
218
+ let score = 0
219
+ const files = [0, 0, 0, 0, 0, 0, 0, 0]
220
+
221
+ // count pawns
222
+ for (let file = 0; file < 8; file++) {
223
+ let whitePawns = 0
224
+ let blackPawns = 0
225
+
226
+ for (let rank = 0; rank < 8; rank++) {
227
+ const square = (String.fromCharCode(97 + file) + (rank + 1)) as Square
228
+ const piece = board.get(square)
229
+ if (piece && piece.type === 'p') {
230
+ if (piece.color === 'w') whitePawns++
231
+ else blackPawns++
232
+ }
233
+ }
234
+
235
+ // penalty for doubled pawns
236
+ if (whitePawns > 1) score -= (whitePawns - 1) * 50
237
+ if (blackPawns > 1) score += (blackPawns - 1) * 50
238
+
239
+ files[file] = whitePawns - blackPawns
240
+ }
241
+
242
+ // penalty for isolated pawns
243
+ for (let file = 0; file < 8; file++) {
244
+ if (files[file] !== 0) {
245
+ const leftFile = file > 0 ? files[file - 1] : 0
246
+ const rightFile = file < 7 ? files[file + 1] : 0
247
+
248
+ if (leftFile === 0 && rightFile === 0) {
249
+ score -= Math.abs(files[file]) * 20
250
+ }
251
+ }
252
+ }
253
+
254
+ return score
255
+ }
256
+
257
+ private static evaluateCenterControl(board: Chess): number {
258
+ let score = 0
259
+ const centerSquares = ['d4', 'd5', 'e4', 'e5']
260
+
261
+ for (const square of centerSquares) {
262
+ const piece = board.get(square as Square)
263
+ if (piece) {
264
+ const value = piece.type === 'p' ? 20 : 10
265
+ score += piece.color === 'w' ? value : -value
266
+ }
267
+ }
268
+
269
+ return score
270
+ }
271
+
272
+ private static findKing(board: Chess, color: Color): Square | null {
273
+ const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
274
+
275
+ for (const file of squares) {
276
+ for (let rank = 1; rank <= 8; rank++) {
277
+ const square = (file + rank) as Square
278
+ const piece = board.get(square)
279
+ if (piece && piece.type === 'k' && piece.color === color) {
280
+ return square
281
+ }
282
+ }
283
+ }
284
+
285
+ return null
286
+ }
287
+
288
+ private static calculateMobility(board: Chess): { whiteMobility: number, blackMobility: number } {
289
+ const originalFen = board.fen()
290
+ const currentTurn = board.turn()
291
+
292
+ let whiteMobility = 0
293
+ let blackMobility = 0
294
+
295
+ if (currentTurn === 'w') {
296
+ whiteMobility = board.moves().length
297
+
298
+ const fenParts = originalFen.split(' ')
299
+ fenParts[1] = 'b'
300
+ try {
301
+ const blackTurnFen = fenParts.join(' ')
302
+ const blackBoard = new Chess(blackTurnFen)
303
+ blackMobility = blackBoard.moves().length
304
+ } catch {
305
+ blackMobility = 0
306
+ }
307
+ } else {
308
+ blackMobility = board.moves().length
309
+ const fenParts = originalFen.split(' ')
310
+ fenParts[1] = 'w'
311
+ try {
312
+ const whiteTurnFen = fenParts.join(' ')
313
+ const whiteBoard = new Chess(whiteTurnFen)
314
+ whiteMobility = whiteBoard.moves().length
315
+ } catch {
316
+ whiteMobility = 0
317
+ }
318
+ }
319
+
320
+ return { whiteMobility, blackMobility }
321
+ }
322
+
323
+
324
+ public static evaluateForColor(board: Chess, color: Color): number {
325
+ const evaluation = this.evaluatePosition(board)
326
+ return color === 'w' ? evaluation : -evaluation
327
+ }
328
+
329
+
330
+ public static getInitiative(board: Chess, color: Color): number {
331
+ const evaluation = this.evaluateForColor(board, color)
332
+ const gameActivity = this.calculateGameActivity(board)
333
+ const baseInitiative = Math.max(0, Math.min(1, (evaluation + 500) / 1000))
334
+ return baseInitiative * gameActivity
335
+ }
336
+
337
+
338
+ private static calculateGameActivity(board: Chess): number {
339
+ let activity = 0
340
+
341
+ // count pieces not on starting squares
342
+ const developmentScore = this.calculateDevelopment(board)
343
+ activity += Math.min(0.4, developmentScore / 8)
344
+
345
+ // count tactical opportunities
346
+ const tacticalScore = this.calculateTacticalActivity(board)
347
+ activity += Math.min(0.3, tacticalScore / 10)
348
+
349
+ // calc material imbalance
350
+ const materialImbalance = Math.abs(this.calculateMaterialBalance(board))
351
+ activity += Math.min(0.2, materialImbalance / 500)
352
+
353
+ // count king threats
354
+ const kingSafety = this.calculateKingThreats(board)
355
+ activity += Math.min(0.1, kingSafety / 5)
356
+
357
+ return Math.min(1, activity)
358
+ }
359
+
360
+ private static calculateDevelopment(board: Chess): number {
361
+ let development = 0
362
+
363
+ const startingPositions = {
364
+ 'b1': 'n', 'g1': 'n', 'c1': 'b', 'f1': 'b', 'd1': 'q', // white
365
+ 'b8': 'n', 'g8': 'n', 'c8': 'b', 'f8': 'b', 'd8': 'q' // black
366
+ }
367
+
368
+ for (const [square, expectedPiece] of Object.entries(startingPositions)) {
369
+ const piece = board.get(square as Square)
370
+ if (!piece || piece.type !== expectedPiece) {
371
+ development++
372
+ }
373
+ }
374
+
375
+ return development
376
+ }
377
+
378
+ private static calculateTacticalActivity(board: Chess): number {
379
+ let tactical = 0
380
+ const moves = board.moves({ verbose: true })
381
+
382
+ for (const move of moves) {
383
+ if (move.captured) tactical += 2 // captures
384
+ if (move.san.includes('+')) tactical += 1 // checks
385
+ if (move.san.includes('#')) tactical += 3 // checkmate
386
+ if (move.promotion) tactical += 2 // promotions
387
+ }
388
+
389
+ return tactical
390
+ }
391
+
392
+ private static calculateMaterialBalance(board: Chess): number {
393
+ let balance = 0
394
+ const squares = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
395
+
396
+ for (const file of squares) {
397
+ for (let rank = 1; rank <= 8; rank++) {
398
+ const square = (file + rank) as Square
399
+ const piece = board.get(square)
400
+ if (piece && piece.type !== 'k') {
401
+ const value = this.PIECE_VALUES[piece.type]
402
+ balance += piece.color === 'w' ? value : -value
403
+ }
404
+ }
405
+ }
406
+
407
+ return balance
408
+ }
409
+
410
+ private static calculateKingThreats(board: Chess): number {
411
+ let threats = 0
412
+ const whiteKing = this.findKing(board, 'w')
413
+ const blackKing = this.findKing(board, 'b')
414
+ if (whiteKing) threats += this.countAttacksNearSquare(board, whiteKing, 'b')
415
+ if (blackKing) threats += this.countAttacksNearSquare(board, blackKing, 'w')
416
+ return threats
417
+ }
418
+
419
+ private static countAttacksNearSquare(board: Chess, square: Square, attackingColor: Color): number {
420
+ let attacks = 0
421
+ const file = square.charCodeAt(0) - 97
422
+ const rank = parseInt(square[1]) - 1
423
+ for (let f = Math.max(0, file - 1); f <= Math.min(7, file + 1); f++) {
424
+ for (let r = Math.max(0, rank - 1); r <= Math.min(7, rank + 1); r++) {
425
+ const checkSquare = (String.fromCharCode(97 + f) + (r + 1)) as Square
426
+ try {
427
+ const moves = board.moves({ square: checkSquare, verbose: true })
428
+ const hasAttack = moves.some((move: any) => {
429
+ const piece = board.get(move.from)
430
+ return piece && piece.color === attackingColor &&
431
+ Math.abs(move.to.charCodeAt(0) - square.charCodeAt(0)) <= 1 &&
432
+ Math.abs(parseInt(move.to[1]) - parseInt(square[1])) <= 1
433
+ })
434
+ if (hasAttack) attacks++
435
+ } catch {
436
+ // ignore invalid moves
437
+ }
438
+ }
439
+ }
440
+
441
+ return attacks
442
+ }
443
+ }
src/hooks/useChessGame.ts ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useRef, useEffect } from 'react'
2
+ import { Chess, Square, Move, Color } from 'chess.js'
3
+ import { GameState, GameHistoryEntry, DraggedPiece } from '../types/chess'
4
+ import { evaluateGameState, isPotentialPromotion } from '../utils/chessUtils'
5
+ import { ChessAI } from '../engines/ChessAI'
6
+
7
+ export function useChessGame() {
8
+ const [gameState, setGameState] = useState<GameState>({
9
+ board: new Chess(),
10
+ gameActive: false,
11
+ playerColor: 'w',
12
+ selectedSquare: null,
13
+ legalMoves: [],
14
+ gameHistory: [],
15
+ gameOver: false,
16
+ gameResult: null,
17
+ promotionMove: null,
18
+ promotionDialogActive: false,
19
+ aiThinking: false,
20
+ aiModelLoaded: false,
21
+ aiModelLoading: false
22
+ })
23
+ const [selectedModel, setSelectedModel] = useState<string>('mlabonne/chesspythia-70m')
24
+
25
+ const [draggedPiece, setDraggedPiece] = useState<DraggedPiece | null>(null)
26
+ const aiTimeoutRef = useRef<NodeJS.Timeout>()
27
+ const chessAI = useRef<ChessAI | null>(null)
28
+
29
+ // init chess model
30
+ useEffect(() => {
31
+ const initializeAI = async () => {
32
+ setGameState(prev => ({ ...prev, aiModelLoading: true }))
33
+
34
+ chessAI.current = new ChessAI(selectedModel)
35
+
36
+ try {
37
+ await chessAI.current.initialize()
38
+ setGameState(prev => ({
39
+ ...prev,
40
+ aiModelLoaded: true,
41
+ aiModelLoading: false
42
+ }))
43
+ console.log(`Chess AI model ${selectedModel} loaded successfully`)
44
+ } catch (error) {
45
+ console.error(`Failed to load Chess AI model ${selectedModel}:`, error)
46
+ setGameState(prev => ({
47
+ ...prev,
48
+ aiModelLoaded: false,
49
+ aiModelLoading: false
50
+ }))
51
+ }
52
+ }
53
+
54
+ initializeAI()
55
+
56
+ return () => {
57
+ if (aiTimeoutRef.current) {
58
+ clearTimeout(aiTimeoutRef.current)
59
+ }
60
+ }
61
+ }, [selectedModel])
62
+
63
+ const startNewGame = useCallback(() => {
64
+ clearTimeout(aiTimeoutRef.current)
65
+
66
+ setGameState(prev => ({
67
+ ...prev,
68
+ board: new Chess(),
69
+ gameActive: true,
70
+ gameOver: false,
71
+ gameHistory: [],
72
+ selectedSquare: null,
73
+ legalMoves: [],
74
+ gameResult: null,
75
+ promotionDialogActive: false,
76
+ promotionMove: null,
77
+ aiThinking: false
78
+ }))
79
+
80
+ if (gameState.playerColor === 'b') {
81
+ setGameState(prev => ({ ...prev, aiThinking: true }))
82
+ aiTimeoutRef.current = setTimeout(() => {
83
+ makeAIMove()
84
+ }, 500)
85
+ }
86
+ }, [gameState.playerColor])
87
+
88
+ const resignGame = useCallback(() => {
89
+ clearTimeout(aiTimeoutRef.current)
90
+
91
+ const winner = gameState.playerColor === 'w' ? 'b' : 'w'
92
+ const gameResult = {
93
+ isGameOver: true,
94
+ winner: winner as Color,
95
+ message: `Game Over! ${gameState.playerColor === 'w' ? 'White' : 'Black'} resigned - ${winner === 'w' ? 'White' : 'Black'} wins!`,
96
+ terminationReason: 'resignation',
97
+ details: `${gameState.playerColor === 'w' ? 'White' : 'Black'} player resigned the game`
98
+ }
99
+
100
+ setGameState(prev => ({
101
+ ...prev,
102
+ gameActive: false,
103
+ gameOver: true,
104
+ gameResult,
105
+ aiThinking: false
106
+ }))
107
+ }, [gameState.playerColor])
108
+
109
+ const togglePlayerColor = useCallback(() => {
110
+ if (!gameState.gameActive) {
111
+ setGameState(prev => ({
112
+ ...prev,
113
+ playerColor: prev.playerColor === 'w' ? 'b' : 'w'
114
+ }))
115
+ }
116
+ }, [gameState.gameActive])
117
+
118
+ const selectSquare = useCallback((square: Square) => {
119
+ if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor || gameState.aiThinking) {
120
+ return
121
+ }
122
+
123
+ const piece = gameState.board.get(square)
124
+
125
+ if (piece && piece.color === gameState.playerColor) {
126
+ const moves = gameState.board.moves({ square, verbose: true })
127
+ setGameState(prev => ({
128
+ ...prev,
129
+ selectedSquare: square,
130
+ legalMoves: moves
131
+ }))
132
+ }
133
+ }, [gameState])
134
+
135
+ const attemptMove = useCallback((from: Square, to: Square) => {
136
+ console.log('attemptMove called:', from, 'to', to)
137
+
138
+ if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor) {
139
+ console.log('Move rejected: game not active or not player turn')
140
+ return false
141
+ }
142
+
143
+ if (isPotentialPromotion(gameState.board, from, to)) {
144
+ const possibleMoves = gameState.board.moves({ square: from, verbose: true })
145
+ const hasLegalPromotion = possibleMoves.some(move =>
146
+ move.to === to && move.promotion !== undefined
147
+ )
148
+
149
+ if (hasLegalPromotion) {
150
+ setGameState(prev => ({
151
+ ...prev,
152
+ promotionMove: { from, to, promotion: 'q' } as Move,
153
+ promotionDialogActive: true,
154
+ selectedSquare: null,
155
+ legalMoves: []
156
+ }))
157
+ return true
158
+ }
159
+ }
160
+
161
+ try {
162
+ const testBoard = new Chess(gameState.board.fen())
163
+ const move = testBoard.move({ from, to })
164
+
165
+ if (move) {
166
+ const historyEntry: GameHistoryEntry = {
167
+ move: move.san,
168
+ moveData: move,
169
+ player: 'Human',
170
+ timestamp: new Date(),
171
+ capturedPiece: move.captured ? { type: move.captured, color: gameState.board.turn() === 'w' ? 'b' : 'w' } : undefined
172
+ }
173
+
174
+ setGameState(prev => ({
175
+ ...prev,
176
+ board: testBoard,
177
+ gameHistory: [...prev.gameHistory, historyEntry],
178
+ selectedSquare: null,
179
+ legalMoves: []
180
+ }))
181
+
182
+ const gameResult = evaluateGameState(testBoard)
183
+ if (gameResult.isGameOver) {
184
+ setGameState(prev => ({
185
+ ...prev,
186
+ gameActive: false,
187
+ gameOver: true,
188
+ gameResult
189
+ }))
190
+ } else {
191
+ // trigger AI move
192
+ setGameState(prev => ({ ...prev, aiThinking: true }))
193
+ aiTimeoutRef.current = setTimeout(() => {
194
+ makeAIMove()
195
+ }, 1000)
196
+ }
197
+
198
+ return true
199
+ } else {
200
+ console.log('Move returned null/false')
201
+ }
202
+ } catch (error) {
203
+ console.log('Invalid move attempted:', error)
204
+ }
205
+
206
+ return false
207
+ }, [gameState])
208
+
209
+
210
+ const makeAIMove = useCallback(async () => {
211
+ if (!chessAI.current) {
212
+ setGameState(prev => ({ ...prev, aiThinking: false }))
213
+ return
214
+ }
215
+
216
+ try {
217
+ setGameState(prev => {
218
+ const currentBoard = new Chess(prev.board.fen())
219
+
220
+ if (!prev.gameActive || currentBoard.turn() === prev.playerColor) {
221
+ return { ...prev, aiThinking: false }
222
+ }
223
+
224
+ const possibleMoves = currentBoard.moves({ verbose: true })
225
+ if (possibleMoves.length === 0) {
226
+ return { ...prev, aiThinking: false }
227
+ }
228
+
229
+ chessAI.current!.getMove(currentBoard, 10000).then(aiMove => {
230
+
231
+ if (!aiMove) {
232
+ setGameState(prev => ({ ...prev, aiThinking: false }))
233
+ return
234
+ }
235
+
236
+ const moveResult = currentBoard.move(aiMove)
237
+ if (!moveResult) {
238
+ setGameState(prev => ({ ...prev, aiThinking: false }))
239
+ return
240
+ }
241
+
242
+ const historyEntry: GameHistoryEntry = {
243
+ move: moveResult.san,
244
+ moveData: moveResult,
245
+ player: 'AI',
246
+ timestamp: new Date(),
247
+ capturedPiece: moveResult.captured ? { type: moveResult.captured, color: currentBoard.turn() === 'w' ? 'b' : 'w' } : undefined
248
+ }
249
+
250
+ const gameResult = evaluateGameState(currentBoard)
251
+
252
+ setGameState(prev => ({
253
+ ...prev,
254
+ board: currentBoard,
255
+ gameHistory: [...prev.gameHistory, historyEntry],
256
+ aiThinking: false,
257
+ gameActive: !gameResult.isGameOver,
258
+ gameOver: gameResult.isGameOver,
259
+ gameResult: gameResult.isGameOver ? gameResult : null
260
+ }))
261
+ }).catch(error => {
262
+ console.error('Error in AI move generation:', error)
263
+ setGameState(prev => ({ ...prev, aiThinking: false }))
264
+ })
265
+
266
+ return prev
267
+ })
268
+
269
+ } catch (error) {
270
+ console.error('Error in AI move setup:', error)
271
+ setGameState(prev => ({ ...prev, aiThinking: false }))
272
+ }
273
+ }, [])
274
+
275
+ const completePromotion = useCallback((promotionPiece: 'q' | 'r' | 'b' | 'n') => {
276
+ if (!gameState.promotionMove) return
277
+
278
+ try {
279
+ const testBoard = new Chess(gameState.board.fen())
280
+ const move = testBoard.move({
281
+ from: gameState.promotionMove.from,
282
+ to: gameState.promotionMove.to,
283
+ promotion: promotionPiece
284
+ })
285
+
286
+ if (move) {
287
+ const historyEntry: GameHistoryEntry = {
288
+ move: move.san,
289
+ moveData: move,
290
+ player: 'Human',
291
+ timestamp: new Date(),
292
+ capturedPiece: move.captured ? { type: move.captured, color: gameState.board.turn() === 'w' ? 'b' : 'w' } : undefined
293
+ }
294
+
295
+ setGameState(prev => ({
296
+ ...prev,
297
+ board: testBoard,
298
+ gameHistory: [...prev.gameHistory, historyEntry],
299
+ selectedSquare: null,
300
+ legalMoves: [],
301
+ promotionDialogActive: false,
302
+ promotionMove: null
303
+ }))
304
+
305
+ const gameResult = evaluateGameState(testBoard)
306
+ if (gameResult.isGameOver) {
307
+ setGameState(prev => ({
308
+ ...prev,
309
+ gameActive: false,
310
+ gameOver: true,
311
+ gameResult
312
+ }))
313
+ } else {
314
+ setGameState(prev => ({ ...prev, aiThinking: true }))
315
+ aiTimeoutRef.current = setTimeout(() => {
316
+ makeAIMove()
317
+ }, 1000)
318
+ }
319
+ } else {
320
+ setGameState(prev => ({
321
+ ...prev,
322
+ promotionDialogActive: false,
323
+ promotionMove: null
324
+ }))
325
+ }
326
+ } catch (error) {
327
+ console.error('Error during promotion:', error)
328
+ setGameState(prev => ({
329
+ ...prev,
330
+ promotionDialogActive: false,
331
+ promotionMove: null
332
+ }))
333
+ }
334
+ }, [gameState.promotionMove, gameState.board, makeAIMove])
335
+
336
+ const startDrag = useCallback((square: Square) => {
337
+ if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor || gameState.aiThinking) {
338
+ return
339
+ }
340
+
341
+ const piece = gameState.board.get(square)
342
+ if (piece && piece.color === gameState.playerColor) {
343
+ setDraggedPiece({ piece, square })
344
+ const moves = gameState.board.moves({ square, verbose: true })
345
+ setGameState(prev => ({
346
+ ...prev,
347
+ selectedSquare: square,
348
+ legalMoves: moves
349
+ }))
350
+ }
351
+ }, [gameState])
352
+
353
+ const endDrag = useCallback((targetSquare: Square | null) => {
354
+ console.log('Ending drag at:', targetSquare, 'from:', draggedPiece?.square)
355
+
356
+ let moveSuccessful = false
357
+ if (draggedPiece && targetSquare && targetSquare !== draggedPiece.square) {
358
+ moveSuccessful = attemptMove(draggedPiece.square, targetSquare)
359
+ }
360
+ setDraggedPiece(null)
361
+
362
+ if (!moveSuccessful) {
363
+ setGameState(prev => ({
364
+ ...prev,
365
+ selectedSquare: null,
366
+ legalMoves: []
367
+ }))
368
+ }
369
+ }, [draggedPiece, attemptMove])
370
+
371
+ const getAIStatus = useCallback(() => {
372
+ if (!chessAI.current) return 'Not initialized'
373
+ return chessAI.current.getModelInfo()
374
+ }, [])
375
+
376
+ const changeModel = useCallback((modelId: string) => {
377
+ if (gameState.gameActive) return
378
+ setSelectedModel(modelId)
379
+ }, [gameState.gameActive])
380
+
381
+ useEffect(() => {
382
+ return () => {
383
+ if (aiTimeoutRef.current) {
384
+ clearTimeout(aiTimeoutRef.current)
385
+ }
386
+ }
387
+ }, [])
388
+
389
+ return {
390
+ gameState,
391
+ draggedPiece,
392
+ selectedModel,
393
+ startNewGame,
394
+ resignGame,
395
+ togglePlayerColor,
396
+ selectSquare,
397
+ attemptMove,
398
+ completePromotion,
399
+ startDrag,
400
+ endDrag,
401
+ getAIStatus,
402
+ changeModel
403
+ }
404
+ }
src/main.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import { ErrorBoundary } from './ErrorBoundary'
5
+ import './styles/index.css'
6
+
7
+ console.log('Starting Musical Chess React App...')
8
+
9
+ ReactDOM.createRoot(document.getElementById('root')!).render(
10
+ <React.StrictMode>
11
+ <ErrorBoundary>
12
+ <App />
13
+ </ErrorBoundary>
14
+ </React.StrictMode>,
15
+ )
src/styles/App.css ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .app-container {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ background-color: #323232;
5
+ color: white;
6
+ display: flex;
7
+ justify-content: center;
8
+ align-items: flex-start;
9
+ overflow: auto;
10
+ padding-top: 5vh;
11
+ }
12
+
13
+ .main-content {
14
+ display: flex;
15
+ flex-direction: column;
16
+ padding: 20px;
17
+ gap: 20px;
18
+ max-width: 100%;
19
+ max-height: 100%;
20
+ }
21
+
22
+ .header {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ gap: 20px;
27
+ position: relative;
28
+ }
29
+
30
+ .title {
31
+ font-size: 48px;
32
+ font-weight: bold;
33
+ margin: 0;
34
+ text-align: center;
35
+ color: #ffffff;
36
+ }
37
+
38
+ .audio-info-button {
39
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
40
+ border: none;
41
+ border-radius: 50%;
42
+ width: 50px;
43
+ height: 50px;
44
+ font-size: 24px;
45
+ cursor: pointer;
46
+ color: white;
47
+ transition: all 0.2s ease;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
52
+ }
53
+
54
+ .audio-info-button:hover {
55
+ transform: scale(1.1);
56
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
57
+ }
58
+
59
+ .game-area {
60
+ display: flex;
61
+ gap: 50px;
62
+ align-items: flex-start;
63
+ justify-content: center;
64
+ }
65
+
66
+ .board-container {
67
+ position: relative;
68
+ flex-shrink: 0;
69
+ }
70
+
71
+ .sidebar {
72
+ width: 400px;
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 20px;
76
+ padding: 20px;
77
+ background-color: #464646;
78
+ border-radius: 10px;
79
+ height: fit-content;
80
+ }
81
+
82
+ @media (max-width: 1200px) {
83
+ .game-area {
84
+ flex-direction: column;
85
+ align-items: center;
86
+ gap: 30px;
87
+ }
88
+
89
+ .sidebar {
90
+ width: 100%;
91
+ max-width: 600px;
92
+ }
93
+
94
+ .title {
95
+ font-size: 36px;
96
+ text-align: center;
97
+ }
98
+ }
99
+
100
+ @media (max-width: 768px) {
101
+ .main-content {
102
+ padding: 10px;
103
+ }
104
+
105
+ .title {
106
+ font-size: 28px;
107
+ }
108
+
109
+ .board-container {
110
+ transform: scale(0.8);
111
+ transform-origin: top center;
112
+ }
113
+
114
+ .sidebar {
115
+ padding: 15px;
116
+ }
117
+ }
src/styles/AudioInfoPopup.css ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .audio-info-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.75);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 2000;
12
+ }
13
+
14
+ .audio-info-popup {
15
+ background: white;
16
+ border-radius: 12px;
17
+ padding: 0;
18
+ max-width: 600px;
19
+ max-height: 80vh;
20
+ width: 90%;
21
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
22
+ overflow: hidden;
23
+ display: flex;
24
+ flex-direction: column;
25
+ }
26
+
27
+ .popup-header {
28
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
29
+ color: white;
30
+ padding: 20px;
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ }
35
+
36
+ .popup-header h2 {
37
+ margin: 0;
38
+ font-size: 1.5rem;
39
+ font-weight: 600;
40
+ }
41
+
42
+ .close-button {
43
+ background: none;
44
+ border: none;
45
+ color: white;
46
+ font-size: 2rem;
47
+ cursor: pointer;
48
+ padding: 0;
49
+ width: 40px;
50
+ height: 40px;
51
+ border-radius: 50%;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ transition: background-color 0.2s;
56
+ }
57
+
58
+ .close-button:hover {
59
+ background: rgba(255, 255, 255, 0.2);
60
+ }
61
+
62
+ .popup-content {
63
+ padding: 24px;
64
+ overflow-y: auto;
65
+ flex: 1;
66
+ }
67
+
68
+ .popup-content section {
69
+ margin-bottom: 24px;
70
+ }
71
+
72
+ .popup-content section:last-child {
73
+ margin-bottom: 0;
74
+ }
75
+
76
+ .popup-content h3 {
77
+ color: #333;
78
+ font-size: 1.25rem;
79
+ margin: 0 0 12px 0;
80
+ font-weight: 600;
81
+ }
82
+
83
+ .popup-content h4 {
84
+ color: #333;
85
+ font-size: 1.1rem;
86
+ margin: 0 0 8px 0;
87
+ font-weight: 600;
88
+ }
89
+
90
+ .popup-content p {
91
+ color: #555;
92
+ line-height: 1.6;
93
+ margin: 0 0 12px 0;
94
+ }
95
+
96
+ .popup-content ul {
97
+ color: #555;
98
+ line-height: 1.6;
99
+ margin: 0;
100
+ padding-left: 20px;
101
+ }
102
+
103
+ .popup-content li {
104
+ margin-bottom: 8px;
105
+ }
106
+
107
+ .note-mapping {
108
+ background: #f8f9fa;
109
+ border: 1px solid #e9ecef;
110
+ border-radius: 6px;
111
+ padding: 12px;
112
+ margin: 12px 0;
113
+ font-family: 'Courier New', monospace;
114
+ }
115
+
116
+ .note-mapping div {
117
+ margin-bottom: 6px;
118
+ }
119
+
120
+ .note-mapping div:last-child {
121
+ margin-bottom: 0;
122
+ }
123
+
124
+ strong {
125
+ color: #333;
126
+ font-weight: 600;
127
+ }
128
+
129
+ @media (max-width: 768px) {
130
+ .audio-info-popup {
131
+ width: 95%;
132
+ max-height: 90vh;
133
+ }
134
+
135
+ .popup-header {
136
+ padding: 16px;
137
+ }
138
+
139
+ .popup-header h2 {
140
+ font-size: 1.25rem;
141
+ }
142
+
143
+ .popup-content {
144
+ padding: 20px;
145
+ }
146
+ }
src/styles/ChessBoard.css ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .chess-board {
2
+ position: relative;
3
+ user-select: none;
4
+ }
5
+
6
+ .board-container {
7
+ position: relative;
8
+ width: 600px;
9
+ height: 600px;
10
+ }
11
+
12
+ .board-squares {
13
+ position: relative;
14
+ width: 600px;
15
+ height: 600px;
16
+ border: 3px solid #ffffff;
17
+ border-radius: 4px;
18
+ }
19
+
20
+ .board-border {
21
+ position: absolute;
22
+ top: -3px;
23
+ left: -3px;
24
+ right: -3px;
25
+ bottom: -3px;
26
+ border: 3px solid #ffffff;
27
+ border-radius: 4px;
28
+ pointer-events: none;
29
+ }
30
+
31
+ .file-labels {
32
+ position: absolute;
33
+ bottom: -30px;
34
+ left: 0;
35
+ width: 600px;
36
+ height: 25px;
37
+ display: flex;
38
+ }
39
+
40
+ .file-label {
41
+ width: 75px;
42
+ height: 25px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
46
+ justify-content: center;
47
+ color: #ffffff;
48
+ font-weight: bold;
49
+ font-size: 14px;
50
+ }
51
+
52
+ .file-letter {
53
+ font-size: 16px;
54
+ margin-bottom: 2px;
55
+ }
56
+
57
+ .file-note {
58
+ font-size: 11px;
59
+ color: #cccccc;
60
+ font-weight: normal;
61
+ }
62
+
63
+ .rank-labels {
64
+ position: absolute;
65
+ left: -50px;
66
+ top: 0;
67
+ width: 45px;
68
+ height: 600px;
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+
73
+ .rank-label {
74
+ width: 45px;
75
+ height: 75px;
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ justify-content: center;
80
+ color: #ffffff;
81
+ font-weight: bold;
82
+ font-size: 14px;
83
+ }
84
+
85
+ .rank-number {
86
+ font-size: 16px;
87
+ margin-bottom: 2px;
88
+ }
89
+
90
+ .rank-octave {
91
+ font-size: 10px;
92
+ color: #cccccc;
93
+ font-weight: normal;
94
+ }
95
+
96
+ .dragged-piece {
97
+ pointer-events: none;
98
+ z-index: 1000;
99
+ }
src/styles/ChessPiece.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .chess-piece {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ cursor: inherit;
6
+ transition: opacity 0.2s ease;
7
+ }
8
+
9
+ .chess-piece.dragging {
10
+ opacity: 0.5;
11
+ }
12
+
13
+ .piece-image {
14
+ width: 100%;
15
+ height: 100%;
16
+ object-fit: contain;
17
+ pointer-events: none;
18
+ user-select: none;
19
+ -webkit-user-drag: none;
20
+ }
21
+
22
+ .piece-symbol {
23
+ font-family: 'Chess Merida', 'Chess Alpha', serif;
24
+ font-weight: bold;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ pointer-events: none;
29
+ user-select: none;
30
+ }
src/styles/ChessSquare.css ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .chess-square {
2
+ position: absolute;
3
+ cursor: pointer;
4
+ transition: all 0.2s ease;
5
+ border-radius: 2px;
6
+ }
7
+
8
+ .chess-square.light {
9
+ background-color: #f0d9b5;
10
+ }
11
+
12
+ .chess-square.dark {
13
+ background-color: #b58863;
14
+ }
15
+
16
+ .chess-square.selected {
17
+ background-color: #ff6b6b !important;
18
+ box-shadow: inset 0 0 0 3px rgba(255, 107, 107, 0.8);
19
+ }
20
+
21
+ .chess-square.legal-move {
22
+ background-color: rgba(144, 238, 144, 0.6) !important;
23
+ }
24
+
25
+ .chess-square.drop-target {
26
+ background-color: rgba(255, 255, 0, 0.4) !important;
27
+ }
28
+
29
+ .chess-square:hover {
30
+ filter: brightness(1.1);
31
+ }
32
+
33
+ .piece-container {
34
+ width: 100%;
35
+ height: 100%;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ cursor: grab;
40
+ }
41
+
42
+ .piece-container:active {
43
+ cursor: grabbing;
44
+ }
45
+
46
+ .legal-move-indicator {
47
+ position: absolute;
48
+ top: 50%;
49
+ left: 50%;
50
+ transform: translate(-50%, -50%);
51
+ width: 16px;
52
+ height: 16px;
53
+ background-color: rgba(0, 128, 0, 0.8);
54
+ border-radius: 50%;
55
+ pointer-events: none;
56
+ }
57
+
58
+ .chess-square.legal-move .legal-move-indicator {
59
+ background-color: rgba(0, 80, 0, 0.9);
60
+ }
src/styles/GameControls.css ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .game-controls {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 20px;
5
+ }
6
+
7
+ .game-status {
8
+ font-size: 20px;
9
+ font-weight: bold;
10
+ color: #ffffff;
11
+ text-align: center;
12
+ min-height: 30px;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ }
17
+
18
+ .control-buttons {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 15px;
22
+ }
23
+
24
+ .main-button {
25
+ background-color: #646464;
26
+ }
27
+
28
+ .color-button {
29
+ background-color: #646464;
30
+ }
31
+
32
+ .audio-button {
33
+ background-color: #646464;
34
+ }
35
+
36
+ .volume-controls {
37
+ margin-top: 20px;
38
+ padding: 20px;
39
+ background-color: rgba(255, 255, 255, 0.05);
40
+ border-radius: 8px;
41
+ border: 1px solid rgba(255, 255, 255, 0.1);
42
+ }
43
+
44
+ .volume-controls-title {
45
+ margin: 0 0 16px 0;
46
+ font-size: 16px;
47
+ font-weight: 600;
48
+ color: #ffffff;
49
+ text-align: center;
50
+ }
51
+
52
+ .model-selector {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 8px;
56
+ padding: 12px;
57
+ background-color: rgba(255, 255, 255, 0.05);
58
+ border-radius: 8px;
59
+ border: 1px solid rgba(255, 255, 255, 0.1);
60
+ }
61
+
62
+ .model-selector label {
63
+ font-size: 14px;
64
+ font-weight: 600;
65
+ color: #ffffff;
66
+ }
67
+
68
+ .model-dropdown {
69
+ padding: 8px 12px;
70
+ border: 1px solid rgba(255, 255, 255, 0.2);
71
+ border-radius: 6px;
72
+ background-color: rgba(255, 255, 255, 0.1);
73
+ color: #ffffff;
74
+ font-size: 14px;
75
+ cursor: pointer;
76
+ transition: all 0.2s ease;
77
+ }
78
+
79
+ .model-dropdown:hover:not(:disabled) {
80
+ background-color: rgba(255, 255, 255, 0.15);
81
+ border-color: rgba(255, 255, 255, 0.3);
82
+ }
83
+
84
+ .model-dropdown:disabled {
85
+ opacity: 0.5;
86
+ cursor: not-allowed;
87
+ }
88
+
89
+ .model-dropdown option {
90
+ background-color: #2a2a2a;
91
+ color: #ffffff;
92
+ }