mirror of
https://github.com/lucaspalomodevelop/binbreak.git
synced 2026-03-13 00:07:28 +00:00
fix: first suggestion always correct (#11)
* feat: randomize suggestions and store correct answer in BinaryNumbersPuzzle * feat: remove unused fields and update assertions in BinaryNumbersPuzzle * feat: update documentation for raw_current_number and clarify signed mode handling * cleanup * cleanup * cleanup
This commit is contained in:
parent
503f9eef0b
commit
b1df1b1fbd
@ -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,
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user