Hide keyboard shortcuts

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 

2 

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 

7 

8 

9class DiscardOption: 

10 DORA_VALUE = 10000 

11 DORA_FIRST_NEIGHBOUR = 1000 

12 DORA_SECOND_NEIGHBOUR = 100 

13 

14 UKEIRE_FIRST_FILTER_PERCENTAGE = 20 

15 UKEIRE_SECOND_FILTER_PERCENTAGE = 25 

16 UKEIRE_DANGER_FILTER_PERCENTAGE = 10 

17 

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 

22 

23 player = None 

24 

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 

52 

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 

69 

70 self.calculate_valuation() 

71 

72 @property 

73 def tile_to_discard_34(self): 

74 return self.tile_to_discard_136 // 4 

75 

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 

106 

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 

111 

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] 

127 

128 simplified_tile = simplify(self.tile_to_discard_34) 

129 value += suit_tile_grades[simplified_tile] 

130 

131 for indicator in self.player.table.dora_indicators: 

132 indicator_34 = indicator // 4 

133 if is_honor(indicator_34): 

134 continue 

135 

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 

139 

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 

143 

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 

147 

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 

153 

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 

157 

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 

161 

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 ) 

165 

166 self.count_of_dora = count_of_dora 

167 value += count_of_dora * DiscardOption.DORA_VALUE 

168 

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] 

174 

175 value = (value * discard_percentage[discarded_tiles]) / 100 

176 

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 

181 

182 self.valuation = int(value)