@@ -6,35 +6,11 @@ import {
66 PUZZLES , filterPuzzles , puzzleDifficulty ,
77 type Puzzle , type Difficulty
88} from "../puzzles/puzzles" ;
9- import {
10- PORTAL_PUZZLES , filterPortalPuzzles ,
11- type PortalPuzzle
12- } from "../puzzles/portal-puzzles" ;
13- import { parseUci , parseSquare , type Square } from "../engine/board" ;
9+ import { parseUci } from "../engine/board" ;
1410import { gameResult } from "../engine/rules" ;
1511
1612type Status = "solving" | "wrong" | "solved" ;
1713type MateFilter = "all" | 1 | 2 | 3 ;
18- type Mode = "standard" | "portal" ;
19-
20- /** Parse extended UCI: "e2e4" or "e2e4@d8" (portal teleport). */
21- function parsePortalUci ( uci : string ) : {
22- from : Square ; to : Square ; portalTo ?: Square ;
23- promotion ?: "Q" | "R" | "B" | "N" ;
24- } {
25- const atIdx = uci . indexOf ( "@" ) ;
26- if ( atIdx !== - 1 ) {
27- const base = uci . slice ( 0 , atIdx ) ;
28- const land = uci . slice ( atIdx + 1 ) ;
29- return {
30- from : parseSquare ( base . slice ( 0 , 2 ) ) ,
31- to : parseSquare ( base . slice ( 2 , 4 ) ) ,
32- portalTo : parseSquare ( land . slice ( 0 , 2 ) ) ,
33- } ;
34- }
35- const u = parseUci ( uci ) ;
36- return { from : u . from , to : u . to , promotion : u . promotion as "Q" | "R" | "B" | "N" | undefined } ;
37- }
3814
3915const DIFF_LABEL : Record < Difficulty | "all" , string > = {
4016 all : "All" ,
@@ -46,10 +22,8 @@ const DIFF_LABEL: Record<Difficulty | "all", string> = {
4622
4723export function PuzzlesScreen ( ) {
4824 const { loadPosition, state, tryMove, activeProfile, recordPuzzleSolved, recordPuzzleAttempt } = useGame ( ) ;
49- const [ mode , setMode ] = useState < Mode > ( "standard" ) ;
5025 const [ difficulty , setDifficulty ] = useState < Difficulty | "all" > ( "beginner" ) ;
5126 const [ mateIn , setMateIn ] = useState < MateFilter > ( "all" ) ;
52- const [ portalMateIn , setPortalMateIn ] = useState < 1 | 2 | "all" > ( "all" ) ;
5327 const [ newOnly , setNewOnly ] = useState ( true ) ;
5428 const [ index , setIndex ] = useState ( 0 ) ;
5529 const [ status , setStatus ] = useState < Status > ( "solving" ) ;
@@ -73,20 +47,13 @@ export function PuzzlesScreen() {
7347 return unsolved . length > 0 ? unsolved : basePool ;
7448 } , [ basePool , newOnly , solvedIds ] ) ;
7549
76- const basePortalPool = useMemo (
77- ( ) => filterPortalPuzzles ( { mateIn : portalMateIn } ) ,
78- [ portalMateIn ]
79- ) ;
80- const portalPool = useMemo ( ( ) => {
81- if ( ! newOnly ) return basePortalPool ;
82- const unsolved = basePortalPool . filter ( ( p ) => ! solvedIds . has ( p . id ) ) ;
83- return unsolved . length > 0 ? unsolved : basePortalPool ;
84- } , [ basePortalPool , newOnly , solvedIds ] ) ;
85-
86- const pool : ( Puzzle | PortalPuzzle ) [ ] = mode === "portal" ? portalPool : stdPool ;
50+ const pool : Puzzle [ ] = stdPool ;
8751 const puzzle = pool [ index ] ;
8852 const sideToMate = useMemo ( ( ) => ( puzzle ? puzzle . setup ( ) . turn : "w" ) , [ puzzle ?. id ] ) ;
89- const totalSolved = activeProfile ?. stats . puzzlesSolved ?? 0 ;
53+ const totalSolved = useMemo (
54+ ( ) => PUZZLES . reduce ( ( n , p ) => n + ( solvedIds . has ( p . id ) ? 1 : 0 ) , 0 ) ,
55+ [ solvedIds ]
56+ ) ;
9057
9158 const diffCounts = useMemo ( ( ) => {
9259 const out : Record < Difficulty | "all" , { solved : number ; total : number } > = {
@@ -118,7 +85,7 @@ export function PuzzlesScreen() {
11885 return out ;
11986 } , [ solvedIds , difficulty ] ) ;
12087
121- useEffect ( ( ) => { setIndex ( 0 ) ; } , [ mateIn , difficulty , newOnly , mode , portalMateIn ] ) ;
88+ useEffect ( ( ) => { setIndex ( 0 ) ; } , [ mateIn , difficulty , newOnly ] ) ;
12289
12390 useEffect ( ( ) => {
12491 if ( ! puzzle ) return ;
@@ -139,20 +106,11 @@ export function PuzzlesScreen() {
139106 const plyIndex = moved - 1 ;
140107 const expected = puzzle . moves [ plyIndex ] ;
141108 const last = state . history [ state . history . length - 1 ] ;
142- const exp = parsePortalUci ( expected ) ;
109+ const exp = parseUci ( expected ) ;
143110 let ok =
144111 last . from . file === exp . from . file && last . from . rank === exp . from . rank &&
145112 last . to . file === exp . to . file && last . to . rank === exp . to . rank &&
146113 ( ! exp . promotion || last . promotion === exp . promotion ) ;
147- if ( ok && exp . portalTo ) {
148- ok = ! ! last . isPortalEntry &&
149- ! ! last . portalTo &&
150- last . portalTo . file === exp . portalTo . file &&
151- last . portalTo . rank === exp . portalTo . rank ;
152- } else if ( ok && ! exp . portalTo && last . isPortalEntry ) {
153- // Expected a non-portal move, but the user made a portal move.
154- ok = false ;
155- }
156114
157115 // Accept alternative winning moves: if the user has just checkmated,
158116 // count the puzzle as solved even when it differs from the scripted line.
@@ -184,9 +142,9 @@ export function PuzzlesScreen() {
184142 }
185143
186144 setPlayedPlies ( moved ) ;
187- const reply = parsePortalUci ( puzzle . moves [ nextIdx ] ) ;
145+ const reply = parseUci ( puzzle . moves [ nextIdx ] ) ;
188146 const t = setTimeout ( ( ) => {
189- tryMove ( reply . from , reply . to , reply . promotion , reply . portalTo ) ;
147+ tryMove ( reply . from , reply . to , reply . promotion as "Q" | "R" | "B" | "N" | undefined ) ;
190148 setPlayedPlies ( ( x ) => x + 1 ) ;
191149 } , 300 ) ;
192150 return ( ) => clearTimeout ( t ) ;
@@ -215,11 +173,9 @@ export function PuzzlesScreen() {
215173 return < div className = "puzzle-banner" > You play { side } . Mate in { puzzle . mateIn ( ) } — find the forced win.{ done } </ div > ;
216174 } , [ status , puzzle , alreadySolved , sideToMate ] ) ;
217175
218- const allSolvedHere = mode === "portal"
219- ? basePortalPool . length > 0 && basePortalPool . every ( ( p ) => solvedIds . has ( p . id ) )
220- : basePool . length > 0 && basePool . every ( ( p ) => solvedIds . has ( p . id ) ) ;
176+ const allSolvedHere = basePool . length > 0 && basePool . every ( ( p ) => solvedIds . has ( p . id ) ) ;
221177
222- const totalCount = mode === "portal" ? PORTAL_PUZZLES . length : PUZZLES . length ;
178+ const totalCount = PUZZLES . length ;
223179
224180 return (
225181 < div className = "screen" >
@@ -230,78 +186,39 @@ export function PuzzlesScreen() {
230186 < h2 > 🧩 Puzzles</ h2 >
231187
232188 < div className = "puzzle-tabs" >
233- < span className = "tabs-label" > Mode</ span >
234- < button
235- className = { mode === "standard" ? "pill active" : "pill" }
236- onClick = { ( ) => setMode ( "standard" ) }
237- > Standard</ button >
238- < button
239- className = { mode === "portal" ? "pill active" : "pill" }
240- onClick = { ( ) => setMode ( "portal" ) }
241- > 🌀 Portal</ button >
189+ < span className = "tabs-label" > Level</ span >
190+ { ( [ "beginner" , "easy" , "medium" , "hard" , "all" ] as const ) . map ( ( d ) => {
191+ const c = diffCounts [ d ] ;
192+ return (
193+ < button key = { d }
194+ className = { difficulty === d ? "pill active" : "pill" }
195+ onClick = { ( ) => setDifficulty ( d ) }
196+ > { DIFF_LABEL [ d ] } < span className = "count" > { c . solved } /{ c . total } </ span > </ button >
197+ ) ;
198+ } ) }
242199 </ div >
243200
244- { mode === "standard" && (
245- < div className = "puzzle-tabs" >
246- < span className = "tabs-label" > Level</ span >
247- { ( [ "beginner" , "easy" , "medium" , "hard" , "all" ] as const ) . map ( ( d ) => {
248- const c = diffCounts [ d ] ;
249- return (
250- < button key = { d }
251- className = { difficulty === d ? "pill active" : "pill" }
252- onClick = { ( ) => setDifficulty ( d ) }
253- > { DIFF_LABEL [ d ] } < span className = "count" > { c . solved } /{ c . total } </ span > </ button >
254- ) ;
255- } ) }
256- </ div >
257- ) }
258-
259- { mode === "standard" ? (
260- < div className = "puzzle-tabs" >
261- < span className = "tabs-label" > Type</ span >
262- { ( [
263- { key : "all" as MateFilter , label : "All" } ,
264- { key : 1 as MateFilter , label : "M1" } ,
265- { key : 2 as MateFilter , label : "M2" } ,
266- { key : 3 as MateFilter , label : "M3" }
267- ] ) . map ( ( it ) => {
268- const c = mateCounts [ it . key ] ;
269- return (
270- < button key = { String ( it . key ) }
271- className = { mateIn === it . key ? "pill active" : "pill" }
272- onClick = { ( ) => setMateIn ( it . key ) }
273- > { it . label } < span className = "count" > { c . solved } /{ c . total } </ span > </ button >
274- ) ;
275- } ) }
276- < label className = "pill toggle" style = { { cursor : "pointer" , marginLeft : "auto" } } >
277- < input type = "checkbox" checked = { newOnly } onChange = { ( e ) => setNewOnly ( e . target . checked ) } />
278- New only
279- </ label >
280- </ div >
281- ) : (
282- < div className = "puzzle-tabs" >
283- < span className = "tabs-label" > Type</ span >
284- { ( [ "all" , 1 , 2 ] as const ) . map ( ( it ) => {
285- const count = basePortalPool . length === 0 && it !== portalMateIn
286- ? PORTAL_PUZZLES . filter ( ( p ) => it === "all" || p . mateIn ( ) === it ) . length
287- : 0 ;
288- const total = PORTAL_PUZZLES . filter ( ( p ) => it === "all" || p . mateIn ( ) === it ) . length ;
289- const solved = PORTAL_PUZZLES . filter ( ( p ) =>
290- ( it === "all" || p . mateIn ( ) === it ) && solvedIds . has ( p . id )
291- ) . length ;
292- return (
293- < button key = { String ( it ) }
294- className = { portalMateIn === it ? "pill active" : "pill" }
295- onClick = { ( ) => setPortalMateIn ( it ) }
296- > { it === "all" ? "All" : `M${ it } ` } < span className = "count" > { solved } /{ total || count } </ span > </ button >
297- ) ;
298- } ) }
299- < label className = "pill toggle" style = { { cursor : "pointer" , marginLeft : "auto" } } >
300- < input type = "checkbox" checked = { newOnly } onChange = { ( e ) => setNewOnly ( e . target . checked ) } />
301- New only
302- </ label >
303- </ div >
304- ) }
201+ < div className = "puzzle-tabs" >
202+ < span className = "tabs-label" > Type</ span >
203+ { ( [
204+ { key : "all" as MateFilter , label : "All" } ,
205+ { key : 1 as MateFilter , label : "M1" } ,
206+ { key : 2 as MateFilter , label : "M2" } ,
207+ { key : 3 as MateFilter , label : "M3" }
208+ ] ) . map ( ( it ) => {
209+ const c = mateCounts [ it . key ] ;
210+ return (
211+ < button key = { String ( it . key ) }
212+ className = { mateIn === it . key ? "pill active" : "pill" }
213+ onClick = { ( ) => setMateIn ( it . key ) }
214+ > { it . label } < span className = "count" > { c . solved } /{ c . total } </ span > </ button >
215+ ) ;
216+ } ) }
217+ < label className = "pill toggle" style = { { cursor : "pointer" , marginLeft : "auto" } } >
218+ < input type = "checkbox" checked = { newOnly } onChange = { ( e ) => setNewOnly ( e . target . checked ) } />
219+ New only
220+ </ label >
221+ </ div >
305222
306223 { allSolvedHere && newOnly && (
307224 < p className = "hint" style = { { margin : "4px 0 8px" } } >
@@ -316,8 +233,7 @@ export function PuzzlesScreen() {
316233 { banner }
317234 < div className = "puzzle-meta" >
318235 < span > Puzzle { index + 1 } / { pool . length } </ span >
319- { "rating" in puzzle && puzzle . rating !== undefined && < span className = "pill" > ⚡ { puzzle . rating } </ span > }
320- { mode === "portal" && < span className = "pill" > 🌀 Portal</ span > }
236+ { puzzle . rating !== undefined && < span className = "pill" > ⚡ { puzzle . rating } </ span > }
321237 { alreadySolved && < span className = "pill solved" > ✓ Solved</ span > }
322238 { entry && entry . attempts > 0 && < span className = "pill" > Tries: { entry . attempts } </ span > }
323239 </ div >
0 commit comments