1- import copy
2-
31from mahjong .constants import AKA_DORA_LIST
42from mahjong .shanten import Shanten
53from mahjong .tile import TilesConverter , Tile
6- from mahjong .utils import is_tile_strictly_isolated , is_pair , is_honor , simplify , is_chi
7- from mahjong .meld import Meld
4+ from mahjong .utils import is_tile_strictly_isolated , is_pair , is_honor , simplify
85
96import utils .decisions_constants as log
107from game .ai .discard import DiscardOption
11- from utils .decisions_logger import DecisionsLogger
12-
138from game .ai .first_version .defence .kabe import KabeTile
9+ from utils .decisions_logger import DecisionsLogger
1410
1511
1612class HandBuilder :
@@ -202,11 +198,7 @@ def find_discard_options(self, tiles, closed_hand, melds=None):
202198
203199 def count_tiles (self , waiting , tiles_34 ):
204200 n = 0
205- not_suitable_tiles = self .ai .current_strategy and self .ai .current_strategy .not_suitable_tiles or []
206201 for tile_34 in waiting :
207- if self .player .is_open_hand and tile_34 in not_suitable_tiles :
208- continue
209-
210202 n += 4 - self .player .total_tiles (tile_34 , tiles_34 )
211203 return n
212204
@@ -304,15 +296,19 @@ def _choose_best_tanki_wait(self, discard_desc):
304296 # if everything is the same we just choose the first one
305297 return best_discard_desc [0 ]['discard_option' ]
306298
307- def _is_furiten (self , tile_34 ):
299+ def _is_waiting_furiten (self , tile_34 ):
308300 discarded_tiles = [x .value // 4 for x in self .player .discards ]
309301 return tile_34 in discarded_tiles
310302
311- def _choose_best_discard_in_tempai (self , tiles , melds , discard_options ):
312- # only 1 option, nothing to choose
313- if len (discard_options ) == 1 :
314- return discard_options [0 ]
303+ def _is_discard_option_furiten (self , discard_option ):
304+ is_furiten = False
305+
306+ for waiting in discard_option .waiting :
307+ is_furiten = is_furiten or self ._is_waiting_furiten (waiting )
315308
309+ return is_furiten
310+
311+ def _choose_best_discard_in_tempai (self , tiles , melds , discard_options ):
316312 # first of all we find tiles that have the best hand cost * ukeire value
317313 call_riichi = not self .player .is_open_hand
318314
@@ -333,31 +329,12 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
333329 discarded_tile = Tile (tile , False )
334330 self .player .discards .append (discarded_tile )
335331
336- hand_cost = 0
332+ is_furiten = self ._is_discard_option_furiten (discard_option )
333+
337334 if len (discard_option .waiting ) == 1 :
338335 waiting = discard_option .waiting [0 ]
339- is_furiten = self ._is_furiten (waiting )
340336
341- hand_cost_tsumo = 0
342- cost_x_ukeire_tsumo = 0
343- hand_value = self .player .ai .estimate_hand_value (waiting , call_riichi = call_riichi , is_tsumo = True )
344- if hand_value .error is None :
345- hand_cost_tsumo = hand_value .cost ['main' ] + 2 * hand_value .cost ['additional' ]
346- cost_x_ukeire_tsumo = hand_cost_tsumo * discard_option .ukeire
347-
348- hand_cost_ron = 0
349- cost_x_ukeire_ron = 0
350- if not is_furiten :
351- hand_value = self .player .ai .estimate_hand_value (waiting , call_riichi = call_riichi , is_tsumo = False )
352- if hand_value .error is None :
353- hand_cost_ron = hand_value .cost ['main' ]
354- cost_x_ukeire_ron = hand_cost_ron * discard_option .ukeire
355-
356- # these are abstract numbers used to compare different waits
357- # some don't have yaku, some furiten, etc.
358- # so we use an abstract formula of 1 tsumo cost + 3 ron costs
359- hand_cost = hand_cost_tsumo + 3 * hand_cost_ron
360- cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
337+ cost_x_ukeire , hand_cost = self ._estimate_cost_x_ukeire (discard_option , call_riichi )
361338
362339 # let's check if this is a tanki wait
363340 results , tiles_34 = self .divide_hand (self .player .tiles , waiting )
@@ -416,30 +393,7 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
416393 'tanki_type' : tanki_type
417394 })
418395 else :
419- cost_x_ukeire_tsumo = 0
420- cost_x_ukeire_ron = 0
421- is_furiten = False
422-
423- for waiting in discard_option .waiting :
424- is_furiten = is_furiten or self ._is_furiten (waiting )
425-
426- for waiting in discard_option .waiting :
427- hand_value = self .player .ai .estimate_hand_value (waiting ,
428- call_riichi = call_riichi ,
429- is_tsumo = True )
430- if hand_value .error is None :
431- cost_x_ukeire_tsumo += (hand_value .cost ['main' ]
432- + 2 * hand_value .cost ['additional' ]
433- ) * discard_option .wait_to_ukeire [waiting ]
434-
435- if not is_furiten :
436- hand_value = self .player .ai .estimate_hand_value (waiting ,
437- call_riichi = call_riichi ,
438- is_tsumo = False )
439- if hand_value .error is None :
440- cost_x_ukeire_ron += hand_value .cost ['main' ] * discard_option .wait_to_ukeire [waiting ]
441-
442- cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
396+ cost_x_ukeire , _ = self ._estimate_cost_x_ukeire (discard_option , call_riichi )
443397
444398 discard_desc .append ({
445399 'discard_option' : discard_option ,
@@ -544,6 +498,10 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
544498 ukeire_field = 'ukeire'
545499 possible_options = sorted (possible_options , key = lambda x : - getattr (x , ukeire_field ))
546500
501+ # only one option - so we choose it
502+ if len (possible_options ) == 1 :
503+ return possible_options [0 ]
504+
547505 # tempai state has a special handling
548506 if first_option .shanten == 0 :
549507 other_tiles_with_same_shanten = [x for x in possible_options if x .shanten == 0 ]
@@ -564,8 +522,16 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
564522
565523 return sorted (min_dora_list , key = lambda x : - getattr (x , ukeire_field ))[0 ]
566524
567- # we filter 10% of options here
525+ # only one option - so we choose it
526+ if len (tiles_without_dora ) == 1 :
527+ return tiles_without_dora [0 ]
528+
529+ # 1-shanten hands have special handling - we can consider future hand cost here
530+ if first_option .shanten == 1 :
531+ return sorted (tiles_without_dora , key = lambda x : (- x .second_level_cost , - x .ukeire_second , x .valuation ))[0 ]
532+
568533 if first_option .shanten == 2 or first_option .shanten == 3 :
534+ # we filter 10% of options here
569535 second_filter_percentage = 10
570536 filtered_options = self ._filter_list_by_percentage (
571537 tiles_without_dora ,
@@ -635,38 +601,89 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals
635601 return discard_option .find_tile_in_hand (closed_hand )
636602
637603 def calculate_second_level_ukeire (self , discard_option ):
638- closed_hand_34 = TilesConverter .to_34_array (self .player .closed_hand )
639604 not_suitable_tiles = self .ai .current_strategy and self .ai .current_strategy .not_suitable_tiles or []
605+ call_riichi = not self .player .is_open_hand
640606
641- tiles = copy .copy (self .player .tiles )
642- tiles .remove (discard_option .find_tile_in_hand (self .player .closed_hand ))
607+ # we are going to do manipulations that require player hand to be updated
608+ # so we save original tiles here and restore it at the end of the function
609+ player_tiles_original = self .player .tiles .copy ()
610+
611+ tile_in_hand = discard_option .find_tile_in_hand (self .player .closed_hand )
612+ self .player .tiles .remove (tile_in_hand )
643613
644614 sum_tiles = 0
615+ sum_cost = 0
645616 for wait_34 in discard_option .waiting :
646617 if self .player .is_open_hand and wait_34 in not_suitable_tiles :
647618 continue
648619
620+ closed_hand_34 = TilesConverter .to_34_array (self .player .closed_hand )
621+ live_tiles = 4 - self .player .total_tiles (wait_34 , closed_hand_34 )
622+
623+ if live_tiles == 0 :
624+ continue
625+
649626 wait_136 = wait_34 * 4
650- tiles .append (wait_136 )
627+ self . player . tiles .append (wait_136 )
651628
652629 results , shanten = self .find_discard_options (
653- tiles ,
630+ self . player . tiles ,
654631 self .player .closed_hand ,
655632 self .player .melds
656633 )
657634 results = [x for x in results if x .shanten == discard_option .shanten - 1 ]
658635
659636 # let's take best ukeire here
660637 if results :
661- best_one = sorted (results , key = lambda x : - x .ukeire )[0 ]
662- live_tiles = 4 - self .player .total_tiles (wait_34 , closed_hand_34 )
663- sum_tiles += best_one .ukeire * live_tiles
664-
665- tiles .remove (wait_136 )
638+ result_has_atodzuke = False
639+ if self .player .is_open_hand :
640+ best_one = results [0 ]
641+ best_ukeire = 0
642+ for result in results :
643+ has_atodzuke = False
644+ ukeire = 0
645+ for wait_34 in result .waiting :
646+ if wait_34 in not_suitable_tiles :
647+ has_atodzuke = True
648+ else :
649+ ukeire += result .wait_to_ukeire [wait_34 ]
650+
651+ # let's consider atodzuke waits to be worse than non-atodzuke ones
652+ if has_atodzuke :
653+ ukeire /= 2
654+
655+ if (ukeire > best_ukeire ) or (ukeire >= best_ukeire and not has_atodzuke ):
656+ best_ukeire = ukeire
657+ best_one = result
658+ result_has_atodzuke = has_atodzuke
659+ else :
660+ best_one = sorted (results , key = lambda x : - x .ukeire )[0 ]
661+ best_ukeire = best_one .ukeire
662+
663+ sum_tiles += best_ukeire * live_tiles
664+
665+ # if we are going to have a tempai (on our second level) - let's also count its cost
666+ if shanten == 0 :
667+ next_tile_in_hand = best_one .find_tile_in_hand (self .player .closed_hand )
668+ self .player .tiles .remove (next_tile_in_hand )
669+ cost_x_ukeire , _ = self ._estimate_cost_x_ukeire (best_one , call_riichi = call_riichi )
670+ # we reduce tile valuation for atodzuke
671+ if result_has_atodzuke :
672+ cost_x_ukeire /= 2
673+ sum_cost += cost_x_ukeire
674+ self .player .tiles .append (next_tile_in_hand )
675+
676+ self .player .tiles .remove (wait_136 )
666677
667678 discard_option .ukeire_second = sum_tiles
679+ if discard_option .shanten == 1 :
680+ discard_option .second_level_cost = sum_cost
668681
669- def _filter_list_by_percentage (self , items , attribute , percentage ):
682+ # restore original state of player hand
683+ self .player .tiles = player_tiles_original
684+
685+ @staticmethod
686+ def _filter_list_by_percentage (items , attribute , percentage ):
670687 filtered_options = []
671688 first_option = items [0 ]
672689 ukeire_borders = round ((getattr (first_option , attribute ) / 100 ) * percentage )
@@ -675,7 +692,8 @@ def _filter_list_by_percentage(self, items, attribute, percentage):
675692 filtered_options .append (x )
676693 return filtered_options
677694
678- def _choose_ukeire_borders (self , first_option , border_percentage , border_field ):
695+ @staticmethod
696+ def _choose_ukeire_borders (first_option , border_percentage , border_field ):
679697 ukeire_borders = round ((getattr (first_option , border_field ) / 100 ) * border_percentage )
680698
681699 if first_option .shanten == 0 and ukeire_borders < 2 :
@@ -688,3 +706,39 @@ def _choose_ukeire_borders(self, first_option, border_percentage, border_field):
688706 ukeire_borders = 8
689707
690708 return ukeire_borders
709+
710+ def _estimate_cost_x_ukeire (self , discard_option , call_riichi ):
711+ cost_x_ukeire_tsumo = 0
712+ cost_x_ukeire_ron = 0
713+ hand_cost_tsumo = 0
714+ hand_cost_ron = 0
715+
716+ is_furiten = self ._is_discard_option_furiten (discard_option )
717+
718+ for waiting in discard_option .waiting :
719+ hand_value = self .player .ai .estimate_hand_value (waiting ,
720+ call_riichi = call_riichi ,
721+ is_tsumo = True )
722+ if hand_value .error is None :
723+ hand_cost_tsumo = hand_value .cost ['main' ] + 2 * hand_value .cost ['additional' ]
724+ cost_x_ukeire_tsumo += hand_cost_tsumo * discard_option .wait_to_ukeire [waiting ]
725+
726+ if not is_furiten :
727+ hand_value = self .player .ai .estimate_hand_value (waiting ,
728+ call_riichi = call_riichi ,
729+ is_tsumo = False )
730+ if hand_value .error is None :
731+ hand_cost_ron = hand_value .cost ['main' ]
732+ cost_x_ukeire_ron += hand_cost_ron * discard_option .wait_to_ukeire [waiting ]
733+
734+ # these are abstract numbers used to compare different waits
735+ # some don't have yaku, some furiten, etc.
736+ # so we use an abstract formula of 1 tsumo cost + 3 ron costs
737+ cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
738+
739+ if len (discard_option .waiting ) == 1 :
740+ hand_cost = hand_cost_tsumo + 3 * hand_cost_ron
741+ else :
742+ hand_cost = None
743+
744+ return cost_x_ukeire , hand_cost
0 commit comments