diff --git a/src/binary_numbers.rs b/src/binary_numbers.rs index a25d483..a13e3c6 100644 --- a/src/binary_numbers.rs +++ b/src/binary_numbers.rs @@ -723,12 +723,16 @@ impl Bits { pub struct BinaryNumbersPuzzle { bits: Bits, - #[allow(dead_code)] - number_mode: NumberMode, - #[allow(dead_code)] - current_number: u32, // scaled value used for suggestions matching - raw_current_number: u32, // raw bit value (unscaled) for display - suggestions: Vec, // Changed to i32 to support signed values + /// Raw bit pattern (unscaled) for display as binary string. + /// This is u32 (not i32) because it stores the BIT PATTERN, not the numeric value. + /// In signed mode, negative numbers use two's complement representation: + /// - For -1 in 4-bit: raw_current_number = 15 (0b1111), displayed as "1111" + /// - For -8 in 4-bit: raw_current_number = 8 (0b1000), displayed as "1000" + /// + /// The same bit pattern has different meanings in signed vs unsigned mode. + raw_current_number: u32, + suggestions: Vec, + correct_answer: i32, selected_suggestion: Option, time_total: f64, time_left: f64, @@ -777,11 +781,12 @@ impl BinaryNumbersPuzzle { }, } - // Shuffle suggestions - suggestions.shuffle(&mut rng); + // Pick a random suggestion as the current number + let correct_index = rng.random_range(0..suggestions.len()); + let current_number_signed = suggestions[correct_index]; - // Pick first suggestion as the current number - let current_number_signed = suggestions[0]; + // Shuffle suggestions so the correct answer is in a random position + suggestions.shuffle(&mut rng); // Calculate raw_current_number based on mode let raw_current_number = match number_mode { @@ -791,18 +796,19 @@ impl BinaryNumbersPuzzle { }, NumberMode::Signed => { // For signed mode, we need to preserve the two's complement representation - // First, get the unscaled signed value + // Example: -1 in 4-bit two's complement is 0b1111 + // We cast i32 to u32 (preserving bit pattern), then mask to n-bits + // Result: -1 becomes 15u32 (0b1111), which displays as "1111" let unscaled_signed = current_number_signed / (scale as i32); // Convert to unsigned bits using two's complement masking + // Casting i32 to u32 reinterprets the bits (not a numeric conversion) // For n-bit number, mask is (2^n - 1) let mask = (1u32 << num_bits) - 1; (unscaled_signed as u32) & mask }, }; - let current_number = current_number_signed.unsigned_abs(); - // Calculate time based on difficulty let time_total = 10.0 - (streak.min(8) as f64 * 0.5); let time_left = time_total; @@ -813,10 +819,9 @@ impl BinaryNumbersPuzzle { Self { bits, - number_mode, - current_number, raw_current_number, suggestions, + correct_answer: current_number_signed, time_total, time_left, selected_suggestion, @@ -832,7 +837,7 @@ impl BinaryNumbersPuzzle { } pub fn is_correct_guess(&self, guess: i32) -> bool { - guess == self.suggestions[0] + guess == self.correct_answer } pub fn current_to_binary_string(&self) -> String { @@ -1001,9 +1006,9 @@ mod tests { for &s in p.suggestions() { assert_eq!(s.unsigned_abs() % scale, 0); } - // current number must be one of suggestions and raw_current_number * scale == current_number - assert!(p.suggestions().contains(&(p.current_number as i32))); - assert_eq!(p.raw_current_number * scale, p.current_number); + // correct answer must be one of suggestions and raw_current_number * scale == correct_answer (unsigned) + assert!(p.suggestions().contains(&p.correct_answer)); + assert_eq!(p.raw_current_number * scale, p.correct_answer.unsigned_abs()); } #[test] @@ -1045,7 +1050,7 @@ mod tests { // the raw_current_number has the sign bit set correctly for _ in 0..20 { let p = BinaryNumbersPuzzle::new(Bits::Four, NumberMode::Signed, 0); - let current_signed = p.suggestions[0]; + let current_signed = p.correct_answer; if current_signed < 0 { // For negative numbers in 4-bit two's complement, the MSB (bit 3) should be 1 @@ -1084,12 +1089,50 @@ mod tests { assert_eq!(p.guess_result, Some(GuessResult::Timeout)); } + #[test] + fn suggestions_are_randomized() { + // Verify that suggestions are properly randomized and the first suggestion + // is not always the correct answer + let mut first_is_correct_count = 0; + let trials = 100; + + for _ in 0..trials { + let p = BinaryNumbersPuzzle::new(Bits::Four, NumberMode::Unsigned, 0); + // Check if the first suggestion happens to be the correct answer + if p.suggestions[0] == p.correct_answer { + first_is_correct_count += 1; + } + } + + // With 3 suggestions for 4-bit mode, we expect roughly 33% to be correct by chance + // Allow a range of 20-50% (which is generous for 100 trials to account for randomness) + // The key point is that it's NOT 100% (which would indicate no randomization) + assert!( + first_is_correct_count >= 20 && first_is_correct_count <= 50, + "First suggestion was correct {} times out of {}, expected around 33% (20-50 range). \ + If this is close to 100%, suggestions are not randomized!", + first_is_correct_count, + trials + ); + + // Also verify that the correct answer is actually one of the suggestions + for _ in 0..10 { + let p = BinaryNumbersPuzzle::new(Bits::Eight, NumberMode::Signed, 0); + assert!( + p.suggestions.contains(&p.correct_answer), + "correct_answer {} must be in suggestions {:?}", + p.correct_answer, + p.suggestions + ); + } + } + #[test] fn finalize_round_correct_increments_score_streak_and_sets_result_state() { with_high_score_file(|| { let mut g = BinaryNumbersGame::new(Bits::Four, NumberMode::Unsigned); // ensure deterministic: mark puzzle correct - let answer = g.puzzle.current_number as i32; + let answer = g.puzzle.correct_answer; g.puzzle.guess_result = Some(GuessResult::Correct); g.finalize_round(); assert_eq!(g.streak, 1);