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

Commit dab07f4

Browse files
authored
Merge pull request #113 from MahjongRepository/chankan
Improve logic to call shouminkan sets
2 parents 68d31bb + 2765985 commit dab07f4

File tree

6 files changed

+154
-51
lines changed

6 files changed

+154
-51
lines changed

project/game/ai/base/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def should_call_riichi(self):
5858

5959
def should_call_kan(self, tile, is_open_kan, from_riichi=False):
6060
"""
61-
When bot can call kan or chankan this method will be called
61+
When bot can call kan or shouminkan this method will be called
6262
:param tile: 136 tile format
6363
:param is_open_kan: boolean
6464
:param from_riichi: boolean

project/game/ai/first_version/main.py

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -236,71 +236,83 @@ def should_call_kan(self, tile, open_kan, from_riichi=False):
236236
tiles_34 = TilesConverter.to_34_array(self.player.tiles)
237237

238238
closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
239-
pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)]
240-
241-
# let's check can we upgrade opened pon to the kan
242-
if pon_melds:
243-
for meld in pon_melds:
244-
# tile is equal to our already opened pon,
245-
# so let's call chankan!
246-
if tile_34 in meld:
247-
return Meld.CHANKAN
248239

249240
melds_34 = copy.copy(self.player.meld_34_tiles)
250241
tiles = copy.copy(self.player.tiles)
251242
closed_hand_tiles = copy.copy(self.player.closed_hand)
252243

253-
# we can try to call closed meld
254-
if closed_hand_34[tile_34] == 3:
244+
new_shanten = 0
245+
previous_shanten = 0
246+
new_waits_count = 0
247+
previous_waits_count = 0
248+
249+
# let's check can we upgrade opened pon to the kan
250+
pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)]
251+
has_shouminkan_candidate = False
252+
for meld in pon_melds:
253+
# tile is equal to our already opened pon
254+
if tile_34 in meld:
255+
has_shouminkan_candidate = True
256+
257+
tiles.append(tile)
258+
closed_hand_tiles.append(tile)
259+
260+
previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
261+
tiles,
262+
closed_hand_tiles,
263+
self.player.melds
264+
)
265+
266+
tiles_34 = TilesConverter.to_34_array(tiles)
267+
tiles_34[tile_34] -= 1
268+
269+
new_waiting, new_shanten = self.hand_builder.calculate_waits(
270+
tiles_34,
271+
self.player.meld_34_tiles
272+
)
273+
new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34)
274+
275+
if not has_shouminkan_candidate:
276+
# we don't have enough tiles in the hand
277+
if closed_hand_34[tile_34] != 3:
278+
return None
279+
255280
if open_kan or from_riichi:
256281
# this 4 tiles can only be used in kan, no other options
257282
previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
258-
previous_waits_cnt = self.hand_builder.count_tiles(previous_waiting, closed_hand_34)
259-
260-
# shanten calculator doesn't like working with kans, so we pretend it's a pon
261-
melds_34 += [[tile_34, tile_34, tile_34]]
262-
closed_hand_34[tile_34] = 0
263-
264-
new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
265-
new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34)
283+
previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34)
266284
else:
267-
# if we can use or tile in the hand for the forms other than KAN
268285
tiles.append(tile)
269-
270286
closed_hand_tiles.append(tile)
271-
closed_hand_34[tile_34] += 1
272287

273-
previous_results, previous_shanten = self.hand_builder.find_discard_options(
288+
previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
274289
tiles,
275290
closed_hand_tiles,
276291
self.player.melds
277292
)
278293

279-
previous_results = [x for x in previous_results if x.shanten == previous_shanten]
280-
281-
# it is possible that we don't have results here
282-
# when we are in agari state (but without yaku)
283-
if not previous_results:
284-
return None
294+
# shanten calculator doesn't like working with kans, so we pretend it's a pon
295+
melds_34 += [[tile_34, tile_34, tile_34]]
296+
new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
285297

286-
previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire
298+
closed_hand_34[tile_34] = 4
299+
new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34)
287300

288-
# shanten calculator doesn't like working with kans, so we pretend it's a pon
289-
closed_hand_34[tile_34] = 0
290-
melds_34 += [[tile_34, tile_34, tile_34]]
301+
# it is possible that we don't have results here
302+
# when we are in agari state (but without yaku)
303+
if previous_shanten is None:
304+
return None
291305

292-
new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
293-
new_waits_cnt = self.hand_builder.count_tiles(new_waiting, closed_hand_34)
306+
# it is not possible to reduce number of shanten by calling a kan
307+
assert new_shanten >= previous_shanten
294308

295-
# it is not possible to reduce number of shanten by calling a kan
296-
assert new_shanten >= previous_shanten
309+
# if shanten number is the same, we should only call kan if ukeire didn't become worse
310+
if new_shanten == previous_shanten:
311+
# we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall)
312+
assert new_waits_count <= previous_waits_count
297313

298-
# if shanten number is the same, we should only call kan if ukeire didn't become worse
299-
if new_shanten == previous_shanten:
300-
# we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall)
301-
assert new_waits_cnt <= previous_waits_cnt
302-
if new_waits_cnt == previous_waits_cnt:
303-
return Meld.KAN
314+
if new_waits_count == previous_waits_count:
315+
return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN
304316

305317
return None
306318

@@ -324,3 +336,21 @@ def enemy_players(self):
324336
Return list of players except our bot
325337
"""
326338
return self.player.table.players[1:]
339+
340+
def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds):
341+
previous_results, previous_shanten = self.hand_builder.find_discard_options(
342+
tiles,
343+
closed_hand_tiles,
344+
melds
345+
)
346+
347+
previous_results = [x for x in previous_results if x.shanten == previous_shanten]
348+
349+
# it is possible that we don't have results here
350+
# when we are in agari state (but without yaku)
351+
if not previous_results:
352+
return None, None
353+
354+
previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire
355+
356+
return previous_shanten, previous_waits_cnt

project/game/ai/first_version/tests/tests_ai.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def test_using_tiles_of_different_suit_for_chi(self):
395395
meld, _ = player.try_to_call_meld(tile, True)
396396
self.assertIsNotNone(meld)
397397

398-
def test_upgrade_opened_pon_to_kan(self):
398+
def test_call_upgrade_pon_and_bad_ukeire_after_call(self):
399399
table = Table()
400400
table.count_of_remaining_tiles = 10
401401

@@ -409,6 +409,57 @@ def test_upgrade_opened_pon_to_kan(self):
409409

410410
self.assertEqual(len(table.player.melds), 1)
411411
self.assertEqual(len(table.player.tiles), 13)
412+
self.assertEqual(table.player.should_call_kan(tile, False), None)
413+
414+
def test_call_upgrade_pon_and_bad_ukeire_after_call_second_case(self):
415+
table = Table()
416+
table.add_dora_indicator(self._string_to_136_tile(honors='5'))
417+
table.count_of_remaining_tiles = 10
418+
player = table.player
419+
420+
tiles = self._string_to_136_array(man='3455567', sou='222', honors='666')
421+
player.init_hand(tiles)
422+
player.add_called_meld(self._make_meld(Meld.PON, man='555'))
423+
player.add_called_meld(self._make_meld(Meld.PON, honors='666'))
424+
425+
tile = self._string_to_136_tile(man='5')
426+
427+
self.assertEqual(player.should_call_kan(tile, False), None)
428+
429+
player.draw_tile(tile)
430+
discarded_tile = player.discard_tile()
431+
432+
self.assertEqual(self._to_string([discarded_tile]), '2s')
433+
434+
def test_call_upgrade_pon_and_bad_ukeire_after_call_third_case(self):
435+
table = Table()
436+
table.count_of_remaining_tiles = 10
437+
player = table.player
438+
439+
tiles = self._string_to_136_array(man='67', pin='6', sou='1344478999')
440+
table.player.init_hand(tiles)
441+
table.player.add_called_meld(self._make_meld(Meld.PON, sou='444'))
442+
443+
tile = self._string_to_136_tile(sou='4')
444+
445+
# we don't want to call shouminkan here
446+
self.assertEqual(table.player.should_call_kan(tile, False), None)
447+
448+
player.draw_tile(tile)
449+
discarded_tile = player.discard_tile()
450+
451+
self.assertEqual(self._to_string([discarded_tile]), '6p')
452+
453+
def test_call_shouminkan(self):
454+
table = Table()
455+
table.count_of_remaining_tiles = 10
456+
457+
tiles = self._string_to_136_array(man='3455567', sou='222', honors='666')
458+
table.player.init_hand(tiles)
459+
table.player.add_called_meld(self._make_meld(Meld.PON, honors='666'))
460+
461+
tile = self._string_to_136_tile(honors='6')
462+
412463
self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN)
413464

414465
def test_call_closed_kan(self):
@@ -451,6 +502,7 @@ def test_opened_kan(self):
451502
tile = self._string_to_136_tile(sou='1')
452503
self.assertEqual(table.player.should_call_kan(tile, True), None)
453504

505+
def test_opened_kan_second_case(self):
454506
table = Table()
455507
table.count_of_remaining_tiles = 10
456508

@@ -466,6 +518,25 @@ def test_opened_kan(self):
466518
tile = self._string_to_136_tile(sou='1')
467519
self.assertEqual(table.player.should_call_kan(tile, True), Meld.KAN)
468520

521+
def test_opened_kan_third_case(self):
522+
# we are in tempai already and there was a crash on 5s meld suggestion
523+
524+
table = Table()
525+
table.count_of_remaining_tiles = 10
526+
table.add_dora_indicator(self._string_to_136_tile(honors='5'))
527+
528+
tiles = self._string_to_136_array(man='456', sou='55567678', honors='66')
529+
table.player.init_hand(tiles)
530+
table.player.add_called_meld(self._make_meld(Meld.CHI, sou='678'))
531+
532+
# to rebuild all caches
533+
table.player.draw_tile(self._string_to_136_tile(pin='9'))
534+
table.player.discard_tile()
535+
536+
tile = self._string_to_136_tile(sou='5')
537+
self.assertEqual(table.player.should_call_kan(tile, True), None)
538+
self.assertEqual(table.player.try_to_call_meld(tile, True), (None, None))
539+
469540
def test_dont_call_kan_in_defence_mode(self):
470541
table = Table()
471542

project/game/player.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ def erase_state(self):
6565
self.round_step = 0
6666

6767
def add_called_meld(self, meld: Meld):
68-
# we already added chankan as a pon set
68+
# we already added shouminkan as a pon set
6969
if meld.type == Meld.CHANKAN:
7070
tile_34 = meld.tiles[0] // 4
7171

7272
pon_set = [x for x in self.melds if x.type == Meld.PON and (x.tiles[0] // 4) == tile_34]
7373

74-
# when we are doing reconnect and we have called chankan set
74+
# when we are doing reconnect and we have called shouminkan set
7575
# we will not have called pon set in the hand
7676
if pon_set:
7777
self.melds.remove(pon_set[0])

project/game/table.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def add_called_meld(self, player_seat, meld):
108108
else:
109109
# can't have a pon or chi from the hand
110110
assert meld.type == Meld.KAN or meld.type == meld.CHANKAN
111-
# player draws additional tile from the wall in case of closed kan or chankan
111+
# player draws additional tile from the wall in case of closed kan or shouminkan
112112
self.count_of_remaining_tiles -= 1
113113

114114
self.get_player(player_seat).add_called_meld(meld)
@@ -119,7 +119,7 @@ def add_called_meld(self, player_seat, meld):
119119
if meld.called_tile is not None:
120120
tiles.remove(meld.called_tile)
121121

122-
# for chankan we already added 3 tiles
122+
# for shouminkan we already added 3 tiles
123123
if meld.type == meld.CHANKAN:
124124
tiles = [meld.tiles[0]]
125125

project/tenhou/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,13 @@ def start_game(self):
281281

282282
if kan_type == Meld.CHANKAN:
283283
meld_type = 5
284+
logger.info('We upgraded pon to kan!')
284285
else:
285286
meld_type = 4
287+
logger.info('We called a closed kan set!')
286288

287289
self._send_message('<N type="{}" hai="{}" />'.format(meld_type, drawn_tile))
288-
logger.info('We called a closed kan\chankan set!')
290+
289291
continue
290292

291293
if not main_player.in_riichi:
@@ -345,7 +347,7 @@ def start_game(self):
345347
]
346348
# we win by other player's discard
347349
if any(i in message for i in win_suggestions):
348-
# enemy called chankan and we can win there
350+
# enemy called shouminkan and we can win there
349351
if self.decoder.is_opened_set_message(message):
350352
meld = self.decoder.parse_meld(message)
351353
tile = meld.called_tile

0 commit comments

Comments
 (0)