66import java .io .IOException ;
77import java .io .OutputStream ;
88import java .net .URL ;
9+ import java .text .Normalizer ;
10+ import java .util .Locale ;
911import java .util .Properties ;
1012import java .util .ResourceBundle ;
1113
14+ import com .model .User ;
15+
1216import javafx .fxml .FXML ;
1317import javafx .fxml .Initializable ;
1418import javafx .scene .control .Alert ;
2226
2327/**
2428 * Anagram puzzle controller — scrambled LPAEP -> APPLE (category Fruit)
25- * Manages the anagram puzzle UI: background loading, attempts/hints state,
26- * simple persistence to a properties file, and navigation on solve/quit.
27- * UI references are checked for null to be defensive when used from FXML.
29+ * Improvements:
30+ * - per-user save file (.escapegame_anagram_<userid>.properties)
31+ * - debug prints showing the save path and loaded state
32+ * - safe parsing of integer properties
33+ * - normalization of user input for robust matching
34+ * - clearSaveForCurrentUser() helper for testing
2835 */
2936public class AnagramPuzzleController implements Initializable {
3037
@@ -54,16 +61,9 @@ public class AnagramPuzzleController implements Initializable {
5461 "apple fruit"
5562 };
5663
57- private final File SAVE_FILE = new File (System .getProperty ("user.home" ), ".escapegame_anagram.properties" );
58-
5964 private static final String RESOURCE_PATH = "/images/background.png" ;
6065 private static final String DEV_FALLBACK = "file:/mnt/data/Screenshot 2025-11-22 202825.png" ;
61- /**
62- * Initialize the puzzle screen: load background image (resource then fallback),
63- * set initial UI labels, draw hearts and restore saved progress.
64- * @param location not used
65- * @param resources not used
66- */
66+
6767 @ Override
6868 public void initialize (URL location , ResourceBundle resources ) {
6969 System .out .println ("AnagramPuzzleController.initialize() start" );
@@ -73,7 +73,7 @@ public void initialize(URL location, ResourceBundle resources) {
7373 URL res = getClass ().getResource (RESOURCE_PATH );
7474 if (res != null ) {
7575 Image img = new Image (res .toExternalForm ());
76- backgroundImage .setImage (img );
76+ if ( backgroundImage != null ) backgroundImage .setImage (img );
7777 loaded = true ;
7878 }
7979 } catch (Exception ex ) {
@@ -83,7 +83,7 @@ public void initialize(URL location, ResourceBundle resources) {
8383 if (!loaded ) {
8484 try {
8585 Image img = new Image (DEV_FALLBACK );
86- backgroundImage .setImage (img );
86+ if ( backgroundImage != null ) backgroundImage .setImage (img );
8787 loaded = true ;
8888 } catch (Exception ex ) {
8989 System .err .println ("Error loading dev fallback image: " + ex .getMessage ());
@@ -104,19 +104,73 @@ public void initialize(URL location, ResourceBundle resources) {
104104 if (hintsLabel != null ) hintsLabel .setText (hintsLeft + " hint(s) available" );
105105 if (scrambledLabel != null ) scrambledLabel .setText ("Scrambled: LPAEP" );
106106 if (categoryLabel != null ) categoryLabel .setText ("Category: Fruit" );
107- if (promptLabel != null ) promptLabel .setText ("Prompt: Unscramble the letters to find the word" );
107+ if (promptLabel != null ) promptLabel .setText ("Prompt: Unscramble the letters to find the word: LPAEP " );
108108 refreshHearts ();
109+
110+ // debug: show exactly which save file will be used
111+ System .out .println ("DEBUG: Anagram save path -> " + getSaveFileForCurrentUser ().getAbsolutePath ());
112+
109113 loadSave ();
110114
115+ // If save marked solved=true, automatically clear it (one-time reset) and re-enable UI
116+ if (solved ) {
117+ System .out .println ("DEBUG: Saved state indicates solved=true — clearing save to reset puzzle." );
118+ clearSaveForCurrentUser ();
119+ solved = false ;
120+ attemptsLeft = 3 ;
121+ hintsLeft = 3 ;
122+ nextHintIndex = 0 ;
123+ if (hintsLabel != null ) hintsLabel .setText (hintsLeft + " hint(s) available" );
124+ if (statusLabel != null ) statusLabel .setText ("" );
125+ if (btnSubmit != null ) btnSubmit .setDisable (false );
126+ if (btnHint != null ) btnHint .setDisable (false );
127+ if (answerField != null ) answerField .setDisable (false );
128+ refreshHearts ();
129+ }
130+
111131 System .out .println ("AnagramPuzzleController.initialize() done" );
112132 }
113- /**
114- * Draw hearts representing remaining attempts and disable inputs when none remain.
115- */
133+
134+ private File getSaveFileForCurrentUser () {
135+ String userId = "guest" ;
136+ try {
137+ User u = null ;
138+ try {
139+ u = App .getCurrentUser ();
140+ } catch (Throwable t ) {
141+ // fallback to reflection if needed
142+ try {
143+ java .lang .reflect .Method gu = App .class .getMethod ("getCurrentUser" );
144+ Object userObj = gu .invoke (null );
145+ if (userObj instanceof User ) {
146+ u = (User ) userObj ;
147+ } else if (userObj != null ) {
148+ try {
149+ java .lang .reflect .Method getId = userObj .getClass ().getMethod ("getUserId" );
150+ Object idObj = getId .invoke (userObj );
151+ if (idObj != null ) {
152+ final String idStr = idObj .toString ();
153+ u = new User () { @ Override public String getUserId () { return idStr ; } };
154+ }
155+ } catch (Throwable ignored ) { }
156+ }
157+ } catch (Throwable ignored ) { }
158+ }
159+ if (u != null && u .getUserId () != null && !u .getUserId ().trim ().isEmpty ()) {
160+ userId = u .getUserId ().trim ();
161+ }
162+ } catch (Throwable t ) {
163+ // ignore, use guest
164+ }
165+ String clean = userId .replaceAll ("\\ s+" , "_" ).toLowerCase (Locale .ROOT );
166+ String filename = ".escapegame_anagram_" + clean + ".properties" ;
167+ return new File (System .getProperty ("user.home" ), filename );
168+ }
169+
116170 private void refreshHearts () {
117171 if (heartsBox == null ) return ;
118172 heartsBox .getChildren ().clear ();
119- for (int i = 0 ; i < attemptsLeft ; i ++) {
173+ for (int i = 0 ; i < Math . max ( 0 , attemptsLeft ) ; i ++) {
120174 Label heart = new Label ("\u2764 " );
121175 heart .setStyle ("-fx-text-fill: #ff4d6d; -fx-font-size: 20px;" );
122176 heartsBox .getChildren ().add (heart );
@@ -126,26 +180,24 @@ private void refreshHearts() {
126180 if (answerField != null ) answerField .setDisable (true );
127181 }
128182 }
129- /**
130- * Called when the user submits an answer. Validates input, checks accepted
131- * answers, updates state, shows alerts, saves progress and navigates on success.
132- */
183+
133184 @ FXML
134185 private void onSubmit () {
135186 if (solved ) {
136187 if (statusLabel != null ) statusLabel .setText ("You already solved the puzzle." );
137188 return ;
138189 }
139190
140- String answer = (answerField == null || answerField .getText () == null ) ? "" : answerField .getText ().trim ().toLowerCase ();
191+ String raw = (answerField == null || answerField .getText () == null ) ? "" : answerField .getText ();
192+ String answer = normalize (raw );
141193 if (answer .isEmpty ()) {
142194 if (statusLabel != null ) statusLabel .setText ("Please enter an answer." );
143195 return ;
144196 }
145197
146198 boolean ok = false ;
147199 for (String a : ACCEPTED_ANSWERS ) {
148- if (a .equals (answer )) { ok = true ; break ; }
200+ if (normalize ( a ) .equals (answer )) { ok = true ; break ; }
149201 }
150202
151203 if (ok ) {
@@ -158,7 +210,7 @@ private void onSubmit() {
158210 saveProgress ();
159211 try { App .setRoot ("opened5" ); } catch (IOException e ) { e .printStackTrace (); }
160212 } else {
161- attemptsLeft -- ;
213+ attemptsLeft = Math . max ( 0 , attemptsLeft - 1 ) ;
162214 refreshHearts ();
163215 if (attemptsLeft <= 0 ) {
164216 if (statusLabel != null ) statusLabel .setText ("No attempts left. The correct answer was: \" apple\" ." );
@@ -171,9 +223,7 @@ private void onSubmit() {
171223 }
172224 }
173225 }
174- /**
175- * Shows the next hint (if available), updates hint counters and saves progress.
176- */
226+
177227 @ FXML
178228 private void onHint () {
179229 if (solved ) {
@@ -186,62 +236,63 @@ private void onHint() {
186236 }
187237
188238 String hint = HINTS [Math .min (nextHintIndex , HINTS .length - 1 )];
189- nextHintIndex ++ ;
190- hintsLeft -- ;
239+ nextHintIndex = Math . min ( nextHintIndex + 1 , HINTS . length ) ;
240+ hintsLeft = Math . max ( 0 , hintsLeft - 1 ) ;
191241 if (hintsLabel != null ) hintsLabel .setText (hintsLeft + " hint(s) available" );
192242 new Alert (Alert .AlertType .INFORMATION , hint ).showAndWait ();
193243 saveProgress ();
194244 }
195- /** Saves progress to the user's properties file and updates the status label. */
245+
196246 @ FXML
197247 private void onSave () {
198248 if (statusLabel != null ) statusLabel .setText (saveProgress () ? "Progress saved." : "Save failed." );
199249 }
200- /** Quit action: navigate back to the previous/opened4 screen. */
250+
201251 @ FXML
202252 private void onQuit () {
203- try { App .setRoot ("opened4" );
204- } catch (IOException e ) {
205- e .printStackTrace ();
206- }
253+ try { App .setRoot ("opened4" ); } catch (IOException e ) { e .printStackTrace (); }
207254 }
208- /**
209- * Persist puzzle state to a properties file under the user's home directory.
210- * @return true if save succeeded, false otherwise
211- */
255+
212256 private boolean saveProgress () {
213257 try {
214258 Properties p = new Properties ();
215259 p .setProperty ("attemptsLeft" , String .valueOf (attemptsLeft ));
216260 p .setProperty ("hintsLeft" , String .valueOf (hintsLeft ));
217261 p .setProperty ("solved" , String .valueOf (solved ));
218262 p .setProperty ("nextHintIndex" , String .valueOf (nextHintIndex ));
219- try (OutputStream os = new FileOutputStream (SAVE_FILE )) {
263+ File out = getSaveFileForCurrentUser ();
264+ try (OutputStream os = new FileOutputStream (out )) {
220265 p .store (os , "Anagram puzzle save" );
221266 }
267+ System .out .println ("DEBUG saved -> " + out .getAbsolutePath ());
222268 return true ;
223269 } catch (Exception e ) {
224270 System .err .println ("Save error: " + e .getMessage ());
225271 return false ;
226272 }
227273 }
228- /**
229- * Load saved puzzle state from properties file if present and update UI.
230- */
274+
231275 private void loadSave () {
232276 try {
233- if (!SAVE_FILE .exists ()) return ;
277+ File f = getSaveFileForCurrentUser ();
278+ if (!f .exists ()) {
279+ System .out .println ("DEBUG: save file not found -> " + f .getAbsolutePath ());
280+ return ;
281+ }
234282 Properties p = new Properties ();
235- try (FileInputStream fis = new FileInputStream (SAVE_FILE )) {
283+ try (FileInputStream fis = new FileInputStream (f )) {
236284 p .load (fis );
237285 }
238- attemptsLeft = Integer . parseInt (p .getProperty ("attemptsLeft" , "3" ) );
239- hintsLeft = Integer . parseInt (p .getProperty ("hintsLeft" , "3" ) );
240- solved = Boolean .parseBoolean (p .getProperty ("solved" , "false" ));
241- nextHintIndex = Integer . parseInt (p .getProperty ("nextHintIndex" , "0" ) );
286+ attemptsLeft = getSafeInt (p .getProperty ("attemptsLeft" ), attemptsLeft );
287+ hintsLeft = getSafeInt (p .getProperty ("hintsLeft" ), hintsLeft );
288+ solved = Boolean .parseBoolean (p .getProperty ("solved" , String . valueOf ( solved ) ));
289+ nextHintIndex = getSafeInt (p .getProperty ("nextHintIndex" ), nextHintIndex );
242290 if (hintsLabel != null ) hintsLabel .setText (hintsLeft + " hint(s) available" );
243291 refreshHearts ();
244292
293+ System .out .println ("DEBUG loaded - attemptsLeft=" + attemptsLeft
294+ + " hintsLeft=" + hintsLeft + " nextHintIndex=" + nextHintIndex + " solved=" + solved
295+ + " (from " + f .getAbsolutePath () + ")" );
245296 if (solved ) {
246297 if (btnSubmit != null ) btnSubmit .setDisable (true );
247298 if (answerField != null ) answerField .setDisable (true );
@@ -252,4 +303,39 @@ private void loadSave() {
252303 System .err .println ("Load save failed: " + e .getMessage ());
253304 }
254305 }
306+
307+ private int getSafeInt (String s , int fallback ) {
308+ if (s == null ) return fallback ;
309+ try {
310+ return Integer .parseInt (s .trim ());
311+ } catch (NumberFormatException ex ) {
312+ System .err .println ("Invalid integer in save file: \" " + s + "\" ; using fallback " + fallback );
313+ return fallback ;
314+ }
315+ }
316+
317+ private static String normalize (String raw ) {
318+ if (raw == null ) return "" ;
319+ String n = Normalizer .normalize (raw , Normalizer .Form .NFKC )
320+ .toLowerCase (Locale .ROOT )
321+ .replaceAll ("[^\\ p{L}\\ p{N}\\ s]" , " " )
322+ .trim ()
323+ .replaceAll ("\\ s+" , " " );
324+ if (n .startsWith ("a " )) n = n .substring (2 ).trim ();
325+ else if (n .startsWith ("an " )) n = n .substring (3 ).trim ();
326+ else if (n .startsWith ("the " )) n = n .substring (4 ).trim ();
327+ return n ;
328+ }
329+
330+ private boolean clearSaveForCurrentUser () {
331+ File f = getSaveFileForCurrentUser ();
332+ if (f .exists ()) {
333+ boolean deleted = f .delete ();
334+ System .out .println ("DEBUG: clearSaveForCurrentUser() deleted=" + deleted + " -> " + f .getAbsolutePath ());
335+ return deleted ;
336+ } else {
337+ System .out .println ("DEBUG: clearSaveForCurrentUser() not found -> " + f .getAbsolutePath ());
338+ return true ;
339+ }
340+ }
255341}
0 commit comments