diff --git a/src/movegen-test-baseline.txt b/src/movegen-test-baseline.txt index 07ebd11..495a68c 100644 --- a/src/movegen-test-baseline.txt +++ b/src/movegen-test-baseline.txt @@ -191,16 +191,16 @@ 13.0460 9B ROTOR 13 === Case 11: rack=ST fen=ZONULE1B2APAID/1KY2RHANJA4/GAM4R2HUI2/7G6D/6FECIT3O/6AE1TOWIES/6I7E/1EnGUARD6D/NAOI2W8/6AT7/5PYE7/5L1L7/2COVE1L7/5X1E7/7N7 === 18.1100 11F (PYE)T 9 - 14.1100 6F T(AE) 5 14.1100 B8 (EA)T 5 + 14.1100 6F T(AE) 5 14.0000 I10 ST 14 - 13.1100 N1 (I)T 4 + 13.1100 7N T(E) 4 13.1100 L3 (U)T 4 - 13.1100 7F T(I) 4 13.1100 5N T(O) 4 - 13.1100 7N T(E) 4 - 13.1100 D8 (GI)T 4 13.1100 D12 T(O) 4 + 13.1100 N1 (I)T 4 + 13.1100 D8 (GI)T 4 + 13.1100 7F T(I) 4 13.0000 N1 (I)TS 13 12.1100 K5 (TO)T 3 12.1100 B7 T(EA) 3 @@ -209,14 +209,14 @@ 11.0000 2N OO 11 11.0000 7L OO 11 10.0000 7K OO 10 - 8.0000 10C OO 8 8.0000 I12 OO 8 + 8.0000 10C OO 8 7.0000 L6 (W)OO 7 6.5220 3G O(R) 9 6.0000 10D OO 6 + 5.0000 N8 OO 5 5.0000 C13 (C)OO 5 5.0000 A3 (G)OO 5 - 5.0000 N8 OO 5 4.0000 13H (L)OO 4 3.5220 1H (B)O 6 3.0000 A9 (N)OO 3 @@ -230,9 +230,9 @@ 7.0000 L6 (W)OO 7 6.5220 3G O(R) 9 6.0000 10D OO 6 - 5.0000 A3 (G)OO 5 5.0000 N8 OO 5 5.0000 C13 (C)OO 5 + 5.0000 A3 (G)OO 5 4.0000 13H (L)OO 4 3.5220 1H (B)O 6 3.0000 15H (N)OO 3 @@ -243,15 +243,15 @@ 1.5220 D12 O(O) 4 1.5220 D8 (GI)O 4 1.5220 A3 (G)O 4 - 1.5220 N1 (I)O 4 1.5220 5N O(O) 4 + 1.5220 N1 (I)O 4 0.5220 O1 (D)O 3 - 0.5220 4N O(D) 3 0.5220 M6 (I)O 3 - 0.5220 C7 O(nO) 3 0.5220 13H (L)O 3 0.5220 8N O(D) 3 + 0.5220 C7 O(nO) 3 0.5220 K5 (TO)O 3 + 0.5220 4N O(D) 3 0.0000 Exch. OO -0.4780 15G O(N) 2 -0.4780 12H (L)O 2 @@ -265,13 +265,13 @@ -2.4780 Exch. O -9.6960 Pass === Case 14: rack=VUAENRU fen=5MOZ6S/2FIREPOTS4p/2Y2UTA1HWAN1E/DAK8L2C/OWE1BIB4E2I/CADGE6U1PA/I4DOGY2R1aT/L2GLORIA1LOVIE/E1XI1TAED2N1N1/RAI8S1T1/13I1/13E1/13R1/15/15 === - 17.4770 12L UV(E)A 16 17.4770 M13 UVA 16 + 17.4770 12L UV(E)A 16 16.8870 12H UNREAV(E) 22 - 15.4770 5J UV(E)A 14 + 15.4770 5I UVA(E) 14 15.4770 K10 UVA 14 + 15.4770 5J UV(E)A 14 15.4770 12K UVA(E) 14 - 15.4770 5I UVA(E) 14 14.4390 12I AVENU(E) 18 12.8570 K10 URVA 16 12.4770 1L VAU(S) 11 diff --git a/src/movegen.rs b/src/movegen.rs index 8524d68..a4757b1 100644 --- a/src/movegen.rs +++ b/src/movegen.rs @@ -65,6 +65,10 @@ struct WorkingBuffer { perpendicular_word_multipliers_for_down_plays: Box<[i8]>, // c*r perpendicular_scores_for_across_plays: Box<[i32]>, // r*c (multiplied by perpendicular_word_multipliers) perpendicular_scores_for_down_plays: Box<[i32]>, // c*r + left_extension_set_for_across_plays: Box<[u64]>, // r*c + right_extension_set_for_across_plays: Box<[u64]>, // r*c + left_extension_set_for_down_plays: Box<[u64]>, // c*r + right_extension_set_for_down_plays: Box<[u64]>, // c*r transposed_board_tiles: Box<[u8]>, // c*r num_tiles_on_board: u16, num_tiles_in_bag: i16, // negative when players also have less than full racks @@ -124,6 +128,10 @@ impl Clone for WorkingBuffer { .perpendicular_scores_for_across_plays .clone(), perpendicular_scores_for_down_plays: self.perpendicular_scores_for_down_plays.clone(), + left_extension_set_for_across_plays: self.left_extension_set_for_across_plays.clone(), + right_extension_set_for_across_plays: self.right_extension_set_for_across_plays.clone(), + left_extension_set_for_down_plays: self.left_extension_set_for_down_plays.clone(), + right_extension_set_for_down_plays: self.right_extension_set_for_down_plays.clone(), transposed_board_tiles: self.transposed_board_tiles.clone(), num_tiles_on_board: self.num_tiles_on_board, num_tiles_in_bag: self.num_tiles_in_bag, @@ -186,6 +194,14 @@ impl Clone for WorkingBuffer { .clone_from(&source.perpendicular_scores_for_across_plays); self.perpendicular_scores_for_down_plays .clone_from(&source.perpendicular_scores_for_down_plays); + self.left_extension_set_for_across_plays + .clone_from(&source.left_extension_set_for_across_plays); + self.right_extension_set_for_across_plays + .clone_from(&source.right_extension_set_for_across_plays); + self.left_extension_set_for_down_plays + .clone_from(&source.left_extension_set_for_down_plays); + self.right_extension_set_for_down_plays + .clone_from(&source.right_extension_set_for_down_plays); self.transposed_board_tiles .clone_from(&source.transposed_board_tiles); self.num_tiles_on_board @@ -278,6 +294,10 @@ impl WorkingBuffer { .into_boxed_slice(), perpendicular_scores_for_across_plays: vec![0i32; rows_times_cols].into_boxed_slice(), perpendicular_scores_for_down_plays: vec![0i32; rows_times_cols].into_boxed_slice(), + left_extension_set_for_across_plays: vec![!0u64; rows_times_cols].into_boxed_slice(), + right_extension_set_for_across_plays: vec![!0u64; rows_times_cols].into_boxed_slice(), + left_extension_set_for_down_plays: vec![!0u64; rows_times_cols].into_boxed_slice(), + right_extension_set_for_down_plays: vec![!0u64; rows_times_cols].into_boxed_slice(), transposed_board_tiles: vec![0u8; rows_times_cols].into_boxed_slice(), num_tiles_on_board: 0, num_tiles_in_bag: 0, @@ -528,6 +548,10 @@ impl WorkingBuffer { p_right: 0, bits: 0, }); + self.left_extension_set_for_across_plays.fill(!0u64); + self.right_extension_set_for_across_plays.fill(!0u64); + self.left_extension_set_for_down_plays.fill(!0u64); + self.right_extension_set_for_down_plays.fill(!0u64); self.accepts_alpha_cache.fill(([0u8; 64], false)); } } @@ -551,7 +575,7 @@ fn gen_classic_cross_set<'a, N: kwg::Node, L: kwg::Node>( ) { let len = output_strider.len(); let step = output_strider.step() as usize; - let kwg = &board_snapshot.kwg; + let kwg = board_snapshot.kwg; let mut last_nonempty = len; { let alphabet = board_snapshot.game_config.alphabet(); @@ -824,7 +848,7 @@ fn gen_jumbled_cross_set<'a, N: kwg::Node, L: kwg::Node>( let len = output_strider.len(); let step = output_strider.step() as usize; let mut wp = output_strider.base() as usize; - let kwg = &board_snapshot.kwg; + let kwg = board_snapshot.kwg; let alphabet = board_snapshot.game_config.alphabet(); let mut prev_wp = !0; for i in 0..len { @@ -906,6 +930,74 @@ fn gen_cross_set<'a, N: kwg::Node, L: kwg::Node>( } } +// Compute left and right extension sets for a strip in the play direction. +// Left extension set at position j: which tiles can extend leftward into j, +// given the board tiles to the right of j. +// Right extension set at position j: which tiles can extend rightward into j, +// given the board tiles to the left of j. +// Non-adjacent empty squares get !0u64 (all bits, no constraint). +fn gen_extension_sets( + kwg: &kwg::Kwg, + board_strip: &[u8], + left_extension_sets: &mut [u64], + right_extension_sets: &mut [u64], +) { + let len = board_strip.len(); + + // Collect extension set bits from a GADDAG node's children. + // Excludes separator (bit 0), then adds blank bit if non-empty. + let collect_bits = |kwg: &kwg::Kwg, p: i32| -> u64 { + if p <= 0 { + return 0; + } + let mut bits = 0u64; + let mut q = kwg[p].arc_index(); + if q > 0 { + loop { + let node = kwg[q]; + bits |= 1u64 << node.tile(); + if node.is_end() { + break; + } + q += 1; + } + } + let bits = bits & !1; + bits | (bits != 0) as u64 + }; + + // Single right-to-left pass. For each tile group, one GADDAG traversal + // yields both left extension (children of p) and right extension + // (children of seek(p, separator)). + let mut p: i32 = 1; + let mut group_right_empty: usize = len; + for j in (0..len).rev() { + if board_strip[j] != 0 { + if j + 1 == len || board_strip[j + 1] == 0 { + group_right_empty = j + 1; + p = 1; + } + p = kwg.seek(p, board_strip[j] & 0x7f); + } else { + if j + 1 < len && board_strip[j + 1] != 0 { + // Empty square immediately left of a tile group. + // p = GADDAG state after traversing entire group right-to-left. + left_extension_sets[j] = collect_bits(kwg, p); + if group_right_empty < len { + right_extension_sets[group_right_empty] = + collect_bits(kwg, if p > 0 { kwg.seek(p, 0) } else { -1 }); + } + } + p = 1; + } + } + // Handle group starting at position 0. + if !board_strip.is_empty() && board_strip[0] != 0 && group_right_empty < len { + right_extension_sets[group_right_empty] = + collect_bits(kwg, if p > 0 { kwg.seek(p, 0) } else { -1 }); + } +} + struct GenPlacePlacementsParams<'a> { board_strip: &'a [u8], alphabet: &'a alphabet::Alphabet, @@ -914,6 +1006,8 @@ struct GenPlacePlacementsParams<'a> { used_tile_scores_shadowr: &'a mut Vec, shadow_strip_buffer: &'a mut [u8], // not really storing letters here cross_set_strip: &'a [CrossSet], + left_extension_strip: &'a [u64], + right_extension_strip: &'a [u64], remaining_word_multipliers_strip: &'a [i8], remaining_tile_multipliers_strip: &'a [i8], perpendicular_word_multipliers_strip: &'a [i8], @@ -1163,7 +1257,10 @@ fn gen_place_placements<'a, PossibleStripPlacementCallbackType: FnMut(i8, i8, i8 } else if this_cross_bits != 1 { // something hooks here and there is a valid letter. // this_cross_bits has bit 1 set, so blank is always allowed. - let matching_bits = this_cross_bits & rack_bits; + let matching_bits = this_cross_bits + & rack_bits + & env.params.left_extension_strip[idx as usize] + & env.params.right_extension_strip[idx as usize]; if matching_bits == 0 { break; } @@ -1268,7 +1365,10 @@ fn gen_place_placements<'a, PossibleStripPlacementCallbackType: FnMut(i8, i8, i8 } else if this_cross_bits != 1 { // something hooks here and there is a valid letter. // this_cross_bits has bit 1 set, so blank is always allowed. - let matching_bits = this_cross_bits & rack_bits; + let matching_bits = this_cross_bits + & rack_bits + & env.params.left_extension_strip[idx as usize] + & env.params.right_extension_strip[idx as usize]; if matching_bits == 0 { break; } @@ -1336,7 +1436,10 @@ fn gen_place_placements<'a, PossibleStripPlacementCallbackType: FnMut(i8, i8, i8 env.rightmost, f32::INFINITY, ); - } else { + } else if (env.params.left_extension_strip[env.anchor as usize] + | env.params.right_extension_strip[env.anchor as usize]) + != 0 + { env.best_possible_equity = f32::NEG_INFINITY; shadow_play_left( env, @@ -1420,6 +1523,8 @@ struct GenPlaceMovesParams<'a, CallbackType: FnMut(i8, &[u8], i32, f32), N: kwg: board_snapshot: &'a BoardSnapshot<'a, N, L>, board_strip: &'a [u8], cross_set_strip: &'a [CrossSet], + left_extension_strip: &'a [u64], + right_extension_strip: &'a [u64], remaining_word_multipliers_strip: &'a [i8], remaining_tile_multipliers_strip: &'a [i8], face_value_scores_strip: &'a [i8], @@ -1552,6 +1657,11 @@ fn gen_classic_place_moves< this_cross_bits = !1; is_unique = true; }; + // Filter by left extension set (tiles to the right the traversal hasn't seen). + this_cross_bits &= env.params.left_extension_strip[idx as usize]; + if this_cross_bits == 0 { + return; + } let new_word_multiplier = acc.word_multiplier * env.params.remaining_word_multipliers_strip[idx as usize] as i32; let tile_multiplier = env.params.remaining_tile_multipliers_strip[idx as usize]; @@ -1674,6 +1784,11 @@ fn gen_classic_place_moves< this_cross_bits = !1; is_unique = true; } + // Filter by right extension set (tiles to the left the traversal hasn't seen). + this_cross_bits &= env.params.right_extension_strip[idx as usize]; + if this_cross_bits == 0 { + return; + } let new_word_multiplier = acc.word_multiplier * env.params.remaining_word_multipliers_strip[idx as usize] as i32; let tile_multiplier = env.params.remaining_tile_multipliers_strip[idx as usize]; @@ -2131,6 +2246,20 @@ fn gen_place_moves_at< } else { &working_buffer.cross_set_for_across_plays[strip_range_start..strip_range_end] }, + left_extension_strip: if placement.down { + &working_buffer.left_extension_set_for_down_plays + [strip_range_start..strip_range_end] + } else { + &working_buffer.left_extension_set_for_across_plays + [strip_range_start..strip_range_end] + }, + right_extension_strip: if placement.down { + &working_buffer.right_extension_set_for_down_plays + [strip_range_start..strip_range_end] + } else { + &working_buffer.right_extension_set_for_across_plays + [strip_range_start..strip_range_end] + }, remaining_word_multipliers_strip: if placement.down { &working_buffer.remaining_word_multipliers_for_down_plays [strip_range_start..strip_range_end] @@ -2922,6 +3051,46 @@ fn kurnia_gen_place_moves_iter< working_buffer.cross_set_for_across_plays[dim.at_row_col(star_row, star_col)] = CrossSet { bits: !1, score: 0 }; } + // extension sets: per strip in the play direction + if matches!( + board_snapshot.game_config.game_rules(), + game_config::GameRules::Classic + ) { + working_buffer + .left_extension_set_for_across_plays + .fill(!0u64); + working_buffer + .right_extension_set_for_across_plays + .fill(!0u64); + working_buffer.left_extension_set_for_down_plays.fill(!0u64); + working_buffer + .right_extension_set_for_down_plays + .fill(!0u64); + for row in 0..dim.rows { + let strip_range_start = (row as isize * dim.cols as isize) as usize; + let strip_range_end = strip_range_start + dim.cols as usize; + gen_extension_sets( + board_snapshot.kwg, + &board_snapshot.board_tiles[strip_range_start..strip_range_end], + &mut working_buffer.left_extension_set_for_across_plays + [strip_range_start..strip_range_end], + &mut working_buffer.right_extension_set_for_across_plays + [strip_range_start..strip_range_end], + ); + } + for col in 0..dim.cols { + let strip_range_start = (col as isize * dim.rows as isize) as usize; + let strip_range_end = strip_range_start + dim.rows as usize; + gen_extension_sets( + board_snapshot.kwg, + &working_buffer.transposed_board_tiles[strip_range_start..strip_range_end], + &mut working_buffer.left_extension_set_for_down_plays + [strip_range_start..strip_range_end], + &mut working_buffer.right_extension_set_for_down_plays + [strip_range_start..strip_range_end], + ); + } + } working_buffer.init_after_cross_sets(board_snapshot); let mut found_placements = std::mem::take(&mut working_buffer.found_placements); found_placements.clear(); @@ -2939,6 +3108,10 @@ fn kurnia_gen_place_moves_iter< [strip_range_start..strip_range_end], // repurpose cross_set_strip: &working_buffer.cross_set_for_across_plays [strip_range_start..strip_range_end], + left_extension_strip: &working_buffer.left_extension_set_for_across_plays + [strip_range_start..strip_range_end], + right_extension_strip: &working_buffer.right_extension_set_for_across_plays + [strip_range_start..strip_range_end], remaining_word_multipliers_strip: &working_buffer .remaining_word_multipliers_for_across_plays [strip_range_start..strip_range_end], @@ -2992,6 +3165,10 @@ fn kurnia_gen_place_moves_iter< [strip_range_start..strip_range_end], // repurpose cross_set_strip: &working_buffer.cross_set_for_down_plays [strip_range_start..strip_range_end], + left_extension_strip: &working_buffer.left_extension_set_for_down_plays + [strip_range_start..strip_range_end], + right_extension_strip: &working_buffer.right_extension_set_for_down_plays + [strip_range_start..strip_range_end], remaining_word_multipliers_strip: &working_buffer .remaining_word_multipliers_for_down_plays[strip_range_start..strip_range_end], remaining_tile_multipliers_strip: &working_buffer