Skip to content

Commit 718b7b7

Browse files
committed
fix: remap Go backend to port 8083, treat JS as local engine, fix AI move animation
- Docker: fix rust healthcheck CMD, remove js-backend service, remap go-backend 8083:8080 - JS backend now uses LocalProvider (in-browser @rumenx/chess, no server) - AI moves use makeMove() directly instead of selectSquare() — eliminates legal move dots flash - Add Makefile with docker and dev shortcuts - Update .env.example and types for new port mappings
1 parent 7d942c0 commit 718b7b7

File tree

10 files changed

+151
-93
lines changed

10 files changed

+151
-93
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ VITE_CHESS_BACKEND=local
33

44
# Backend URLs (only used for remote backends)
55
# VITE_CHESS_RUST_URL=http://localhost:8082
6-
# VITE_CHESS_GO_URL=http://localhost:8080
7-
# VITE_CHESS_JS_URL=http://localhost:8081
6+
# VITE_CHESS_GO_URL=http://localhost:8083
7+
# JS engine uses in-browser @rumenx/chess (no server needed)

Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# react-chess — convenience shortcuts
2+
# Usage: make <target>
3+
4+
.PHONY: help init start stop restart rebuild logs status clean dev test lint build
5+
6+
help: ## Show this help
7+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
8+
9+
# ── Docker ────────────────────────────────────────────────
10+
11+
init: ## Build images and start all containers
12+
docker compose up --build -d
13+
14+
start: ## Start containers (no rebuild)
15+
docker compose up -d
16+
17+
stop: ## Stop and remove containers
18+
docker compose down
19+
20+
restart: ## Restart containers (no rebuild)
21+
docker compose restart
22+
23+
rebuild: ## Force rebuild images and recreate containers
24+
docker compose up --build --force-recreate -d
25+
26+
logs: ## Tail container logs (Ctrl-C to stop)
27+
docker compose logs -f
28+
29+
status: ## Show running containers
30+
docker compose ps
31+
32+
clean: ## Stop containers, remove images and volumes
33+
docker compose down --rmi local -v
34+
35+
# ── Local Development ─────────────────────────────────────
36+
37+
dev: ## Start Vite dev server (HMR)
38+
npm run dev
39+
40+
test: ## Run Jest test suite
41+
npm test
42+
43+
lint: ## ESLint + type-check
44+
npm run lint && npm run type-check
45+
46+
build: ## Production build
47+
npm run build

docker-compose.yml

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ services:
1919
environment:
2020
- VITE_CHESS_BACKEND=local
2121
- VITE_CHESS_RUST_URL=http://localhost:8082
22-
- VITE_CHESS_GO_URL=http://localhost:8080
22+
- VITE_CHESS_GO_URL=http://localhost:8083
2323
- VITE_CHESS_JS_URL=http://localhost:8081
2424
depends_on:
2525
rust-backend:
@@ -28,9 +28,6 @@ services:
2828
go-backend:
2929
condition: service_healthy
3030
required: false
31-
js-backend:
32-
condition: service_healthy
33-
required: false
3431
restart: unless-stopped
3532

3633
# ── Rust Backend (rust-chess) ───────────────────────────
@@ -44,7 +41,7 @@ services:
4441
- PORT=8082
4542
- RUST_LOG=info
4643
healthcheck:
47-
test: ["/rust-chess", "--health-check"]
44+
test: ["CMD", "/rust-chess", "--health-check"]
4845
interval: 15s
4946
timeout: 3s
5047
start_period: 5s
@@ -57,7 +54,7 @@ services:
5754
context: ../go-chess
5855
dockerfile: Dockerfile
5956
ports:
60-
- "8080:8080"
57+
- "8083:8080"
6158
environment:
6259
- CHESS_HOST=0.0.0.0
6360
- CHESS_PORT=8080
@@ -69,19 +66,5 @@ services:
6966
retries: 3
7067
restart: unless-stopped
7168

72-
# ── JS Backend (npm-chess) ─────────────────────────────
73-
js-backend:
74-
build:
75-
context: ../npm-chess
76-
dockerfile: Dockerfile
77-
ports:
78-
- "8081:8081"
79-
environment:
80-
- PORT=8081
81-
healthcheck:
82-
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8081/api/v1/health"]
83-
interval: 15s
84-
timeout: 3s
85-
start_period: 5s
86-
retries: 3
87-
restart: unless-stopped
69+
# NOTE: js-backend (npm-chess) is a library, not a server —
70+
# no Dockerfile available. Run it separately if needed.

src/App.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,8 @@ function App() {
176176
if (cancelled) return;
177177
const move = ChessAI.computeBestMove(engine, aiDifficulty);
178178
if (move) {
179-
selectSquare(move.from);
180-
setTimeout(() => {
181-
if (!cancelled) selectSquare(move.to);
182-
setTimeout(() => {
183-
if (!cancelled) setAiThinking(false);
184-
}, 0);
185-
}, 80);
179+
makeMove(move.from, move.to);
180+
if (!cancelled) setAiThinking(false);
186181
} else {
187182
setAiThinking(false);
188183
}
@@ -199,7 +194,7 @@ function App() {
199194
isTimeout,
200195
playerColor,
201196
engine,
202-
selectSquare,
197+
makeMove,
203198
aiDifficulty,
204199
history.length,
205200
]);
@@ -287,12 +282,11 @@ function App() {
287282
<header className="app__header">
288283
<h1 className="app__title">React Chess</h1>
289284
<p className="app__subtitle">
290-
Backend:{' '}
291-
<strong>{backends[backendId].label}</strong>
285+
Backend: <strong>{backends[backendId].label}</strong>
292286
{backendId !== 'local' && (
293287
<span
294288
className={`app__connection-dot ${connected ? 'app__connection-dot--ok' : connected === false ? 'app__connection-dot--err' : 'app__connection-dot--pending'}`}
295-
title={connected ? 'Connected' : connectionError ?? 'Checking…'}
289+
title={connected ? 'Connected' : (connectionError ?? 'Checking…')}
296290
/>
297291
)}
298292
</p>
@@ -325,7 +319,7 @@ function App() {
325319
))}
326320
</select>
327321
</div>
328-
{backendId !== 'local' && (
322+
{backendId !== 'local' && backendId !== 'js' && (
329323
<>
330324
<div className="board-settings__option">
331325
<label className="board-settings__label" htmlFor="backend-url">
@@ -366,9 +360,7 @@ function App() {
366360
</button>
367361
</div>
368362
{!capabilities.undo && (
369-
<div className="board-settings__note">
370-
⚠️ This backend does not support undo
371-
</div>
363+
<div className="board-settings__note">⚠️ This backend does not support undo</div>
372364
)}
373365
</>
374366
)}
@@ -594,9 +586,7 @@ function App() {
594586
</div>
595587
)}
596588
{gameError && (
597-
<div className="board-info__banner board-info__banner--error">
598-
⚠️ {gameError}
599-
</div>
589+
<div className="board-info__banner board-info__banner--error">⚠️ {gameError}</div>
600590
)}
601591
<div className="board-info__row">
602592
<span className="board-info__status">{statusMessage}</span>

src/hooks/useChessBackendGame.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
138138
setCurrentPly(state.moveCount);
139139
setRemoteGameId(state.id);
140140
},
141-
[engine, updateGameState],
141+
[engine, updateGameState]
142142
);
143143

144144
// Auto-create remote game on mount / backend switch
@@ -220,7 +220,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
220220
isRemote,
221221
remoteGameId,
222222
provider,
223-
],
223+
]
224224
);
225225

226226
// -----------------------------------------------------------------------
@@ -264,7 +264,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
264264
makeMove(selectedSquare, square);
265265
}
266266
},
267-
[engine, turn, selectedSquare, makeMove],
267+
[engine, turn, selectedSquare, makeMove]
268268
);
269269

270270
// -----------------------------------------------------------------------
@@ -309,7 +309,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
309309
.finally(() => setLoading(false));
310310
}
311311
},
312-
[engine, updateGameState, timeControl.initialMs, isRemote, provider, applyRemoteState],
312+
[engine, updateGameState, timeControl.initialMs, isRemote, provider, applyRemoteState]
313313
);
314314

315315
// -----------------------------------------------------------------------
@@ -330,7 +330,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
330330
});
331331
}
332332
},
333-
[engine, updateGameState, isRemote, remoteGameId, provider],
333+
[engine, updateGameState, isRemote, remoteGameId, provider]
334334
);
335335

336336
const getFEN = useCallback(() => engine.getFEN(), [engine]);
@@ -341,7 +341,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
341341

342342
const isValidMoveTarget = useCallback(
343343
(square: string): boolean => legalMoves.some((m) => m.to === square),
344-
[legalMoves],
344+
[legalMoves]
345345
);
346346

347347
const getAllLegalMoves = useCallback(() => engine.getLegalMoves(), [engine]);
@@ -360,7 +360,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
360360
setLegalMoves([]);
361361
setCurrentPly(plyIndex);
362362
},
363-
[engine, history, updateGameState],
363+
[engine, history, updateGameState]
364364
);
365365

366366
// -----------------------------------------------------------------------
@@ -377,13 +377,13 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
377377
status === 'insufficient_material' ||
378378
status === 'threefold_repetition' ||
379379
status === 'fifty_move_rule',
380-
[status],
380+
[status]
381381
);
382382

383383
const result = useMemo(() => engine.getResult(), [engine, status]);
384384
const lastMove = useMemo(
385385
() => (history.length > 0 ? history[history.length - 1] : null),
386-
[history],
386+
[history]
387387
);
388388
const canUndo = useMemo(() => history.length > 0, [history]);
389389

@@ -442,7 +442,7 @@ export function useChessBackendGame(initialFEN?: string): BackendGameHook {
442442
setTimeoutWinner(null);
443443
setLastTick(null);
444444
},
445-
[],
445+
[]
446446
);
447447

448448
return {

src/providers/BackendContext.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function envUrls(): Partial<Record<BackendId, string>> {
8585
export function BackendProvider({ children }: { children: ReactNode }) {
8686
// Persisted backend choice (falls back to env var, then 'local')
8787
const [backendId, setBackendId] = useState<BackendId>(() =>
88-
loadJSON<BackendId>(STORAGE_KEY_BACKEND, envBackend()),
88+
loadJSON<BackendId>(STORAGE_KEY_BACKEND, envBackend())
8989
);
9090

9191
// Persisted URL overrides per backend (seeded from env vars)
@@ -96,7 +96,9 @@ export function BackendProvider({ children }: { children: ReactNode }) {
9696
});
9797

9898
// Connection state
99-
const [connected, setConnected] = useState<boolean | null>(backendId === 'local' ? true : null);
99+
const [connected, setConnected] = useState<boolean | null>(
100+
backendId === 'local' || backendId === 'js' ? true : null
101+
);
100102
const [connectionError, setConnectionError] = useState<string | null>(null);
101103
const [checking, setChecking] = useState(false);
102104

@@ -113,15 +115,15 @@ export function BackendProvider({ children }: { children: ReactNode }) {
113115

114116
// Build provider (recreated when backend or URL changes)
115117
const provider = useMemo<ChessProvider>(() => {
116-
if (backendId === 'local') return new LocalProvider();
118+
if (backendId === 'local' || backendId === 'js') return new LocalProvider();
117119
const cfg = backends[backendId];
118-
const url = cfg.url ?? BACKEND_PRESETS[backendId].url ?? 'http://localhost:8080';
120+
const url = cfg.url ?? BACKEND_PRESETS[backendId].url ?? 'http://localhost:8083';
119121
return new RemoteProvider(backendId, url);
120122
}, [backendId, backends]);
121123

122124
// Health check for remote backends
123125
const checkConnection = useCallback(async () => {
124-
if (backendId === 'local') {
126+
if (backendId === 'local' || backendId === 'js') {
125127
setConnected(true);
126128
setConnectionError(null);
127129
return;
@@ -140,9 +142,7 @@ export function BackendProvider({ children }: { children: ReactNode }) {
140142
}
141143
} catch (err) {
142144
setConnected(false);
143-
setConnectionError(
144-
err instanceof Error ? err.message : 'Cannot reach backend',
145-
);
145+
setConnectionError(err instanceof Error ? err.message : 'Cannot reach backend');
146146
} finally {
147147
setChecking(false);
148148
}
@@ -163,17 +163,14 @@ export function BackendProvider({ children }: { children: ReactNode }) {
163163
saveJSON(STORAGE_KEY_URLS, urlOverrides);
164164
}, [urlOverrides]);
165165

166-
const switchBackend = useCallback(
167-
(id: BackendId, customUrl?: string) => {
168-
if (customUrl && id !== 'local') {
169-
setUrlOverrides((prev) => ({ ...prev, [id]: customUrl }));
170-
}
171-
setConnected(id === 'local' ? true : null);
172-
setConnectionError(null);
173-
setBackendId(id);
174-
},
175-
[],
176-
);
166+
const switchBackend = useCallback((id: BackendId, customUrl?: string) => {
167+
if (customUrl && id !== 'local') {
168+
setUrlOverrides((prev) => ({ ...prev, [id]: customUrl }));
169+
}
170+
setConnected(id === 'local' ? true : null);
171+
setConnectionError(null);
172+
setBackendId(id);
173+
}, []);
177174

178175
const setBackendUrl = useCallback((id: BackendId, url: string) => {
179176
setUrlOverrides((prev) => ({ ...prev, [id]: url }));
@@ -203,7 +200,7 @@ export function BackendProvider({ children }: { children: ReactNode }) {
203200
setBackendUrl,
204201
checkConnection,
205202
checking,
206-
],
203+
]
207204
);
208205

209206
return <BackendContext.Provider value={value}>{children}</BackendContext.Provider>;

src/providers/RemoteProvider.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ async function request<T>(url: string, init?: RequestInit): Promise<T> {
9393
if (!res.ok) {
9494
// Normalise error extraction across backends
9595
const errObj = body?.error;
96-
const code = typeof errObj === 'object' ? errObj?.code : errObj ?? 'UNKNOWN';
97-
const msg = typeof errObj === 'object' ? errObj?.message : body?.message ?? res.statusText;
96+
const code = typeof errObj === 'object' ? errObj?.code : (errObj ?? 'UNKNOWN');
97+
const msg = typeof errObj === 'object' ? errObj?.message : (body?.message ?? res.statusText);
9898
throw new ApiError(res.status, String(code), String(msg));
9999
}
100100
return body as T;
@@ -159,7 +159,7 @@ export class RemoteProvider implements ChessProvider {
159159
gameId: string,
160160
from: string,
161161
to: string,
162-
promotion?: string,
162+
promotion?: string
163163
): Promise<NormGameState> {
164164
const body: Record<string, unknown> = { from, to };
165165
if (promotion) body.promotion = promotion;
@@ -183,7 +183,7 @@ export class RemoteProvider implements ChessProvider {
183183
async getLegalMoves(gameId: string, fromSquare?: string): Promise<NormLegalMove[]> {
184184
const qs = fromSquare ? `?from=${fromSquare}` : '';
185185
const raw = await request<Record<string, unknown>>(
186-
this.url(`/games/${gameId}/legal-moves${qs}`),
186+
this.url(`/games/${gameId}/legal-moves${qs}`)
187187
);
188188
return this.adapter.normLegalMoves(raw);
189189
}
@@ -230,9 +230,7 @@ export class RemoteProvider implements ChessProvider {
230230

231231
async getAnalysis(gameId: string, depth?: number): Promise<NormAnalysis> {
232232
const qs = depth ? `?depth=${depth}` : '';
233-
const raw = await request<Record<string, unknown>>(
234-
this.url(`/games/${gameId}/analysis${qs}`),
235-
);
233+
const raw = await request<Record<string, unknown>>(this.url(`/games/${gameId}/analysis${qs}`));
236234
return this.adapter.normAnalysis(raw);
237235
}
238236

0 commit comments

Comments
 (0)