Skip to content

Commit acb3825

Browse files
authored
Merge pull request #174 from smly/fix/feat-shanten
fix(shanten): correct ukeire over-counting by subtracting hand tile copies
2 parents 4db06f7 + e541a0c commit acb3825

1 file changed

Lines changed: 141 additions & 20 deletions

File tree

riichienv-core/src/shanten.rs

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,17 @@ pub fn calculate_effective_tiles(hand_tiles: &[u32]) -> u32 {
266266
let current_shanten = calculate_shanten(hand_tiles);
267267
let mut effective_count = 0;
268268

269-
for tile_type in 0..34 {
270-
let count_in_hand = hand_tiles.iter().filter(|&&t| (t / 4) == tile_type).count();
271-
if count_in_hand >= 4 {
269+
let mut hand_counts = [0u8; TILE_MAX];
270+
for &tile in hand_tiles {
271+
let tile_type = (tile / 4) as usize;
272+
if tile_type < TILE_MAX {
273+
hand_counts[tile_type] += 1;
274+
}
275+
}
276+
277+
for tile_type in 0..34u32 {
278+
// skip: already holding all 4 copies
279+
if hand_counts[tile_type as usize] >= 4 {
272280
continue;
273281
}
274282

@@ -299,19 +307,25 @@ pub fn calculate_best_ukeire(hand_tiles: &[u32], visible_tiles: &[u32]) -> u32 {
299307

300308
let current_shanten = calculate_shanten(hand_tiles);
301309

310+
let mut base_counts = [0u8; TILE_MAX];
311+
for &tile in hand_tiles {
312+
let tile_type = (tile / 4) as usize;
313+
if tile_type < TILE_MAX {
314+
base_counts[tile_type] += 1;
315+
}
316+
}
317+
302318
for (idx, _) in hand_tiles.iter().enumerate() {
303319
let new_hand: Vec<u32> = hand_tiles
304320
.iter()
305321
.enumerate()
306322
.filter(|(i, _)| *i != idx)
307323
.map(|(_, &t)| t)
308324
.collect();
309-
let mut new_hand_counts = [0u8; TILE_MAX];
310-
for &tile in &new_hand {
311-
let tile_type = (tile / 4) as usize;
312-
if tile_type < TILE_MAX {
313-
new_hand_counts[tile_type] += 1;
314-
}
325+
let removed_type = (hand_tiles[idx] / 4) as usize;
326+
let mut new_hand_counts = base_counts;
327+
if removed_type < TILE_MAX {
328+
new_hand_counts[removed_type] -= 1;
315329
}
316330

317331
let new_shanten = calculate_shanten(&new_hand);
@@ -320,7 +334,8 @@ pub fn calculate_best_ukeire(hand_tiles: &[u32], visible_tiles: &[u32]) -> u32 {
320334
}
321335

322336
let mut ukeire = 0;
323-
for tile_type in 0..34 {
337+
for tile_type in 0..34u32 {
338+
// skip: already holding all 4 copies
324339
if new_hand_counts[tile_type as usize] >= 4 {
325340
continue;
326341
}
@@ -330,7 +345,9 @@ pub fn calculate_best_ukeire(hand_tiles: &[u32], visible_tiles: &[u32]) -> u32 {
330345
let test_shanten = calculate_shanten(&test_hand);
331346

332347
if test_shanten < new_shanten {
333-
ukeire += 4 - visible_counts[tile_type as usize];
348+
ukeire += 4
349+
- visible_counts[tile_type as usize]
350+
- new_hand_counts[tile_type as usize] as u32;
334351
}
335352
}
336353

@@ -437,9 +454,17 @@ pub fn calculate_effective_tiles_3p(hand_tiles: &[u32]) -> u32 {
437454
let current_shanten = calculate_shanten_3p(hand_tiles);
438455
let mut effective_count = 0;
439456

457+
let mut hand_counts = [0u8; TILE_MAX];
458+
for &tile in hand_tiles {
459+
let tile_type = (tile / 4) as usize;
460+
if tile_type < TILE_MAX {
461+
hand_counts[tile_type] += 1;
462+
}
463+
}
464+
440465
for &tile_type in &SANMA_VALID_TILE_TYPES {
441-
let count_in_hand = hand_tiles.iter().filter(|&&t| (t / 4) == tile_type).count();
442-
if count_in_hand >= 4 {
466+
// skip: already holding all 4 copies
467+
if hand_counts[tile_type as usize] >= 4 {
443468
continue;
444469
}
445470

@@ -470,19 +495,25 @@ pub fn calculate_best_ukeire_3p(hand_tiles: &[u32], visible_tiles: &[u32]) -> u3
470495

471496
let current_shanten = calculate_shanten_3p(hand_tiles);
472497

498+
let mut base_counts = [0u8; TILE_MAX];
499+
for &tile in hand_tiles {
500+
let tile_type = (tile / 4) as usize;
501+
if tile_type < TILE_MAX {
502+
base_counts[tile_type] += 1;
503+
}
504+
}
505+
473506
for (idx, _) in hand_tiles.iter().enumerate() {
474507
let new_hand: Vec<u32> = hand_tiles
475508
.iter()
476509
.enumerate()
477510
.filter(|(i, _)| *i != idx)
478511
.map(|(_, &t)| t)
479512
.collect();
480-
let mut new_hand_counts = [0u8; TILE_MAX];
481-
for &tile in &new_hand {
482-
let tile_type = (tile / 4) as usize;
483-
if tile_type < TILE_MAX {
484-
new_hand_counts[tile_type] += 1;
485-
}
513+
let removed_type = (hand_tiles[idx] / 4) as usize;
514+
let mut new_hand_counts = base_counts;
515+
if removed_type < TILE_MAX {
516+
new_hand_counts[removed_type] -= 1;
486517
}
487518

488519
let new_shanten = calculate_shanten_3p(&new_hand);
@@ -492,6 +523,7 @@ pub fn calculate_best_ukeire_3p(hand_tiles: &[u32], visible_tiles: &[u32]) -> u3
492523

493524
let mut ukeire = 0;
494525
for &tile_type in &SANMA_VALID_TILE_TYPES {
526+
// skip: already holding all 4 copies
495527
if new_hand_counts[tile_type as usize] >= 4 {
496528
continue;
497529
}
@@ -501,7 +533,9 @@ pub fn calculate_best_ukeire_3p(hand_tiles: &[u32], visible_tiles: &[u32]) -> u3
501533
let test_shanten = calculate_shanten_3p(&test_hand);
502534

503535
if test_shanten < new_shanten {
504-
ukeire += 4 - visible_counts[tile_type as usize];
536+
ukeire += 4
537+
- visible_counts[tile_type as usize]
538+
- new_hand_counts[tile_type as usize] as u32;
505539
}
506540
}
507541

@@ -510,3 +544,90 @@ pub fn calculate_best_ukeire_3p(hand_tiles: &[u32], visible_tiles: &[u32]) -> u3
510544

511545
max_ukeire
512546
}
547+
548+
#[cfg(all(test, feature = "python"))]
549+
mod tests {
550+
use super::*;
551+
552+
/// Helper: build tile IDs from tile-type indices (each using instance 0).
553+
fn tiles_from_types(types: &[u32]) -> Vec<u32> {
554+
types.iter().map(|&t| t * 4).collect()
555+
}
556+
557+
#[test]
558+
fn test_best_ukeire_subtracts_hand_counts() {
559+
// 123m 456m 789m 12p 112z (14 tiles, shanten=0).
560+
// Best discard: 2z → 123m 456m 789m 12p 11z (tenpai, waiting 3p).
561+
// Draw 3p: remaining = 4 - 0(visible) - 0(hand) = 4.
562+
// Total ukeire = 4.
563+
let hand = tiles_from_types(&[
564+
0, 1, 2, // 123m
565+
3, 4, 5, // 456m
566+
6, 7, 8, // 789m
567+
9, 10, // 12p
568+
27, 27, 28, // 112z
569+
]);
570+
let visible: Vec<u32> = vec![];
571+
572+
let ukeire = calculate_best_ukeire(&hand, &visible);
573+
assert_eq!(ukeire, 4, "tenpai waiting on 3p should have ukeire=4");
574+
}
575+
576+
#[test]
577+
fn test_best_ukeire_3p_subtracts_hand_counts() {
578+
// 123p 456p 789p 12s 112z (14 tiles, shanten=0).
579+
// Best discard: 2z → 123p 456p 789p 12s 11z (tenpai, waiting 3s).
580+
// Draw 3s: remaining = 4 - 0(visible) - 0(hand) = 4.
581+
let hand = tiles_from_types(&[
582+
9, 10, 11, // 123p
583+
12, 13, 14, // 456p
584+
15, 16, 17, // 789p
585+
18, 19, // 12s
586+
27, 27, 28, // 112z
587+
]);
588+
let visible: Vec<u32> = vec![];
589+
590+
let ukeire = calculate_best_ukeire_3p(&hand, &visible);
591+
assert_eq!(ukeire, 4, "tenpai waiting on 3s should have ukeire=4");
592+
}
593+
594+
#[test]
595+
fn test_best_ukeire_hand_copies_reduce_remaining() {
596+
// 123m 456m 789m 33p 332z (14 tiles).
597+
// Discard 2z → 123m 456m 789m 33p 33z (13 tiles, tenpai).
598+
// Winning draw: 3p → 333p as koutsu, or 3z → 333z as koutsu.
599+
// 3p in hand: 2 copies → remaining = 4 - 0 - 2 = 2
600+
// 3z in hand: 2 copies → remaining = 4 - 0 - 2 = 2
601+
// Total ukeire = 4 (not 8 as old code would compute).
602+
let hand = tiles_from_types(&[
603+
0, 1, 2, // 123m
604+
3, 4, 5, // 456m
605+
6, 7, 8, // 789m
606+
11, 11, // 33p
607+
29, 29, 28, // 332z
608+
]);
609+
let visible: Vec<u32> = vec![];
610+
611+
let ukeire = calculate_best_ukeire(&hand, &visible);
612+
assert_eq!(ukeire, 4, "hand copies should reduce remaining count");
613+
}
614+
615+
#[test]
616+
fn test_best_ukeire_visible_and_hand_both_subtracted() {
617+
// 123m 456m 789m 33p 332z (14 tiles).
618+
// 1 copy of 3p visible → 3p remaining = 4 - 1 - 2 = 1
619+
// 3z remaining = 4 - 0 - 2 = 2
620+
// Total ukeire = 3.
621+
let hand = tiles_from_types(&[
622+
0, 1, 2, // 123m
623+
3, 4, 5, // 456m
624+
6, 7, 8, // 789m
625+
11, 11, // 33p
626+
29, 29, 28, // 332z
627+
]);
628+
let visible = vec![11 * 4 + 2]; // 3p instance 2
629+
630+
let ukeire = calculate_best_ukeire(&hand, &visible);
631+
assert_eq!(ukeire, 3, "visible + hand copies should both reduce count");
632+
}
633+
}

0 commit comments

Comments
 (0)