Coverage for project/game/ai/discard.py : 100%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from typing import List
3from game.ai.helpers.defence import TileDangerHandler
4from game.ai.strategies.main import BaseStrategy
5from mahjong.tile import TilesConverter
6from mahjong.utils import is_honor, is_man, is_pin, is_sou, plus_dora, simplify
9class DiscardOption:
10 DORA_VALUE = 10000
11 DORA_FIRST_NEIGHBOUR = 1000
12 DORA_SECOND_NEIGHBOUR = 100
14 UKEIRE_FIRST_FILTER_PERCENTAGE = 20
15 UKEIRE_SECOND_FILTER_PERCENTAGE = 25
16 UKEIRE_DANGER_FILTER_PERCENTAGE = 10
18 MIN_UKEIRE_DANGER_BORDER = 2
19 MIN_UKEIRE_TEMPAI_BORDER = 2
20 MIN_UKEIRE_SHANTEN_1_BORDER = 4
21 MIN_UKEIRE_SHANTEN_2_BORDER = 8
23 player = None
25 # in 136 tile format
26 tile_to_discard_136 = None
27 # are we calling riichi on this tile or not
28 with_riichi = None
29 # array of tiles that will improve our hand
30 waiting: List[int] = None
31 # how much tiles will improve our hand
32 ukeire = None
33 ukeire_second = None
34 # number of shanten for that tile
35 shanten = None
36 # sometimes we had to force tile to be discarded
37 had_to_be_discarded = False
38 # calculated tile value, for sorting
39 valuation = None
40 # how danger this tile is
41 danger = None
42 # wait to ukeire map
43 wait_to_ukeire = None
44 # second level cost approximation for 1-shanten hands
45 second_level_cost = None
46 # second level average number of waits approximation for 1-shanten hands
47 average_second_level_waits = None
48 # second level average cost approximation for 1-shanten hands
49 average_second_level_cost = None
50 # special descriptor for tempai with additional info
51 tempai_descriptor = None
53 def __init__(self, player, tile_to_discard_136, shanten, waiting, ukeire, wait_to_ukeire=None):
54 self.player = player
55 self.tile_to_discard_136 = tile_to_discard_136
56 self.with_riichi = False
57 self.shanten = shanten
58 self.waiting = waiting
59 self.ukeire = ukeire
60 self.ukeire_second = 0
61 self.count_of_dora = 0
62 self.danger = TileDangerHandler()
63 self.had_to_be_discarded = False
64 self.wait_to_ukeire = wait_to_ukeire
65 self.second_level_cost = 0
66 self.average_second_level_waits = 0
67 self.average_second_level_cost = 0
68 self.tempai_descriptor = None
70 self.calculate_valuation()
72 @property
73 def tile_to_discard_34(self):
74 return self.tile_to_discard_136 // 4
76 def serialize(self):
77 data = {
78 "tile": TilesConverter.to_one_line_string(
79 [self.tile_to_discard_136], print_aka_dora=self.player.table.has_aka_dora
80 ),
81 "shanten": self.shanten,
82 "ukeire": self.ukeire,
83 "valuation": self.valuation,
84 "danger": {
85 "max_danger": self.danger.get_max_danger(),
86 "sum_danger": self.danger.get_sum_danger(),
87 "weighted_danger": self.danger.get_weighted_danger(),
88 "min_border": self.danger.get_min_danger_border(),
89 "danger_border": self.danger.danger_border,
90 "weighted_cost": self.danger.weighted_cost,
91 "danger_reasons": self.danger.values,
92 "can_be_used_for_ryanmen": self.danger.can_be_used_for_ryanmen,
93 },
94 }
95 if self.shanten == 0:
96 data["with_riichi"] = self.with_riichi
97 if self.ukeire_second:
98 data["ukeire2"] = self.ukeire_second
99 if self.average_second_level_waits:
100 data["average_second_level_waits"] = self.average_second_level_waits
101 if self.average_second_level_cost:
102 data["average_second_level_cost"] = self.average_second_level_cost
103 if self.had_to_be_discarded:
104 data["had_to_be_discarded"] = self.had_to_be_discarded
105 return data
107 def calculate_valuation(self):
108 # base is 100 for ability to mark tiles as not needed (like set value to 50)
109 value = 100
110 honored_value = 20
112 if is_honor(self.tile_to_discard_34):
113 if self.tile_to_discard_34 in self.player.valued_honors:
114 count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard_34]
115 # for west-west, east-east we had to double tile value
116 value += honored_value * len(count_of_winds)
117 else:
118 # aim for tanyao
119 if (
120 self.player.ai.open_hand_handler.current_strategy
121 and self.player.ai.open_hand_handler.current_strategy.type == BaseStrategy.TANYAO
122 ):
123 suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10]
124 # usual hand
125 else:
126 suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10]
128 simplified_tile = simplify(self.tile_to_discard_34)
129 value += suit_tile_grades[simplified_tile]
131 for indicator in self.player.table.dora_indicators:
132 indicator_34 = indicator // 4
133 if is_honor(indicator_34):
134 continue
136 # indicator and tile not from the same suit
137 if is_sou(indicator_34) and not is_sou(self.tile_to_discard_34):
138 continue
140 # indicator and tile not from the same suit
141 if is_man(indicator_34) and not is_man(self.tile_to_discard_34):
142 continue
144 # indicator and tile not from the same suit
145 if is_pin(indicator_34) and not is_pin(self.tile_to_discard_34):
146 continue
148 simplified_indicator = simplify(indicator_34)
149 simplified_dora = simplified_indicator + 1
150 # indicator is 9 man
151 if simplified_dora == 9:
152 simplified_dora = 0
154 # tile so close to the dora
155 if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora:
156 value += DiscardOption.DORA_FIRST_NEIGHBOUR
158 # tile not far away from dora
159 if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora:
160 value += DiscardOption.DORA_SECOND_NEIGHBOUR
162 count_of_dora = plus_dora(
163 self.tile_to_discard_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora
164 )
166 self.count_of_dora = count_of_dora
167 value += count_of_dora * DiscardOption.DORA_VALUE
169 if is_honor(self.tile_to_discard_34):
170 # depends on how much honor tiles were discarded
171 # we will decrease tile value
172 discard_percentage = [100, 75, 20, 0, 0]
173 discarded_tiles = self.player.table.revealed_tiles[self.tile_to_discard_34]
175 value = (value * discard_percentage[discarded_tiles]) / 100
177 # three honor tiles were discarded,
178 # so we don't need this tile anymore
179 if value == 0:
180 self.had_to_be_discarded = True
182 self.valuation = int(value)