Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 64 additions & 21 deletions src/binary_numbers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32>, // 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<i32>,
correct_answer: i32,
selected_suggestion: Option<i32>,
time_total: f64,
time_left: f64,
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down