@@ -167,11 +167,16 @@ impl Battle {
167167
168168 /// Calculate spawn position jitter from entropy seed
169169 /// Returns (x_offset, y_offset) in range [-10, 10]
170- fn calculate_spawn_jitter ( & self , seed_offset : usize ) -> ( isize , isize ) {
170+ pub ( crate ) fn calculate_spawn_jitter ( & self , seed_offset : usize ) -> ( isize , isize ) {
171171 if self . entropy_seed == [ 0u8 ; 32 ] {
172172 return ( 0 , 0 ) ;
173173 }
174174
175+ // Ensure seed_offset + 7 is within bounds of 32-byte array
176+ if seed_offset + 7 >= 32 {
177+ return ( 0 , 0 ) ;
178+ }
179+
175180 // Use different parts of entropy seed for x and y
176181 let x_bytes = [
177182 self . entropy_seed [ seed_offset] ,
@@ -186,12 +191,13 @@ impl Battle {
186191 self . entropy_seed [ seed_offset + 7 ] ,
187192 ] ;
188193
189- let x_val = i32:: from_le_bytes ( x_bytes) ;
190- let y_val = i32:: from_le_bytes ( y_bytes) ;
194+ // Use u32 to avoid negative modulo issues
195+ let x_val = u32:: from_le_bytes ( x_bytes) ;
196+ let y_val = u32:: from_le_bytes ( y_bytes) ;
191197
192- // Map to [-10, 10] range
193- let x_jitter = ( x_val % 21 - 10 ) as isize ;
194- let y_jitter = ( y_val % 21 - 10 ) as isize ;
198+ // Map to [-10, 10] range: (x % 21) gives 0-20, subtract 10 gives -10 to 10
199+ let x_jitter = ( x_val % 21 ) as isize - 10 ;
200+ let y_jitter = ( y_val % 21 ) as isize - 10 ;
195201
196202 ( x_jitter, y_jitter)
197203 }
@@ -227,7 +233,12 @@ impl Battle {
227233 // Random energy from entropy
228234 let energy = ( self . entropy_seed [ ( seed_idx + 20 ) % 32 ] % 100 ) + 1 ;
229235
230- grid. set ( Position :: new ( x, y) , Cell :: alive ( energy) ) ;
236+ // Skip positions that already have live cells (gliders)
237+ let pos = Position :: new ( x, y) ;
238+ if grid. get ( pos) . is_alive ( ) {
239+ continue ;
240+ }
241+ grid. set ( pos, Cell :: alive ( energy) ) ;
231242 }
232243 }
233244
@@ -298,7 +309,7 @@ impl Battle {
298309 }
299310
300311 /// Calculate energy fluctuations from entropy (±5%)
301- fn calculate_energy_fluctuations ( & self ) -> ( f64 , f64 ) {
312+ pub ( crate ) fn calculate_energy_fluctuations ( & self ) -> ( f64 , f64 ) {
302313 let fluct_a_byte = self . entropy_seed [ 24 ] ;
303314 let fluct_b_byte = self . entropy_seed [ 25 ] ;
304315
@@ -426,19 +437,22 @@ impl Battle {
426437 }
427438
428439 /// Lexicographic tiebreaker using hash of glider + entropy seed
429- fn lexicographic_break ( & self ) -> BattleOutcome {
440+ pub ( crate ) fn lexicographic_break ( & self ) -> BattleOutcome {
430441 let hash_a = self . hash_glider ( & self . glider_a ) ;
431442 let hash_b = self . hash_glider ( & self . glider_b ) ;
432443
433444 if hash_a < hash_b {
434445 BattleOutcome :: AWins
435- } else {
446+ } else if hash_a > hash_b {
436447 BattleOutcome :: BWins
448+ } else {
449+ // Hashes equal - should never happen with proper entropy, but handle gracefully
450+ BattleOutcome :: AWins
437451 }
438452 }
439453
440454 /// Simple FNV-1a hash for deterministic tiebreaking
441- fn hash_glider ( & self , glider : & Glider ) -> u64 {
455+ pub ( crate ) fn hash_glider ( & self , glider : & Glider ) -> u64 {
442456 let mut hash = 0xcbf29ce484222325 ; // FNV offset basis
443457
444458 // Mix in entropy seed
@@ -453,6 +467,12 @@ impl Battle {
453467 hash = hash. wrapping_mul ( 0x100000001b3 ) ;
454468 }
455469
470+ // Mix in glider position
471+ hash ^= glider. position . x as u64 ;
472+ hash = hash. wrapping_mul ( 0x100000001b3 ) ;
473+ hash ^= glider. position . y as u64 ;
474+ hash = hash. wrapping_mul ( 0x100000001b3 ) ;
475+
456476 hash
457477 }
458478
@@ -656,10 +676,155 @@ mod tests {
656676 assert ! ( ted_a >= 0.0 ) ;
657677 assert ! ( ted_b >= 0.0 ) ;
658678
659- // Outcome should be valid
679+ // Outcome should never be Tie with MII+ tiebreaker system fully implemented
660680 assert ! ( matches!(
661681 outcome,
662- BattleOutcome :: AWins | BattleOutcome :: BWins | BattleOutcome :: Tie
682+ BattleOutcome :: AWins | BattleOutcome :: BWins
663683 ) ) ;
664684 }
685+
686+ #[ test]
687+ fn test_spawn_jitter_range ( ) {
688+ // Test that spawn jitter stays within [-10, 10] range
689+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
690+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
691+
692+ // Test with various entropy seeds
693+ for seed_byte in [ 0u8 , 1 , 127 , 255 ] {
694+ let entropy_seed = [ seed_byte; 32 ] ;
695+ let battle = Battle :: with_entropy ( glider_a. clone ( ) , glider_b. clone ( ) , 10 , entropy_seed) ;
696+
697+ let ( jitter_x, jitter_y) = battle. calculate_spawn_jitter ( 0 ) ;
698+ assert ! ( jitter_x >= -10 && jitter_x <= 10 , "X jitter out of range: {}" , jitter_x) ;
699+ assert ! ( jitter_y >= -10 && jitter_y <= 10 , "Y jitter out of range: {}" , jitter_y) ;
700+ }
701+ }
702+
703+ #[ test]
704+ fn test_spawn_jitter_determinism ( ) {
705+ // Test that same entropy seed produces same jitter
706+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
707+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
708+ let entropy_seed = [ 42u8 ; 32 ] ;
709+
710+ let battle1 = Battle :: with_entropy ( glider_a. clone ( ) , glider_b. clone ( ) , 10 , entropy_seed) ;
711+ let battle2 = Battle :: with_entropy ( glider_a, glider_b, 10 , entropy_seed) ;
712+
713+ assert_eq ! ( battle1. calculate_spawn_jitter( 0 ) , battle2. calculate_spawn_jitter( 0 ) ) ;
714+ assert_eq ! ( battle1. calculate_spawn_jitter( 8 ) , battle2. calculate_spawn_jitter( 8 ) ) ;
715+ }
716+
717+ #[ test]
718+ fn test_energy_fluctuations_range ( ) {
719+ // Test that energy fluctuations stay within [0.95, 1.05] range
720+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
721+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
722+
723+ for seed_byte in [ 0u8 , 1 , 127 , 255 ] {
724+ let entropy_seed = [ seed_byte; 32 ] ;
725+ let battle = Battle :: with_entropy ( glider_a. clone ( ) , glider_b. clone ( ) , 10 , entropy_seed) ;
726+
727+ let ( fluct_a, fluct_b) = battle. calculate_energy_fluctuations ( ) ;
728+ assert ! ( fluct_a >= 0.95 && fluct_a <= 1.05 , "Fluctuation A out of range: {}" , fluct_a) ;
729+ assert ! ( fluct_b >= 0.95 && fluct_b <= 1.05 , "Fluctuation B out of range: {}" , fluct_b) ;
730+ }
731+ }
732+
733+ #[ test]
734+ fn test_noise_skips_existing_cells ( ) {
735+ // Test that noise doesn't overwrite existing glider cells
736+ let glider_a = Glider :: with_energy ( GliderPattern :: Standard , SPAWN_A , 200 ) ;
737+ let glider_b = Glider :: with_energy ( GliderPattern :: Standard , SPAWN_B , 200 ) ;
738+ let entropy_seed = [ 1u8 ; 32 ] ;
739+
740+ let battle = Battle :: with_entropy ( glider_a, glider_b, 10 , entropy_seed) ;
741+ let grid = battle. initial_grid ( ) ;
742+
743+ // Glider cells should still have their original high energy (200)
744+ // Noise cells have energy between 1-100
745+ // Check that high-energy cells exist (indicating gliders weren't overwritten)
746+ let mut high_energy_count = 0 ;
747+ for y in 0 ..1024 {
748+ for x in 0 ..1024 {
749+ let cell = grid. get ( Position :: new ( x, y) ) ;
750+ if cell. energy ( ) >= 200 {
751+ high_energy_count += 1 ;
752+ }
753+ }
754+ }
755+
756+ // Both gliders should have their cells intact (each has 5 cells for Standard pattern)
757+ assert ! ( high_energy_count >= 10 , "Expected at least 10 high-energy cells, got {}" , high_energy_count) ;
758+ }
759+
760+ #[ test]
761+ fn test_lexicographic_tiebreaker_determinism ( ) {
762+ // Test that same gliders with same entropy produce same outcome
763+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
764+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
765+ let entropy_seed = [ 42u8 ; 32 ] ;
766+
767+ let battle1 = Battle :: with_entropy ( glider_a. clone ( ) , glider_b. clone ( ) , 10 , entropy_seed) ;
768+ let battle2 = Battle :: with_entropy ( glider_a, glider_b, 10 , entropy_seed) ;
769+
770+ let outcome1 = battle1. lexicographic_break ( ) ;
771+ let outcome2 = battle2. lexicographic_break ( ) ;
772+
773+ assert_eq ! ( outcome1, outcome2, "Same inputs should produce same lexicographic outcome" ) ;
774+ }
775+
776+ #[ test]
777+ fn test_lexicographic_different_positions ( ) {
778+ // Test that gliders with same pattern but different positions produce different hashes
779+ let glider_a1 = Glider :: new ( GliderPattern :: Standard , Position :: new ( 100 , 100 ) ) ;
780+ let glider_a2 = Glider :: new ( GliderPattern :: Standard , Position :: new ( 200 , 200 ) ) ;
781+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
782+ let entropy_seed = [ 42u8 ; 32 ] ;
783+
784+ let battle1 = Battle :: with_entropy ( glider_a1, glider_b. clone ( ) , 10 , entropy_seed) ;
785+ let battle2 = Battle :: with_entropy ( glider_a2, glider_b, 10 , entropy_seed) ;
786+
787+ let hash1 = battle1. hash_glider ( & battle1. glider_a ) ;
788+ let hash2 = battle2. hash_glider ( & battle2. glider_a ) ;
789+
790+ assert_ne ! ( hash1, hash2, "Same pattern at different positions should produce different hashes" ) ;
791+ }
792+
793+ #[ test]
794+ fn test_lexicographic_different_entropy ( ) {
795+ // Test that same gliders with different entropy produce different hashes
796+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
797+ let glider_b = Glider :: new ( GliderPattern :: Standard , SPAWN_B ) ;
798+
799+ let battle1 = Battle :: with_entropy ( glider_a. clone ( ) , glider_b. clone ( ) , 10 , [ 1u8 ; 32 ] ) ;
800+ let battle2 = Battle :: with_entropy ( glider_a, glider_b, 10 , [ 2u8 ; 32 ] ) ;
801+
802+ let hash1 = battle1. hash_glider ( & battle1. glider_a ) ;
803+ let hash2 = battle2. hash_glider ( & battle2. glider_a ) ;
804+
805+ assert_ne ! ( hash1, hash2, "Different entropy seeds should produce different hashes" ) ;
806+ }
807+
808+ #[ test]
809+ fn test_lexicographic_ordering ( ) {
810+ // Test that hash ordering is consistent
811+ let glider_a = Glider :: new ( GliderPattern :: Standard , SPAWN_A ) ;
812+ let glider_b = Glider :: new ( GliderPattern :: Heavyweight , SPAWN_B ) ;
813+ let entropy_seed = [ 42u8 ; 32 ] ;
814+
815+ let battle = Battle :: with_entropy ( glider_a, glider_b, 10 , entropy_seed) ;
816+ let hash_a = battle. hash_glider ( & battle. glider_a ) ;
817+ let hash_b = battle. hash_glider ( & battle. glider_b ) ;
818+
819+ let outcome = battle. lexicographic_break ( ) ;
820+
821+ if hash_a < hash_b {
822+ assert_eq ! ( outcome, BattleOutcome :: AWins ) ;
823+ } else if hash_a > hash_b {
824+ assert_eq ! ( outcome, BattleOutcome :: BWins ) ;
825+ } else {
826+ // If hashes are equal, lexicographic_break returns AWins
827+ assert_eq ! ( outcome, BattleOutcome :: AWins ) ;
828+ }
829+ }
665830}
0 commit comments