Skip to content

Commit 1e5bca0

Browse files
committed
Add portal check feedback and board-rotation toggle
1 parent a67bc4c commit 1e5bca0

6 files changed

Lines changed: 34 additions & 4 deletions

File tree

src/components/Board.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export function Board({ flipped = false }: Props) {
6868
const wPortals = state.portals?.w ?? [];
6969
const bPortals = state.portals?.b ?? [];
7070
const isTeleportMove = !!lastMove?.isPortalEntry;
71-
const rotateBlackForFixedBoard = mode.kind === "two-player" && !store.settings.autoFlip;
71+
const rotateBlackForFixedBoard =
72+
mode.kind === "two-player" &&
73+
!store.settings.autoFlip &&
74+
store.settings.rotateBlackPiecesFixedBoard;
7275

7376
return (
7477
<>

src/components/Controls.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useGame } from "../GameContext";
2+
import { inCheck } from "../engine/rules";
23

34
export function Controls() {
45
const {
@@ -14,11 +15,14 @@ export function Controls() {
1415
togglePause,
1516
replayLastMove
1617
} = useGame();
18+
const checkNow = result.kind === "ongoing" && inCheck(state, state.turn);
1719
const statusText =
1820
result.kind === "ongoing"
1921
? paused
2022
? "⏸ Paused"
21-
: `${state.turn === "w" ? "White" : "Black"} to move`
23+
: checkNow
24+
? `Check — ${state.turn === "w" ? "White" : "Black"} to move`
25+
: `${state.turn === "w" ? "White" : "Black"} to move`
2226
: result.kind === "checkmate"
2327
? `Checkmate — ${result.winner === "w" ? "White" : "Black"} wins`
2428
: result.kind === "stalemate"

src/engine/__tests__/portal.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from "vitest";
22
import { GameState, initialState, parseFEN, parseSquare, positionKey, sqEq } from "../board";
3-
import { allLegalMoves, legalMovesFrom, makeMove, teleportTargets } from "../rules";
3+
import { allLegalMoves, inCheck, legalMovesFrom, makeMove, teleportTargets } from "../rules";
44

55
/** Wrap a state into Portal Chess mode with given creator type. */
66
function asPortal(s: GameState, creator: "Q" | "R" | "B" | "N" | "K" = "Q"): GameState {
@@ -136,6 +136,18 @@ describe("Portal Chess: teleport entry", () => {
136136
expect(moves.length).toBe(1);
137137
});
138138

139+
it("Teleport move can immediately give check", () => {
140+
const s = asPortal(parseFEN("4k3/4p3/8/8/8/8/8/K7 w - - 0 1"));
141+
s.board[3][6] = { type: "N", color: "w" }; // Ng4 on its own portal
142+
s.portals = { w: [parseSquare("g4")], b: [], max: 1 };
143+
const move = legalMovesFrom(s, parseSquare("g4"))
144+
.find((m) => m.isPortalEntry && sqEq(m.to, parseSquare("f6")));
145+
expect(move).toBeDefined();
146+
147+
const ns = makeMove(s, move!);
148+
expect(inCheck(ns, "b")).toBe(true);
149+
});
150+
139151
it("With adjacency rule OFF (default), targets next to other pieces are allowed", () => {
140152
const s = asPortal(parseFEN("8/8/8/8/8/1P6/8/8 w - - 0 1"));
141153
s.board[2][5] = { type: "N", color: "w" }; // Nf3 on its own portal

src/engine/storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface Settings {
3131
activeProfileId: string | null;
3232
timerSeconds: number; // 0 = off
3333
animationSpeed: AnimationSpeed;
34+
rotateBlackPiecesFixedBoard: boolean;
3435
theme: BoardTheme;
3536
pieceSet: PieceSet;
3637
sound: boolean;
@@ -59,6 +60,7 @@ const DEFAULT_SETTINGS: Settings = {
5960
activeProfileId: null,
6061
timerSeconds: 30,
6162
animationSpeed: "slow",
63+
rotateBlackPiecesFixedBoard: false,
6264
theme: "wood",
6365
pieceSet: "modern",
6466
sound: true,

src/screens/NewGameScreen.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,15 @@ export function NewGameScreen() {
213213
/>
214214
{" "}Auto-turn board after each move
215215
</label>
216-
<p className="hint">Turn this off to keep the board fixed from White's side.</p>
216+
<label>
217+
<input
218+
type="checkbox"
219+
checked={store.settings.rotateBlackPiecesFixedBoard}
220+
onChange={(e) => updateSetting("rotateBlackPiecesFixedBoard", e.target.checked)}
221+
/>
222+
{" "}Rotate black pieces 180° when board is fixed
223+
</label>
224+
<p className="hint">Turn off auto-turn to keep the board fixed from White's side. Optional: rotate black pieces for over-the-board seating.</p>
217225
</section>
218226
)}
219227

src/screens/SettingsScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export function SettingsScreen() {
7575
<label><input type="checkbox" checked={s.sound} onChange={(e) => updateSetting("sound", e.target.checked)} /> Sound effects</label>
7676
<label><input type="checkbox" checked={s.haptics} onChange={(e) => updateSetting("haptics", e.target.checked)} /> Haptics (iPhone)</label>
7777
<label><input type="checkbox" checked={s.autoFlip} onChange={(e) => updateSetting("autoFlip", e.target.checked)} /> Auto-flip board in 2-player mode</label>
78+
<label><input type="checkbox" checked={s.rotateBlackPiecesFixedBoard} onChange={(e) => updateSetting("rotateBlackPiecesFixedBoard", e.target.checked)} /> Rotate black pieces 180° on fixed 2-player board</label>
7879
<label><input type="checkbox" checked={s.showThreats} onChange={(e) => updateSetting("showThreats", e.target.checked)} /> Show threatened pieces (learn mode)</label>
7980
<label><input type="checkbox" checked={s.explodeOnCapture} onChange={(e) => updateSetting("explodeOnCapture", e.target.checked)} /> 💥 Explode pieces on capture</label>
8081
</section>

0 commit comments

Comments
 (0)