Skip to content
This repository was archived by the owner on Jul 8, 2023. It is now read-only.

Commit 33b663a

Browse files
authored
Merge pull request #111 from MahjongRepository/ukeire2
Fixes for second level ukeire
2 parents 9723dac + bb9a19d commit 33b663a

File tree

21 files changed

+374
-137
lines changed

21 files changed

+374
-137
lines changed

project/game/ai/discard.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class DiscardOption(object):
3232
danger = None
3333
# wait to ukeire map
3434
wait_to_ukeire = None
35+
# second level cost approximation for 1-shanten hands
36+
second_level_cost = None
3537

3638
def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100, wait_to_ukeire=None):
3739
"""

project/game/ai/first_version/defence/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def should_go_to_defence_mode(self, discard_candidate=None):
4848
shanten = self.player.ai.shanten
4949
waiting = self.player.ai.waiting
5050

51+
if not waiting:
52+
waiting = []
53+
5154
# if we are in riichi, we can't defence
5255
if self.player.in_riichi:
5356
return False

project/game/ai/first_version/defence/suji.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: utf-8 -*-
22
from mahjong.utils import is_man, simplify, is_pin, is_sou, plus_dora, is_aka_dora
3-
from mahjong.tile import TilesConverter
43

54
from game.ai.first_version.defence.defence import Defence, DefenceTile
65

project/game/ai/first_version/hand_builder.py

Lines changed: 128 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import copy
2-
31
from mahjong.constants import AKA_DORA_LIST
42
from mahjong.shanten import Shanten
53
from 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

96
import utils.decisions_constants as log
107
from game.ai.discard import DiscardOption
11-
from utils.decisions_logger import DecisionsLogger
12-
138
from game.ai.first_version.defence.kabe import KabeTile
9+
from utils.decisions_logger import DecisionsLogger
1410

1511

1612
class 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

project/game/ai/first_version/main.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,9 @@ def init_hand(self):
8181
'Hand: {}'.format(self.player.format_hand_for_print()),
8282
])
8383

84-
# it will set correct hand shanten number and ukeire to the new hand
85-
# tile will not be removed from the hand
86-
self.discard_tile(None, print_log=False)
87-
self.player.in_tempai = False
88-
89-
# Let's decide what we will do with our hand (like open for tanyao and etc.)
90-
self.determine_strategy(self.player.tiles)
84+
self.shanten = self.shanten_calculator.calculate_shanten(
85+
TilesConverter.to_34_array(self.player.tiles)
86+
)
9187

9288
def draw_tile(self, tile_136):
9389
self.determine_strategy(self.player.tiles)

project/game/ai/first_version/riichi.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from mahjong.tile import TilesConverter
22
from mahjong.utils import is_honor, simplify, is_pair, is_chi
33

4-
from game.ai.first_version.defence.kabe import KabeTile
5-
64

75
class Riichi:
86

project/game/ai/first_version/strategies/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,6 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discar
247247
for meld_34 in possible_melds:
248248
meld_34_copy = meld_34.copy()
249249
closed_hand_copy = closed_hand.copy()
250-
open_sets_34 = self.player.meld_34_tiles + [meld_34]
251250

252251
meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
253252
meld_34_copy.remove(discarded_tile_34)

project/game/ai/first_version/strategies/tanyao.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,13 @@ def determine_what_to_discard(self, discard_options, hand, open_melds):
140140
continue
141141

142142
# there is no sense to wait 1-4 if we have open hand
143-
all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting])
144-
if all_waiting_are_fine:
145-
results.append(item)
143+
# but let's only avoid atodzuke tiles in tempai, the rest will be dealt with in
144+
# generic logic
145+
if item.shanten == 0:
146+
all_waiting_are_fine = all(
147+
[(self.is_tile_suitable(x * 4) or item.wait_to_ukeire[x] == 0) for x in item.waiting])
148+
if all_waiting_are_fine:
149+
results.append(item)
146150

147151
if not_suitable_tiles:
148152
return not_suitable_tiles

0 commit comments

Comments
 (0)