diff --git a/.flake8 b/.flake8
index 2d5c5816..42f0565a 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 120
-exclude = settings.py
\ No newline at end of file
+exclude = *settings.py,tests_validate_hand.py
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 6a71182b..a720f7d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,4 +20,7 @@ project/game/data/*
project/analytics/data/*
# temporary files
-experiments
\ No newline at end of file
+experiments
+
+*.prof
+profile.py
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index dad4a5be..480b8b8c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,10 @@
language: python
+dist: xenial
+sudo: true
python:
- "3.5"
- "3.6"
+ - "3.7"
before_script:
- cd project
install: "pip install -r project/requirements.txt"
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..4087cbea
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,5 @@
+## Tenhou bot code of conduct:
+
+1. A robot may not injure a human being or, through inaction, allow a human being to come to harm.
+2. A robot must obey orders given it by human beings except where such orders would conflict with the First Law.
+3. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.
diff --git a/bin/run.sh b/bin/run.sh
index 42287e1d..e0742c84 100644
--- a/bin/run.sh
+++ b/bin/run.sh
@@ -1,19 +1,21 @@
#!/bin/sh -e
-# cron will run each 5 minutes
-# and will search the run process
+# the cron job will be executed each 5 minutes
+# and it will try to find bot process
# if there is no process, it will run it
+# example of usage
+# */5 * * * * bash /root/bot/bin/run.sh bot_settings_name
-# */5 * * * * bash /root/bot/bin/run.sh
+SETTINGS_NAME="$1"
-PID=`ps -eaf | grep project/main.py | grep -v grep | awk '{print $2}'`
+PID=`ps -eaf | grep "project/main.py -s ${SETTINGS_NAME}" | grep -v grep | awk '{print $2}'`
-if [[ "" = "$PID" ]]; then
- /root/bot/env/bin/python /root/bot/project/main.py
+if [[ "" = "$PID" ]]; then
+ /root/bot/env/bin/python /root/bot/project/main.py -s ${SETTINGS_NAME}
else
WORKED_SECONDS=`ps -p "$PID" -o etimes=`
- # if process run > 60 minutes, probably it hang and we need to kill it
- if [ ${WORKED_SECONDS} -gt "3600" ]; then
+ # if process run > 60 minutes, probably it hangs and we need to kill it
+ if [[ ${WORKED_SECONDS} -gt "3600" ]]; then
kill ${PID}
fi
fi
\ No newline at end of file
diff --git a/project/game/ai/base/main.py b/project/game/ai/base/main.py
index 65318521..2af139ac 100644
--- a/project/game/ai/base/main.py
+++ b/project/game/ai/base/main.py
@@ -56,11 +56,12 @@ def should_call_riichi(self):
"""
return False
- def should_call_kan(self, tile, is_open_kan):
+ def should_call_kan(self, tile, is_open_kan, from_riichi=False):
"""
- When bot can call kan or chankan this method will be called
+ When bot can call kan or shouminkan this method will be called
:param tile: 136 tile format
:param is_open_kan: boolean
+ :param from_riichi: boolean
:return: kan type (Meld.KAN, Meld.CHANKAN) or None
"""
return False
diff --git a/project/game/ai/discard.py b/project/game/ai/discard.py
index 0b460b54..7a5e0802 100644
--- a/project/game/ai/discard.py
+++ b/project/game/ai/discard.py
@@ -1,10 +1,16 @@
# -*- coding: utf-8 -*-
from mahjong.constants import AKA_DORA_LIST
from mahjong.tile import TilesConverter
-from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora
+from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora, is_sou, is_man, is_pin
+
+from game.ai.first_version.strategies.main import BaseStrategy
class DiscardOption(object):
+ DORA_VALUE = 10000
+ DORA_FIRST_NEIGHBOUR = 1000
+ DORA_SECOND_NEIGHBOUR = 100
+
player = None
# in 34 tile format
@@ -12,7 +18,8 @@ class DiscardOption(object):
# array of tiles that will improve our hand
waiting = None
# how much tiles will improve our hand
- tiles_count = None
+ ukeire = None
+ ukeire_second = None
# number of shanten for that tile
shanten = None
# sometimes we had to force tile to be discarded
@@ -23,25 +30,45 @@ class DiscardOption(object):
valuation = None
# how danger this tile is
danger = None
+ # wait to ukeire map
+ wait_to_ukeire = None
+ # second level cost approximation for 1-shanten hands
+ second_level_cost = None
- def __init__(self, player, tile_to_discard, shanten, waiting, tiles_count, danger=100):
+ def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100, wait_to_ukeire=None):
"""
:param player:
:param tile_to_discard: tile in 34 format
:param waiting: list of tiles in 34 format
- :param tiles_count: count of tiles to wait after discard
+ :param ukeire: count of tiles to wait after discard
"""
self.player = player
self.tile_to_discard = tile_to_discard
self.shanten = shanten
self.waiting = waiting
- self.tiles_count = tiles_count
+ self.ukeire = ukeire
+ self.ukeire_second = 0
+ self.count_of_dora = 0
self.danger = danger
self.had_to_be_saved = False
self.had_to_be_discarded = False
+ self.wait_to_ukeire = wait_to_ukeire
self.calculate_value()
+ def __unicode__(self):
+ tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4])
+ return 'tile={}, shanten={}, ukeire={}, ukeire2={}, valuation={}'.format(
+ tile_format_136,
+ self.shanten,
+ self.ukeire,
+ self.ukeire_second,
+ self.valuation
+ )
+
+ def __repr__(self):
+ return '{}'.format(self.__unicode__())
+
def find_tile_in_hand(self, closed_hand):
"""
Find and return 136 tile in closed player hand
@@ -69,31 +96,66 @@ def find_tile_in_hand(self, closed_hand):
return TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, closed_hand)
- def calculate_value(self, shanten=None):
+ def calculate_value(self):
# base is 100 for ability to mark tiles as not needed (like set value to 50)
value = 100
honored_value = 20
- # we don't need to keep honor tiles in almost completed hand
- if shanten and shanten <= 2:
- honored_value = 0
-
if is_honor(self.tile_to_discard):
if self.tile_to_discard in self.player.valued_honors:
count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard]
# for west-west, east-east we had to double tile value
value += honored_value * len(count_of_winds)
else:
- # suits
- suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10]
+ # aim for tanyao
+ if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO:
+ suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10]
+ # usual hand
+ else:
+ suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10]
+
simplified_tile = simplify(self.tile_to_discard)
value += suit_tile_grades[simplified_tile]
+ for indicator in self.player.table.dora_indicators:
+ indicator_34 = indicator // 4
+ if is_honor(indicator_34):
+ continue
+
+ # indicator and tile not from the same suit
+ if is_sou(indicator_34) and not is_sou(self.tile_to_discard):
+ continue
+
+ # indicator and tile not from the same suit
+ if is_man(indicator_34) and not is_man(self.tile_to_discard):
+ continue
+
+ # indicator and tile not from the same suit
+ if is_pin(indicator_34) and not is_pin(self.tile_to_discard):
+ continue
+
+ simplified_indicator = simplify(indicator_34)
+ simplified_dora = simplified_indicator + 1
+ # indicator is 9 man
+ if simplified_dora == 9:
+ simplified_dora = 0
+
+ # tile so close to the dora
+ if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora:
+ value += DiscardOption.DORA_FIRST_NEIGHBOUR
+
+ # tile not far away from dora
+ if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora:
+ value += DiscardOption.DORA_SECOND_NEIGHBOUR
+
count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators)
- if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao):
+
+ tile_136 = self.find_tile_in_hand(self.player.closed_hand)
+ if is_aka_dora(tile_136, self.player.table.has_aka_dora):
count_of_dora += 1
- value += 50 * count_of_dora
+ self.count_of_dora = count_of_dora
+ value += count_of_dora * DiscardOption.DORA_VALUE
if is_honor(self.tile_to_discard):
# depends on how much honor tiles were discarded
@@ -108,4 +170,4 @@ def calculate_value(self, shanten=None):
if value == 0:
self.had_to_be_discarded = True
- self.valuation = value
+ self.valuation = int(value)
diff --git a/project/game/ai/first_version/defence/impossible_wait.py b/project/game/ai/first_version/defence/impossible_wait.py
index a6104f61..e1198f2a 100644
--- a/project/game/ai/first_version/defence/impossible_wait.py
+++ b/project/game/ai/first_version/defence/impossible_wait.py
@@ -14,10 +14,10 @@ def find_tiles_to_discard(self, _):
results = []
for x in HONOR_INDICES:
- if self.player.total_tiles(x, self.defence.hand_34) == 4:
+ if self.player.total_tiles(x, self.defence.closed_hand_34) == 4:
results.append(DefenceTile(x, DefenceTile.SAFE))
- if self.player.total_tiles(x, self.defence.hand_34) == 3:
+ if self.player.total_tiles(x, self.defence.closed_hand_34) == 3:
results.append(DefenceTile(x, DefenceTile.ALMOST_SAFE_TILE))
return results
diff --git a/project/game/ai/first_version/defence/kabe.py b/project/game/ai/first_version/defence/kabe.py
index 8d534da8..339f7994 100644
--- a/project/game/ai/first_version/defence/kabe.py
+++ b/project/game/ai/first_version/defence/kabe.py
@@ -7,52 +7,96 @@
class Kabe(Defence):
- def find_tiles_to_discard(self, _):
- results = []
-
+ def find_all_kabe(self, tiles_34):
# all indices shifted to -1
kabe_matrix = [
- {'indices': [1], 'blocked_tiles': [0]},
- {'indices': [2], 'blocked_tiles': [0, 1]},
- {'indices': [3], 'blocked_tiles': [1, 2]},
- {'indices': [4], 'blocked_tiles': [2, 6]},
- {'indices': [5], 'blocked_tiles': [6, 7]},
- {'indices': [6], 'blocked_tiles': [7, 8]},
- {'indices': [7], 'blocked_tiles': [8]},
- {'indices': [1, 5], 'blocked_tiles': [3]},
- {'indices': [2, 6], 'blocked_tiles': [4]},
- {'indices': [3, 7], 'blocked_tiles': [5]},
- {'indices': [1, 4], 'blocked_tiles': [2, 3]},
- {'indices': [2, 5], 'blocked_tiles': [3, 4]},
- {'indices': [3, 6], 'blocked_tiles': [4, 5]},
- {'indices': [4, 7], 'blocked_tiles': [5, 6]},
+ {'indices': [1], 'blocked_tiles': [0], 'type': KabeTile.STRONG_KABE},
+ {'indices': [2], 'blocked_tiles': [0, 1], 'type': KabeTile.STRONG_KABE},
+ {'indices': [6], 'blocked_tiles': [7, 8], 'type': KabeTile.STRONG_KABE},
+ {'indices': [7], 'blocked_tiles': [8], 'type': KabeTile.STRONG_KABE},
+ {'indices': [0, 3], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE},
+ {'indices': [1, 3], 'blocked_tiles': [2], 'type': KabeTile.STRONG_KABE},
+ {'indices': [1, 4], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE},
+ {'indices': [2, 4], 'blocked_tiles': [3], 'type': KabeTile.STRONG_KABE},
+ {'indices': [2, 5], 'blocked_tiles': [3, 4], 'type': KabeTile.STRONG_KABE},
+ {'indices': [3, 5], 'blocked_tiles': [4], 'type': KabeTile.STRONG_KABE},
+ {'indices': [3, 6], 'blocked_tiles': [4, 5], 'type': KabeTile.STRONG_KABE},
+ {'indices': [4, 6], 'blocked_tiles': [5], 'type': KabeTile.STRONG_KABE},
+ {'indices': [4, 7], 'blocked_tiles': [5, 6], 'type': KabeTile.STRONG_KABE},
+ {'indices': [5, 7], 'blocked_tiles': [6], 'type': KabeTile.STRONG_KABE},
+ {'indices': [5, 8], 'blocked_tiles': [6, 7], 'type': KabeTile.STRONG_KABE},
+
+ {'indices': [3], 'blocked_tiles': [1, 2], 'type': KabeTile.WEAK_KABE},
+ {'indices': [4], 'blocked_tiles': [2, 6], 'type': KabeTile.WEAK_KABE},
+ {'indices': [5], 'blocked_tiles': [6, 7], 'type': KabeTile.WEAK_KABE},
+ {'indices': [1, 5], 'blocked_tiles': [3], 'type': KabeTile.WEAK_KABE},
+ {'indices': [2, 6], 'blocked_tiles': [4], 'type': KabeTile.WEAK_KABE},
+ {'indices': [3, 7], 'blocked_tiles': [5], 'type': KabeTile.WEAK_KABE},
]
- suits = self._suits_tiles(self.defence.hand_34)
+ kabe_tiles_strong = []
+ kabe_tiles_weak = []
+ kabe_tiles_partial = []
+
+ suits = self._suits_tiles(tiles_34)
for x in range(0, 3):
suit = suits[x]
# "kabe" - 4 revealed tiles
kabe_tiles = []
+ partial_kabe_tiles = []
for y in range(0, 9):
suit_tile = suit[y]
if suit_tile == 4:
kabe_tiles.append(y)
+ elif suit_tile == 3:
+ partial_kabe_tiles.append(y)
- blocked_indices = []
for matrix_item in kabe_matrix:
- all_indices = len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0
- if all_indices:
- blocked_indices.extend(matrix_item['blocked_tiles'])
+ if len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0:
+ for tile in matrix_item['blocked_tiles']:
+ if matrix_item['type'] == KabeTile.STRONG_KABE:
+ kabe_tiles_strong.append(tile + x * 9)
+ else:
+ kabe_tiles_weak.append(tile + x * 9)
+
+ if len(list(set(matrix_item['indices']) - set(partial_kabe_tiles))) == 0:
+ for tile in matrix_item['blocked_tiles']:
+ kabe_tiles_partial.append(tile + x * 9)
+
+ kabe_tiles_unique = []
+ kabe_tiles_strong = list(set(kabe_tiles_strong))
+ kabe_tiles_weak = list(set(kabe_tiles_weak))
+ kabe_tiles_partial = list(set(kabe_tiles_partial))
+
+ for tile in kabe_tiles_strong:
+ kabe_tiles_unique.append(KabeTile(tile, KabeTile.STRONG_KABE))
+
+ for tile in kabe_tiles_weak:
+ if tile not in kabe_tiles_strong:
+ kabe_tiles_unique.append(KabeTile(tile, KabeTile.WEAK_KABE))
+
+ for tile in kabe_tiles_partial:
+ if tile not in kabe_tiles_strong and tile not in kabe_tiles_weak:
+ kabe_tiles_unique.append(KabeTile(tile, KabeTile.PARTIAL_KABE))
+
+ return kabe_tiles_unique
- blocked_indices = list(set(blocked_indices))
- for index in blocked_indices:
- # let's find 34 tile index
- tile = index + x * 9
- if self.player.total_tiles(tile, self.defence.hand_34) == 4:
- results.append(DefenceTile(tile, DefenceTile.SAFE))
+ def find_tiles_to_discard(self, _):
+ all_kabe = self.find_all_kabe(self.defence.closed_hand_34)
+
+ results = []
+
+ for kabe in all_kabe:
+ # we don't use it for defence now
+ if kabe.kabe_type == KabeTile.PARTIAL_KABE:
+ continue
- if self.player.total_tiles(tile, self.defence.hand_34) == 3:
- results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE))
+ tile = kabe.tile_34
+ if self.player.total_tiles(tile, self.defence.closed_hand_34) == 4:
+ results.append(DefenceTile(tile, DefenceTile.SAFE))
+
+ if self.player.total_tiles(tile, self.defence.closed_hand_34) == 3:
+ results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE))
return results
@@ -88,3 +132,16 @@ def _suits_tiles(self, tiles_34):
suits[suit_index][simplified_tile] += total_tiles
return suits
+
+
+class KabeTile(object):
+ STRONG_KABE = 0
+ WEAK_KABE = 1
+ PARTIAL_KABE = 2
+
+ tile_34 = None
+ kabe_type = None
+
+ def __init__(self, tile_34, kabe_type):
+ self.tile_34 = tile_34
+ self.kabe_type = kabe_type
diff --git a/project/game/ai/first_version/defence/main.py b/project/game/ai/first_version/defence/main.py
index c1bc1205..a9dc14c6 100644
--- a/project/game/ai/first_version/defence/main.py
+++ b/project/game/ai/first_version/defence/main.py
@@ -45,9 +45,12 @@ def should_go_to_defence_mode(self, discard_candidate=None):
waiting = discard_candidate.waiting
# we have 13 tiles in hand (this is not our turn)
else:
- shanten = self.player.ai.previous_shanten
+ shanten = self.player.ai.shanten
waiting = self.player.ai.waiting
+ if not waiting:
+ waiting = []
+
# if we are in riichi, we can't defence
if self.player.in_riichi:
return False
@@ -108,7 +111,13 @@ def should_go_to_defence_mode(self, discard_candidate=None):
return False
- def try_to_find_safe_tile_to_discard(self, discard_results):
+ def try_to_find_safe_tile_to_discard(self):
+ discard_results, _ = self.player.ai.hand_builder.find_discard_options(
+ self.player.tiles,
+ self.player.closed_hand,
+ self.player.melds
+ )
+
self.hand_34 = TilesConverter.to_34_array(self.player.tiles)
self.closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
@@ -205,7 +214,7 @@ def _find_tile_to_discard(self, safe_tiles, discard_tiles):
if not was_safe_tiles:
return None
- final_results = sorted(discard_tiles, key=lambda x: (x.danger, x.shanten, -x.tiles_count, x.valuation))
+ final_results = sorted(discard_tiles, key=lambda x: (x.danger, x.shanten, -x.ukeire, x.valuation))
return final_results[0]
diff --git a/project/game/ai/first_version/defence/suji.py b/project/game/ai/first_version/defence/suji.py
index e9cf7432..cde1f749 100644
--- a/project/game/ai/first_version/defence/suji.py
+++ b/project/game/ai/first_version/defence/suji.py
@@ -12,54 +12,90 @@ class Suji(Defence):
# 3-6-9
THIRD_SUJI = 3
- def find_tiles_to_discard(self, players):
- found_suji = []
- for player in players:
- suji = []
- suits = [[], [], []]
+ def find_suji(self, safe_tiles_34):
+ suji = []
+ suits = [[], [], []]
+
+ # let's cast each tile to 0-8 presentation
+ for tile in safe_tiles_34:
+ if is_man(tile):
+ suits[0].append(simplify(tile))
+
+ if is_pin(tile):
+ suits[1].append(simplify(tile))
- # let's cast each tile to 0-8 presentation
- safe_tiles = player.all_safe_tiles
- for tile in safe_tiles:
- if is_man(tile):
- suits[0].append(simplify(tile))
+ if is_sou(tile):
+ suits[2].append(simplify(tile))
- if is_pin(tile):
- suits[1].append(simplify(tile))
+ for x in range(0, 3):
+ simplified_tiles = suits[x]
+ base = x * 9
- if is_sou(tile):
- suits[2].append(simplify(tile))
+ # 1-4-7
+ if 3 in simplified_tiles:
+ suji.append(self.FIRST_SUJI + base)
- for x in range(0, 3):
- simplified_tiles = suits[x]
- base = x * 9
+ # double 1-4-7
+ if 0 in simplified_tiles and 6 in simplified_tiles:
+ suji.append(self.FIRST_SUJI + base)
- # 1-4-7
- if 3 in simplified_tiles:
- suji.append(self.FIRST_SUJI + base)
+ # 2-5-8
+ if 4 in simplified_tiles:
+ suji.append(self.SECOND_SUJI + base)
- # double 1-4-7
- if 0 in simplified_tiles and 6 in simplified_tiles:
- suji.append(self.FIRST_SUJI + base)
+ # double 2-5-8
+ if 1 in simplified_tiles and 7 in simplified_tiles:
+ suji.append(self.SECOND_SUJI + base)
- # 2-5-8
- if 4 in simplified_tiles:
- suji.append(self.SECOND_SUJI + base)
+ # 3-6-9
+ if 5 in simplified_tiles:
+ suji.append(self.THIRD_SUJI + base)
- # double 2-5-8
- if 1 in simplified_tiles and 7 in simplified_tiles:
- suji.append(self.SECOND_SUJI + base)
+ # double 3-6-9
+ if 2 in simplified_tiles and 8 in simplified_tiles:
+ suji.append(self.THIRD_SUJI + base)
- # 3-6-9
- if 5 in simplified_tiles:
- suji.append(self.THIRD_SUJI + base)
+ suji = list(set(suji))
- # double 3-6-9
- if 2 in simplified_tiles and 8 in simplified_tiles:
- suji.append(self.THIRD_SUJI + base)
+ return suji
- suji = list(set(suji))
+ def find_suji_against_self(self, player):
+ discards_34 = list(set([x.value // 4 for x in player.discards]))
+ all_suji = self.find_suji(discards_34)
+
+ result = []
+ for suji in all_suji:
+ suji_temp = suji % 9
+ base = suji - suji_temp - 1
+
+ if suji_temp == self.FIRST_SUJI:
+ result += [
+ base + 1,
+ base + 4,
+ base + 7
+ ]
+
+ if suji_temp == self.SECOND_SUJI:
+ result += [
+ base + 2,
+ base + 5,
+ base + 8
+ ]
+
+ if suji_temp == self.THIRD_SUJI:
+ result += [
+ base + 3,
+ base + 6,
+ base + 9
+ ]
+
+ return result
+
+ def find_tiles_to_discard(self, enemies):
+ found_suji = []
+ for enemy in enemies:
+ suji = self.find_suji(enemy.all_safe_tiles)
found_suji.append(suji)
if not found_suji:
@@ -108,7 +144,9 @@ def _suji_tiles(self, suji):
# mark dora tiles as dangerous tiles to discard
for tile in result:
- if plus_dora(tile.value * 4, self.table.dora_indicators) or is_aka_dora(tile.value * 4, self.table.has_open_tanyao):
+ is_dora = plus_dora(tile.value * 4, self.table.dora_indicators) \
+ or is_aka_dora(tile.value * 4, self.table.has_open_tanyao)
+ if is_dora:
tile.danger += 100
return result
diff --git a/project/game/ai/first_version/hand_builder.py b/project/game/ai/first_version/hand_builder.py
new file mode 100644
index 00000000..9ff28a66
--- /dev/null
+++ b/project/game/ai/first_version/hand_builder.py
@@ -0,0 +1,767 @@
+from mahjong.constants import AKA_DORA_LIST
+from mahjong.shanten import Shanten
+from mahjong.tile import TilesConverter, Tile
+from mahjong.utils import is_tile_strictly_isolated, is_pair, is_honor, simplify
+
+import utils.decisions_constants as log
+from game.ai.discard import DiscardOption
+from game.ai.first_version.defence.kabe import KabeTile
+from utils.decisions_logger import DecisionsLogger
+
+
+class HandBuilder:
+ player = None
+ ai = None
+
+ def __init__(self, player, ai):
+ self.player = player
+ self.ai = ai
+
+ class TankiWait:
+ TANKI_WAIT_NON_YAKUHAI = 1
+ TANKI_WAIT_SELF_YAKUHAI = 2
+ TANKI_WAIT_ALL_YAKUHAI = 3
+ TANKI_WAIT_69_KABE = 4
+ TANKI_WAIT_69_SUJI = 5
+ TANKI_WAIT_69_RAW = 6
+ TANKI_WAIT_28_KABE = 7
+ TANKI_WAIT_28_SUJI = 8
+ TANKI_WAIT_28_RAW = 9
+ TANKI_WAIT_37_KABE = 10
+ TANKI_WAIT_37_SUJI = 11
+ TANKI_WAIT_37_RAW = 12
+ TANKI_WAIT_456_KABE = 13
+ TANKI_WAIT_456_SUJI = 14
+ TANKI_WAIT_456_RAW = 15
+
+ tanki_wait_same_ukeire_2_3_prio = {
+ TANKI_WAIT_NON_YAKUHAI: 15,
+ TANKI_WAIT_69_KABE: 14,
+ TANKI_WAIT_69_SUJI: 14,
+ TANKI_WAIT_SELF_YAKUHAI: 13,
+ TANKI_WAIT_ALL_YAKUHAI: 12,
+ TANKI_WAIT_28_KABE: 11,
+ TANKI_WAIT_28_SUJI: 11,
+ TANKI_WAIT_37_KABE: 10,
+ TANKI_WAIT_37_SUJI: 10,
+ TANKI_WAIT_69_RAW: 9,
+ TANKI_WAIT_456_KABE: 8,
+ TANKI_WAIT_456_SUJI: 8,
+ TANKI_WAIT_28_RAW: 7,
+ TANKI_WAIT_456_RAW: 6,
+ TANKI_WAIT_37_RAW: 5
+ }
+
+ tanki_wait_same_ukeire_1_prio = {
+ TANKI_WAIT_NON_YAKUHAI: 15,
+ TANKI_WAIT_SELF_YAKUHAI: 14,
+ TANKI_WAIT_ALL_YAKUHAI: 13,
+ TANKI_WAIT_69_KABE: 12,
+ TANKI_WAIT_69_SUJI: 12,
+ TANKI_WAIT_28_KABE: 11,
+ TANKI_WAIT_28_SUJI: 11,
+ TANKI_WAIT_37_KABE: 10,
+ TANKI_WAIT_37_SUJI: 10,
+ TANKI_WAIT_69_RAW: 9,
+ TANKI_WAIT_456_KABE: 8,
+ TANKI_WAIT_456_SUJI: 8,
+ TANKI_WAIT_28_RAW: 7,
+ TANKI_WAIT_456_RAW: 6,
+ TANKI_WAIT_37_RAW: 5
+ }
+
+ tanki_wait_diff_ukeire_prio = {
+ TANKI_WAIT_NON_YAKUHAI: 1,
+ TANKI_WAIT_SELF_YAKUHAI: 1,
+ TANKI_WAIT_ALL_YAKUHAI: 1,
+ TANKI_WAIT_69_KABE: 1,
+ TANKI_WAIT_69_SUJI: 1,
+ TANKI_WAIT_28_KABE: 0,
+ TANKI_WAIT_28_SUJI: 0,
+ TANKI_WAIT_37_KABE: 0,
+ TANKI_WAIT_37_SUJI: 0,
+ TANKI_WAIT_69_RAW: 0,
+ TANKI_WAIT_456_KABE: 0,
+ TANKI_WAIT_456_SUJI: 0,
+ TANKI_WAIT_28_RAW: 0,
+ TANKI_WAIT_456_RAW: 0,
+ TANKI_WAIT_37_RAW: 0
+ }
+
+ def discard_tile(self, tiles, closed_hand, melds, print_log=True):
+ selected_tile = self.choose_tile_to_discard(
+ tiles,
+ closed_hand,
+ melds,
+ print_log=print_log
+ )
+
+ # bot think that there is a threat on the table
+ # and better to fold
+ # if we can't find safe tiles, let's continue to build our hand
+ if self.ai.defence.should_go_to_defence_mode(selected_tile):
+ if not self.ai.in_defence:
+ DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
+ self.ai.in_defence = True
+
+ defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard()
+ if defence_tile:
+ return self.process_discard_option(defence_tile, closed_hand)
+ else:
+ if self.ai.in_defence:
+ DecisionsLogger.debug(log.DEFENCE_DEACTIVATE)
+ self.ai.in_defence = False
+
+ return self.process_discard_option(selected_tile, closed_hand, print_log=print_log)
+
+ def calculate_shanten(self, tiles_34, open_sets_34=None):
+ shanten_with_chiitoitsu = self.ai.shanten_calculator.calculate_shanten(tiles_34,
+ open_sets_34,
+ chiitoitsu=True)
+ shanten_without_chiitoitsu = self.ai.shanten_calculator.calculate_shanten(tiles_34,
+ open_sets_34,
+ chiitoitsu=False)
+
+ if shanten_with_chiitoitsu == 0 and shanten_without_chiitoitsu >= 1:
+ shanten = shanten_with_chiitoitsu
+ use_chiitoitsu = True
+ elif shanten_with_chiitoitsu == 1 and shanten_without_chiitoitsu >= 3:
+ shanten = shanten_with_chiitoitsu
+ use_chiitoitsu = True
+ else:
+ shanten = shanten_without_chiitoitsu
+ use_chiitoitsu = False
+
+ return shanten, use_chiitoitsu
+
+ def calculate_waits(self, tiles_34, open_sets_34=None):
+ """
+ :param tiles_34: array of tiles in 34 formant, 13 of them (this is important)
+ :param open_sets_34: array of array with tiles in 34 format
+ :return: array of waits in 34 format and number of shanten
+ """
+
+ shanten, use_chiitoitsu = self.calculate_shanten(tiles_34, open_sets_34)
+
+ waiting = []
+ for j in range(0, 34):
+ if tiles_34[j] == 4:
+ continue
+
+ tiles_34[j] += 1
+
+ key = '{},{},{}'.format(
+ ''.join([str(x) for x in tiles_34]),
+ ';'.join([str(x) for x in open_sets_34]),
+ use_chiitoitsu and 1 or 0
+ )
+
+ if key in self.ai.hand_cache:
+ new_shanten = self.ai.hand_cache[key]
+ else:
+ new_shanten = self.ai.shanten_calculator.calculate_shanten(
+ tiles_34,
+ open_sets_34,
+ chiitoitsu=use_chiitoitsu
+ )
+ self.ai.hand_cache[key] = new_shanten
+
+ if new_shanten == shanten - 1:
+ waiting.append(j)
+
+ tiles_34[j] -= 1
+
+ return waiting, shanten
+
+ def find_discard_options(self, tiles, closed_hand, melds=None):
+ """
+ :param tiles: array of tiles in 136 format
+ :param closed_hand: array of tiles in 136 format
+ :param melds:
+ :return:
+ """
+ if melds is None:
+ melds = []
+
+ open_sets_34 = [x.tiles_34 for x in melds]
+
+ tiles_34 = TilesConverter.to_34_array(tiles)
+ closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
+ is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles)
+
+ results = []
+ for hand_tile in range(0, 34):
+ if not closed_tiles_34[hand_tile]:
+ continue
+
+ tiles_34[hand_tile] -= 1
+ waiting, shanten = self.calculate_waits(tiles_34, open_sets_34)
+ tiles_34[hand_tile] += 1
+
+ if waiting:
+ wait_to_ukeire = dict(zip(waiting, [self.count_tiles([x], closed_tiles_34) for x in waiting]))
+ results.append(DiscardOption(player=self.player,
+ shanten=shanten,
+ tile_to_discard=hand_tile,
+ waiting=waiting,
+ ukeire=self.count_tiles(waiting, closed_tiles_34),
+ wait_to_ukeire=wait_to_ukeire))
+
+ if is_agari:
+ shanten = Shanten.AGARI_STATE
+ else:
+ shanten, _ = self.calculate_shanten(
+ tiles_34,
+ open_sets_34
+ )
+
+ return results, shanten
+
+ def count_tiles(self, waiting, tiles_34):
+ n = 0
+ for tile_34 in waiting:
+ n += 4 - self.player.total_tiles(tile_34, tiles_34)
+ return n
+
+ def divide_hand(self, tiles, waiting):
+ tiles_copy = tiles.copy()
+
+ for i in range(0, 4):
+ if waiting * 4 + i not in tiles_copy:
+ tiles_copy += [waiting * 4 + i]
+ break
+
+ tiles_34 = TilesConverter.to_34_array(tiles_copy)
+
+ results = self.player.ai.hand_divider.divide_hand(tiles_34)
+ return results, tiles_34
+
+ def check_suji_and_kabe(self, tiles_34, waiting):
+ # let's find suji-traps in our discard
+ suji_tiles = self.player.ai.defence.suji.find_suji_against_self(self.player)
+ have_suji = waiting in suji_tiles
+
+ # let's find kabe
+ kabe_tiles = self.player.ai.defence.kabe.find_all_kabe(tiles_34)
+ have_kabe = False
+ for kabe in kabe_tiles:
+ if waiting == kabe.tile_34 and kabe.kabe_type == KabeTile.STRONG_KABE:
+ have_kabe = True
+
+ return have_suji, have_kabe
+
+ def _choose_best_tanki_wait(self, discard_desc):
+ discard_desc = sorted(discard_desc, key=lambda k: k['hand_cost'], reverse=True)
+
+ # we are always choosing between exactly two tanki waits
+ assert len(discard_desc) == 2
+
+ discard_desc = [x for x in discard_desc if x['hand_cost'] != 0]
+
+ # we are guaranteed to have at least one wait with cost by caller logic
+ assert len(discard_desc) > 0
+
+ if len(discard_desc) == 1:
+ return discard_desc[0]['discard_option']
+
+ # if not 1 then 2
+ assert len(discard_desc) == 2
+
+ num_furiten_waits = len([x for x in discard_desc if x['is_furiten']])
+ # if we choose tanki, we always prefer non-furiten wait over furiten one, no matter what the cost is
+ if num_furiten_waits == 1:
+ return [x for x in discard_desc if not x['is_furiten']][0]['discard_option']
+
+ best_discard_desc = [x for x in discard_desc if x['hand_cost'] == discard_desc[0]['hand_cost']]
+
+ # first of all we choose the most expensive wait
+ if len(best_discard_desc) == 1:
+ return best_discard_desc[0]['discard_option']
+
+ best_ukeire = best_discard_desc[0]['discard_option'].ukeire
+ diff = best_ukeire - best_discard_desc[1]['discard_option'].ukeire
+ # if both tanki waits have the same ukeire
+ if diff == 0:
+ # case when we have 2 or 3 tiles to wait for
+ if best_ukeire == 2 or best_ukeire == 3:
+ best_discard_desc = sorted(best_discard_desc,
+ key=lambda k: self.TankiWait.tanki_wait_same_ukeire_2_3_prio[
+ k['tanki_type']],
+ reverse=True)
+ return best_discard_desc[0]['discard_option']
+
+ # case when we have 1 tile to wait for
+ if best_ukeire == 1:
+ best_discard_desc = sorted(best_discard_desc,
+ key=lambda k: self.TankiWait.tanki_wait_same_ukeire_1_prio[
+ k['tanki_type']],
+ reverse=True)
+ return best_discard_desc[0]['discard_option']
+
+ # should never reach here
+ assert False
+
+ # if one tanki wait has 1 more tile to wait than the other we only choose the latter one if it is
+ # a wind or alike and the first one is not
+ if diff == 1:
+ prio_0 = self.TankiWait.tanki_wait_diff_ukeire_prio[best_discard_desc[0]['tanki_type']]
+ prio_1 = self.TankiWait.tanki_wait_diff_ukeire_prio[best_discard_desc[1]['tanki_type']]
+ if prio_1 > prio_0:
+ return best_discard_desc[1]['discard_option']
+
+ return best_discard_desc[0]['discard_option']
+
+ if diff > 1:
+ return best_discard_desc[0]['discard_option']
+
+ # if everything is the same we just choose the first one
+ return best_discard_desc[0]['discard_option']
+
+ def _is_waiting_furiten(self, tile_34):
+ discarded_tiles = [x.value // 4 for x in self.player.discards]
+ return tile_34 in discarded_tiles
+
+ def _is_discard_option_furiten(self, discard_option):
+ is_furiten = False
+
+ for waiting in discard_option.waiting:
+ is_furiten = is_furiten or self._is_waiting_furiten(waiting)
+
+ return is_furiten
+
+ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
+ # first of all we find tiles that have the best hand cost * ukeire value
+ call_riichi = not self.player.is_open_hand
+
+ discard_desc = []
+ player_tiles_copy = self.player.tiles.copy()
+ player_melds_copy = self.player.melds.copy()
+
+ closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
+
+ for discard_option in discard_options:
+ tile = discard_option.find_tile_in_hand(self.player.closed_hand)
+ # temporary remove discard option to estimate hand value
+ self.player.tiles = tiles.copy()
+ self.player.tiles.remove(tile)
+ # temporary replace melds
+ self.player.melds = melds.copy()
+ # for kabe/suji handling
+ discarded_tile = Tile(tile, False)
+ self.player.discards.append(discarded_tile)
+
+ is_furiten = self._is_discard_option_furiten(discard_option)
+
+ if len(discard_option.waiting) == 1:
+ waiting = discard_option.waiting[0]
+
+ cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi)
+
+ # let's check if this is a tanki wait
+ results, tiles_34 = self.divide_hand(self.player.tiles, waiting)
+ result = results[0]
+
+ tanki_type = None
+
+ is_tanki = False
+ for hand_set in result:
+ if waiting not in hand_set:
+ continue
+
+ if is_pair(hand_set):
+ is_tanki = True
+
+ if is_honor(waiting):
+ # TODO: differentiate between self honor and honor for all players
+ if waiting in self.player.valued_honors:
+ tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI
+ else:
+ tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI
+ break
+
+ simplified_waiting = simplify(waiting)
+ have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting)
+
+ # TODO: not sure about suji/kabe priority, so we keep them same for now
+ if 3 <= simplified_waiting <= 5:
+ if have_suji or have_kabe:
+ tanki_type = self.TankiWait.TANKI_WAIT_456_KABE
+ else:
+ tanki_type = self.TankiWait.TANKI_WAIT_456_RAW
+ elif 2 <= simplified_waiting <= 6:
+ if have_suji or have_kabe:
+ tanki_type = self.TankiWait.TANKI_WAIT_37_KABE
+ else:
+ tanki_type = self.TankiWait.TANKI_WAIT_37_RAW
+ elif 1 <= simplified_waiting <= 7:
+ if have_suji or have_kabe:
+ tanki_type = self.TankiWait.TANKI_WAIT_28_KABE
+ else:
+ tanki_type = self.TankiWait.TANKI_WAIT_28_RAW
+ else:
+ if have_suji or have_kabe:
+ tanki_type = self.TankiWait.TANKI_WAIT_69_KABE
+ else:
+ tanki_type = self.TankiWait.TANKI_WAIT_69_RAW
+ break
+
+ discard_desc.append({
+ 'discard_option': discard_option,
+ 'hand_cost': hand_cost,
+ 'cost_x_ukeire': cost_x_ukeire,
+ 'is_furiten': is_furiten,
+ 'is_tanki': is_tanki,
+ 'tanki_type': tanki_type
+ })
+ else:
+ cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi)
+
+ discard_desc.append({
+ 'discard_option': discard_option,
+ 'hand_cost': None,
+ 'cost_x_ukeire': cost_x_ukeire,
+ 'is_furiten': is_furiten,
+ 'is_tanki': False,
+ 'tanki_type': None
+ })
+
+ # reverse all temporary tile tweaks
+ self.player.tiles = player_tiles_copy
+ self.player.melds = player_melds_copy
+ self.player.discards.remove(discarded_tile)
+
+ discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True)
+
+ # if we don't have any good options, e.g. all our possible waits ara karaten
+ # FIXME: in that case, discard the safest tile
+ if discard_desc[0]['cost_x_ukeire'] == 0:
+ return sorted(discard_options, key=lambda x: x.valuation)[0]
+
+ num_tanki_waits = len([x for x in discard_desc if x['is_tanki']])
+
+ # what if all our waits are tanki waits? we need a special handling for that case
+ if num_tanki_waits == len(discard_options):
+ return self._choose_best_tanki_wait(discard_desc)
+
+ best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']]
+
+ # we only have one best option based on ukeire and cost, nothing more to do here
+ if len(best_discard_desc) == 1:
+ return best_discard_desc[0]['discard_option']
+
+ # if we have several options that give us similar wait
+ # FIXME: 1. we find the safest tile to discard
+ # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
+ return best_discard_desc[0]['discard_option']
+
+ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
+ """
+ Try to find best tile to discard, based on different rules
+ """
+
+ discard_options, _ = self.find_discard_options(
+ tiles,
+ closed_hand,
+ melds
+ )
+
+ # our strategy can affect discard options
+ if self.ai.current_strategy:
+ discard_options = self.ai.current_strategy.determine_what_to_discard(
+ discard_options,
+ closed_hand,
+ melds
+ )
+
+ had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded]
+ if had_to_be_discarded_tiles:
+ discard_options = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))
+ DecisionsLogger.debug(
+ log.DISCARD_OPTIONS,
+ 'Discard marked tiles first',
+ discard_options,
+ print_log=print_log
+ )
+ return discard_options[0]
+
+ # remove needed tiles from discard options
+ discard_options = [x for x in discard_options if not x.had_to_be_saved]
+
+ discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire))
+ first_option = discard_options[0]
+ results_with_same_shanten = [x for x in discard_options if x.shanten == first_option.shanten]
+
+ possible_options = [first_option]
+ ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire')
+ for discard_option in results_with_same_shanten:
+ # there is no sense to check already chosen tile
+ if discard_option.tile_to_discard == first_option.tile_to_discard:
+ continue
+
+ # let's choose tiles that are close to the max ukeire tile
+ if discard_option.ukeire >= first_option.ukeire - ukeire_borders:
+ possible_options.append(discard_option)
+
+ if first_option.shanten in [1, 2, 3]:
+ ukeire_field = 'ukeire_second'
+ for x in possible_options:
+ self.calculate_second_level_ukeire(x, tiles, melds)
+
+ possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))
+
+ filter_percentage = 20
+ possible_options = self._filter_list_by_percentage(
+ possible_options,
+ ukeire_field,
+ filter_percentage
+ )
+ else:
+ ukeire_field = 'ukeire'
+ possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))
+
+ # only one option - so we choose it
+ if len(possible_options) == 1:
+ return possible_options[0]
+
+ # tempai state has a special handling
+ if first_option.shanten == 0:
+ other_tiles_with_same_shanten = [x for x in possible_options if x.shanten == 0]
+ return self._choose_best_discard_in_tempai(tiles, melds, other_tiles_with_same_shanten)
+
+ tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0]
+
+ # we have only dora candidates to discard
+ if not tiles_without_dora:
+ DecisionsLogger.debug(
+ log.DISCARD_OPTIONS,
+ context=possible_options,
+ print_log=print_log
+ )
+
+ min_dora = min([x.count_of_dora for x in possible_options])
+ min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora]
+
+ return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0]
+
+ # only one option - so we choose it
+ if len(tiles_without_dora) == 1:
+ return tiles_without_dora[0]
+
+ # 1-shanten hands have special handling - we can consider future hand cost here
+ if first_option.shanten == 1:
+ return sorted(tiles_without_dora, key=lambda x: (-x.second_level_cost, -x.ukeire_second, x.valuation))[0]
+
+ if first_option.shanten == 2 or first_option.shanten == 3:
+ # we filter 10% of options here
+ second_filter_percentage = 10
+ filtered_options = self._filter_list_by_percentage(
+ tiles_without_dora,
+ ukeire_field,
+ second_filter_percentage
+ )
+ # we should also consider borders for 3+ shanten hands
+ else:
+ best_option_without_dora = tiles_without_dora[0]
+ ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field)
+ filtered_options = []
+ for discard_option in tiles_without_dora:
+ val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders
+ if getattr(discard_option, ukeire_field) >= val:
+ filtered_options.append(discard_option)
+
+ DecisionsLogger.debug(
+ log.DISCARD_OPTIONS,
+ context=possible_options,
+ print_log=print_log
+ )
+
+ closed_hand_34 = TilesConverter.to_34_array(closed_hand)
+ isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)]
+ # isolated tiles should be discarded first
+ if isolated_tiles:
+ # let's sort tiles by value and let's choose less valuable tile to discard
+ return sorted(isolated_tiles, key=lambda x: x.valuation)[0]
+
+ # there are no isolated tiles or we don't care about them
+ # let's discard tile with greater ukeire/ukeire2
+ filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field))
+ first_option = filtered_options[0]
+
+ other_tiles_with_same_ukeire = [x for x in filtered_options
+ if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)]
+
+ # it will happen with shanten=1, all tiles will have ukeire_second == 0
+ # or in tempai we can have several tiles with same ukeire
+ if other_tiles_with_same_ukeire:
+ return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0]
+
+ # we have only one candidate to discard with greater ukeire
+ return first_option
+
+ def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True):
+ if print_log:
+ DecisionsLogger.debug(
+ log.DISCARD,
+ context=discard_option
+ )
+
+ self.player.in_tempai = discard_option.shanten == 0
+ self.ai.waiting = discard_option.waiting
+ self.ai.shanten = discard_option.shanten
+ self.ai.ukeire = discard_option.ukeire
+ self.ai.ukeire_second = discard_option.ukeire_second
+
+ # when we called meld we don't need "smart" discard
+ if force_discard:
+ return discard_option.find_tile_in_hand(closed_hand)
+
+ last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None
+ if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
+ return self.player.last_draw
+ else:
+ return discard_option.find_tile_in_hand(closed_hand)
+
+ def calculate_second_level_ukeire(self, discard_option, tiles, melds):
+ not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or []
+ call_riichi = not self.player.is_open_hand
+
+ # we are going to do manipulations that require player hand to be updated
+ # so we save original tiles here and restore it at the end of the function
+ player_tiles_original = self.player.tiles.copy()
+
+ tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand)
+
+ self.player.tiles = tiles.copy()
+ self.player.tiles.remove(tile_in_hand)
+
+ sum_tiles = 0
+ sum_cost = 0
+ for wait_34 in discard_option.waiting:
+ if self.player.is_open_hand and wait_34 in not_suitable_tiles:
+ continue
+
+ closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
+ live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34)
+
+ if live_tiles == 0:
+ continue
+
+ wait_136 = wait_34 * 4
+ self.player.tiles.append(wait_136)
+
+ results, shanten = self.find_discard_options(
+ self.player.tiles,
+ self.player.closed_hand,
+ melds
+ )
+ results = [x for x in results if x.shanten == discard_option.shanten - 1]
+
+ # let's take best ukeire here
+ if results:
+ result_has_atodzuke = False
+ if self.player.is_open_hand:
+ best_one = results[0]
+ best_ukeire = 0
+ for result in results:
+ has_atodzuke = False
+ ukeire = 0
+ for wait_34 in result.waiting:
+ if wait_34 in not_suitable_tiles:
+ has_atodzuke = True
+ else:
+ ukeire += result.wait_to_ukeire[wait_34]
+
+ # let's consider atodzuke waits to be worse than non-atodzuke ones
+ if has_atodzuke:
+ ukeire /= 2
+
+ if (ukeire > best_ukeire) or (ukeire >= best_ukeire and not has_atodzuke):
+ best_ukeire = ukeire
+ best_one = result
+ result_has_atodzuke = has_atodzuke
+ else:
+ best_one = sorted(results, key=lambda x: -x.ukeire)[0]
+ best_ukeire = best_one.ukeire
+
+ sum_tiles += best_ukeire * live_tiles
+
+ # if we are going to have a tempai (on our second level) - let's also count its cost
+ if shanten == 0:
+ next_tile_in_hand = best_one.find_tile_in_hand(self.player.closed_hand)
+ self.player.tiles.remove(next_tile_in_hand)
+ cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, call_riichi=call_riichi)
+ # we reduce tile valuation for atodzuke
+ if result_has_atodzuke:
+ cost_x_ukeire /= 2
+ sum_cost += cost_x_ukeire
+ self.player.tiles.append(next_tile_in_hand)
+
+ self.player.tiles.remove(wait_136)
+
+ discard_option.ukeire_second = sum_tiles
+ if discard_option.shanten == 1:
+ discard_option.second_level_cost = sum_cost
+
+ # restore original state of player hand
+ self.player.tiles = player_tiles_original
+
+ @staticmethod
+ def _filter_list_by_percentage(items, attribute, percentage):
+ filtered_options = []
+ first_option = items[0]
+ ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage)
+ for x in items:
+ if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders:
+ filtered_options.append(x)
+ return filtered_options
+
+ @staticmethod
+ def _choose_ukeire_borders(first_option, border_percentage, border_field):
+ ukeire_borders = round((getattr(first_option, border_field) / 100) * border_percentage)
+
+ if first_option.shanten == 0 and ukeire_borders < 2:
+ ukeire_borders = 2
+
+ if first_option.shanten == 1 and ukeire_borders < 4:
+ ukeire_borders = 4
+
+ if first_option.shanten >= 2 and ukeire_borders < 8:
+ ukeire_borders = 8
+
+ return ukeire_borders
+
+ def _estimate_cost_x_ukeire(self, discard_option, call_riichi):
+ cost_x_ukeire_tsumo = 0
+ cost_x_ukeire_ron = 0
+ hand_cost_tsumo = 0
+ hand_cost_ron = 0
+
+ is_furiten = self._is_discard_option_furiten(discard_option)
+
+ for waiting in discard_option.waiting:
+ hand_value = self.player.ai.estimate_hand_value(waiting,
+ call_riichi=call_riichi,
+ is_tsumo=True)
+ if hand_value.error is None:
+ hand_cost_tsumo = hand_value.cost['main'] + 2 * hand_value.cost['additional']
+ cost_x_ukeire_tsumo += hand_cost_tsumo * discard_option.wait_to_ukeire[waiting]
+
+ if not is_furiten:
+ hand_value = self.player.ai.estimate_hand_value(waiting,
+ call_riichi=call_riichi,
+ is_tsumo=False)
+ if hand_value.error is None:
+ hand_cost_ron = hand_value.cost['main']
+ cost_x_ukeire_ron += hand_cost_ron * discard_option.wait_to_ukeire[waiting]
+
+ # these are abstract numbers used to compare different waits
+ # some don't have yaku, some furiten, etc.
+ # so we use an abstract formula of 1 tsumo cost + 3 ron costs
+ cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
+
+ if len(discard_option.waiting) == 1:
+ hand_cost = hand_cost_tsumo + 3 * hand_cost_ron
+ else:
+ hand_cost = None
+
+ return cost_x_ukeire, hand_cost
diff --git a/project/game/ai/first_version/main.py b/project/game/ai/first_version/main.py
index 872d8c88..d8323f53 100644
--- a/project/game/ai/first_version/main.py
+++ b/project/game/ai/first_version/main.py
@@ -1,191 +1,131 @@
# -*- coding: utf-8 -*-
-import logging
+import copy
+import utils.decisions_constants as log
from mahjong.agari import Agari
-from mahjong.constants import AKA_DORA_LIST, CHUN, HAKU, HATSU
+from mahjong.constants import AKA_DORA_LIST, DISPLAY_WINDS
from mahjong.hand_calculating.divider import HandDivider
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.hand_calculating.hand_config import HandConfig
from mahjong.meld import Meld
from mahjong.shanten import Shanten
from mahjong.tile import TilesConverter
-from mahjong.utils import is_pair, is_pon
+from mahjong.utils import is_pon
from game.ai.base.main import InterfaceAI
-from game.ai.discard import DiscardOption
from game.ai.first_version.defence.main import DefenceHandler
+from game.ai.first_version.hand_builder import HandBuilder
+from game.ai.first_version.riichi import Riichi
+from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy
+from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy
+from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy
from game.ai.first_version.strategies.honitsu import HonitsuStrategy
from game.ai.first_version.strategies.main import BaseStrategy
from game.ai.first_version.strategies.tanyao import TanyaoStrategy
from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy
-
-logger = logging.getLogger('ai')
+from utils.decisions_logger import DecisionsLogger
class ImplementationAI(InterfaceAI):
- version = '0.3.2'
+ version = '0.4.0-dev'
agari = None
- shanten = None
+ shanten_calculator = None
defence = None
+ riichi = None
hand_divider = None
finished_hand = None
- last_discard_option = None
- previous_shanten = 7
+ shanten = 7
+ ukeire = 0
+ ukeire_second = 0
in_defence = False
waiting = None
current_strategy = None
+ last_discard_option = None
+
+ hand_cache = {}
def __init__(self, player):
super(ImplementationAI, self).__init__(player)
self.agari = Agari()
- self.shanten = Shanten()
+ self.shanten_calculator = Shanten()
self.defence = DefenceHandler(player)
+ self.riichi = Riichi(player)
self.hand_divider = HandDivider()
self.finished_hand = HandCalculator()
- self.previous_shanten = 7
- self.current_strategy = None
- self.waiting = []
- self.in_defence = False
- self.last_discard_option = None
+ self.hand_builder = HandBuilder(player, self)
- def init_hand(self):
- """
- Let's decide what we will do with our hand (like open for tanyao and etc.)
- """
- self.determine_strategy()
+ self.erase_state()
def erase_state(self):
- self.current_strategy = None
+ self.shanten = 7
+ self.ukeire = 0
+ self.ukeire_second = 0
self.in_defence = False
+ self.waiting = None
+
+ self.current_strategy = None
self.last_discard_option = None
- def draw_tile(self, tile):
- """
- :param tile: 136 tile format
- :return:
- """
- self.determine_strategy()
+ self.hand_cache = {}
+
+ def init_hand(self):
+ DecisionsLogger.debug(log.INIT_HAND, context=[
+ 'Round wind: {}'.format(DISPLAY_WINDS[self.table.round_wind_tile]),
+ 'Player wind: {}'.format(DISPLAY_WINDS[self.player.player_wind]),
+ 'Hand: {}'.format(self.player.format_hand_for_print()),
+ ])
+
+ self.shanten, _ = self.hand_builder.calculate_shanten(TilesConverter.to_34_array(self.player.tiles))
+
+ def draw_tile(self, tile_136):
+ self.determine_strategy(self.player.tiles)
- def discard_tile(self, discard_tile):
+ def discard_tile(self, discard_tile, print_log=True):
# we called meld and we had discard tile that we wanted to discard
if discard_tile is not None:
if not self.last_discard_option:
return discard_tile
- return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True)
-
- results, shanten = self.calculate_outs(self.player.tiles,
- self.player.closed_hand,
- self.player.open_hand_34_tiles)
-
- selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten)
-
- # bot think that there is a threat on the table
- # and better to fold
- # if we can't find safe tiles, let's continue to build our hand
- if self.defence.should_go_to_defence_mode(selected_tile):
- if not self.in_defence:
- logger.info('We decided to fold against other players')
- self.in_defence = True
-
- defence_tile = self.defence.try_to_find_safe_tile_to_discard(results)
- if defence_tile:
- return self.process_discard_option(defence_tile, self.player.closed_hand)
- else:
- self.in_defence = False
-
- return self.process_discard_option(selected_tile, self.player.closed_hand)
-
- def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False):
- tiles_34 = TilesConverter.to_34_array(self.player.tiles)
-
- # we had to update tiles value there
- # because it is related with shanten number
- for result in results:
- result.tiles_count = self.count_tiles(result.waiting, tiles_34)
- result.calculate_value(shanten)
-
- # current strategy can affect on our discard options
- # so, don't use strategy specific choices for calling riichi
- if self.current_strategy:
- results = self.current_strategy.determine_what_to_discard(self.player.closed_hand,
- results,
- shanten,
- False,
- None,
- had_was_open)
-
- return self.chose_tile_to_discard(results)
-
- def calculate_outs(self, tiles, closed_hand, open_sets_34=None):
- """
- :param tiles: array of tiles in 136 format
- :param closed_hand: array of tiles in 136 format
- :param open_sets_34: array of array with tiles in 34 format
- :return:
- """
- tiles_34 = TilesConverter.to_34_array(tiles)
- closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
- is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles)
-
- results = []
- for hand_tile in range(0, 34):
- if not closed_tiles_34[hand_tile]:
- continue
-
- tiles_34[hand_tile] -= 1
-
- shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34)
-
- waiting = []
- for j in range(0, 34):
- if hand_tile == j or tiles_34[j] == 4:
- continue
-
- tiles_34[j] += 1
- if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1:
- waiting.append(j)
- tiles_34[j] -= 1
-
- tiles_34[hand_tile] += 1
+ return self.hand_builder.process_discard_option(self.last_discard_option, self.player.closed_hand, True)
- if waiting:
- results.append(DiscardOption(player=self.player,
- shanten=shanten,
- tile_to_discard=hand_tile,
- waiting=waiting,
- tiles_count=self.count_tiles(waiting, tiles_34)))
+ return self.hand_builder.discard_tile(
+ self.player.tiles,
+ self.player.closed_hand,
+ self.player.melds,
+ print_log
+ )
- if is_agari:
- shanten = Shanten.AGARI_STATE
- else:
- shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34)
+ def try_to_call_meld(self, tile_136, is_kamicha_discard):
+ tiles_136_previous = self.player.tiles[:]
+ tiles_136 = tiles_136_previous + [tile_136]
+ self.determine_strategy(tiles_136)
- return results, shanten
+ if not self.current_strategy:
+ return None, None
- def count_tiles(self, waiting, tiles_34):
- n = 0
- for item in waiting:
- n += 4 - self.player.total_tiles(item, tiles_34)
- return n
+ tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous)
+ previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34_previous, self.player.meld_34_tiles)
- def try_to_call_meld(self, tile, is_kamicha_discard):
- if not self.current_strategy:
+ if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari():
return None, None
- meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard)
- tile_to_discard = None
+ meld, discard_option = self.current_strategy.try_to_call_meld(tile_136, is_kamicha_discard, tiles_136)
if discard_option:
self.last_discard_option = discard_option
- tile_to_discard = discard_option.tile_to_discard
- return meld, tile_to_discard
+ DecisionsLogger.debug(log.MELD_CALL, 'Try to call meld', context=[
+ 'Hand: {}'.format(self.player.format_hand_for_print(tile_136)),
+ 'Meld: {}'.format(meld),
+ 'Discard after meld: {}'.format(discard_option)
+ ])
+
+ return meld, discard_option
- def determine_strategy(self):
+ def determine_strategy(self, tiles_136):
# for already opened hand we don't need to give up on selected strategy
if self.player.is_open_hand and self.current_strategy:
return False
@@ -193,97 +133,42 @@ def determine_strategy(self):
old_strategy = self.current_strategy
self.current_strategy = None
- # order is important
- strategies = [
- YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player),
- HonitsuStrategy(BaseStrategy.HONITSU, self.player),
- ]
+ # order is important, we add strategies with the highest priority first
+ strategies = []
if self.player.table.has_open_tanyao:
strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player))
+ strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player))
+ strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player))
+ strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player))
+
+ strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player))
+ strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player))
+
for strategy in strategies:
- if strategy.should_activate_strategy():
+ if strategy.should_activate_strategy(tiles_136):
self.current_strategy = strategy
+ break
if self.current_strategy:
if not old_strategy or self.current_strategy.type != old_strategy.type:
- message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy)
- if old_strategy:
- message += ' from {}'.format(old_strategy)
- logger.debug(message)
- logger.debug('With hand: {}'.format(TilesConverter.to_one_line_string(self.player.tiles)))
+ DecisionsLogger.debug(
+ log.STRATEGY_ACTIVATE,
+ context=self.current_strategy,
+ )
if not self.current_strategy and old_strategy:
- logger.debug('{} gave up on {}'.format(self.player.name, old_strategy))
+ DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy)
return self.current_strategy and True or False
- def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
- """
- Try to find best tile to discard, based on different valuations
- """
-
- def sorting(x):
- # - is important for x.tiles_count
- # in that case we will discard tile that will give for us more tiles
- # to complete a hand
- return x.shanten, -x.tiles_count, x.valuation
-
- had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded]
- if had_to_be_discarded_tiles:
- had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting)
- selected_tile = had_to_be_discarded_tiles[0]
- else:
- results = sorted(results, key=sorting)
- # remove needed tiles from discard options
- results = [x for x in results if not x.had_to_be_saved]
-
- # let's chose most valuable tile first
- temp_tile = results[0]
- # and let's find all tiles with same shanten
- results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten]
- possible_options = [temp_tile]
- for discard_option in results_with_same_shanten:
- # there is no sense to check already chosen tile
- if discard_option.tile_to_discard == temp_tile.tile_to_discard:
- continue
-
- # we don't need to select tiles almost dead waits
- if discard_option.tiles_count <= 2:
- continue
-
- # let's check all other tiles with same shanten
- # maybe we can find tiles that have almost same tiles count number
- if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2:
- possible_options.append(discard_option)
-
- # let's sort got tiles by value and let's chose less valuable tile to discard
- possible_options = sorted(possible_options, key=lambda x: x.valuation)
- selected_tile = possible_options[0]
-
- return selected_tile
-
- def process_discard_option(self, discard_option, closed_hand, force_discard=False):
- self.waiting = discard_option.waiting
- self.player.ai.previous_shanten = discard_option.shanten
- self.player.in_tempai = self.player.ai.previous_shanten == 0
-
- # when we called meld we don't need "smart" discard
- if force_discard:
- return discard_option.find_tile_in_hand(closed_hand)
-
- last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None
- if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
- return self.player.last_draw
- else:
- return discard_option.find_tile_in_hand(closed_hand)
-
- def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
+ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False, is_tsumo=False):
"""
:param win_tile: 34 tile format
:param tiles:
:param call_riichi:
+ :param is_tsumo
:return:
"""
win_tile *= 4
@@ -293,16 +178,17 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
win_tile += 1
if not tiles:
- tiles = self.player.tiles
+ tiles = copy.copy(self.player.tiles)
tiles += [win_tile]
config = HandConfig(
is_riichi=call_riichi,
player_wind=self.player.player_wind,
- round_wind=self.player.table.round_wind,
+ round_wind=self.player.table.round_wind_tile,
has_aka_dora=self.player.table.has_aka_dora,
- has_open_tanyao=self.player.table.has_open_tanyao
+ has_open_tanyao=self.player.table.has_open_tanyao,
+ is_tsumo=is_tsumo,
)
result = self.finished_hand.estimate_hand_value(tiles,
@@ -313,49 +199,22 @@ def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
return result
def should_call_riichi(self):
- # empty waiting can be found in some cases
- if not self.waiting:
- return False
-
- if self.in_defence:
- return False
-
- # we have a good wait, let's riichi
- if len(self.waiting) > 1:
- return True
-
- waiting = self.waiting[0]
- tiles = self.player.closed_hand + [waiting * 4]
- closed_melds = [x for x in self.player.melds if not x.opened]
- for meld in closed_melds:
- tiles.extend(meld.tiles[:3])
-
- tiles_34 = TilesConverter.to_34_array(tiles)
-
- results = self.hand_divider.divide_hand(tiles_34)
- result = results[0]
-
- count_of_pairs = len([x for x in result if is_pair(x)])
- # with chitoitsu we can call a riichi with pair wait
- if count_of_pairs == 7:
- return True
-
- for hand_set in result:
- # better to not call a riichi for a pair wait
- # it can be easily improved
- if is_pair(hand_set) and waiting in hand_set:
- return False
-
- return True
+ return self.riichi.should_call_riichi()
- def should_call_kan(self, tile, open_kan):
+ def should_call_kan(self, tile, open_kan, from_riichi=False):
"""
Method will decide should we call a kan,
or upgrade pon to kan
:param tile: 136 tile format
:param open_kan: boolean
+ :param from_riichi: boolean
:return: kan type
"""
+
+ # we can't call kan on the latest tile
+ if self.table.count_of_remaining_tiles <= 1:
+ return None
+
# we don't need to add dora for other players
if self.player.ai.in_defence:
return None
@@ -375,39 +234,85 @@ def should_call_kan(self, tile, open_kan):
tile_34 = tile // 4
tiles_34 = TilesConverter.to_34_array(self.player.tiles)
+
closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
- pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)]
+
+ melds_34 = copy.copy(self.player.meld_34_tiles)
+ tiles = copy.copy(self.player.tiles)
+ closed_hand_tiles = copy.copy(self.player.closed_hand)
+
+ new_shanten = 0
+ previous_shanten = 0
+ new_waits_count = 0
+ previous_waits_count = 0
# let's check can we upgrade opened pon to the kan
- if pon_melds:
- for meld in pon_melds:
- # tile is equal to our already opened pon,
- # so let's call chankan!
- if tile_34 in meld:
- return Meld.CHANKAN
-
- count_of_needed_tiles = 4
- # for open kan 3 tiles is enough to call a kan
- if open_kan:
- count_of_needed_tiles = 3
-
- # we have 3 tiles in our hand,
- # so we can try to call closed meld
- if closed_hand_34[tile_34] == count_of_needed_tiles:
- if not open_kan:
- # to correctly count shanten in the hand
- # we had do subtract drown tile
+ pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)]
+ has_shouminkan_candidate = False
+ for meld in pon_melds:
+ # tile is equal to our already opened pon
+ if tile_34 in meld:
+ has_shouminkan_candidate = True
+
+ tiles.append(tile)
+ closed_hand_tiles.append(tile)
+
+ previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
+ tiles,
+ closed_hand_tiles,
+ self.player.melds
+ )
+
+ tiles_34 = TilesConverter.to_34_array(tiles)
tiles_34[tile_34] -= 1
- melds = self.player.open_hand_34_tiles
- previous_shanten = self.shanten.calculate_shanten(tiles_34, melds)
+ new_waiting, new_shanten = self.hand_builder.calculate_waits(
+ tiles_34,
+ self.player.meld_34_tiles
+ )
+ new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34)
+
+ if not has_shouminkan_candidate:
+ # we don't have enough tiles in the hand
+ if closed_hand_34[tile_34] != 3:
+ return None
+
+ if open_kan or from_riichi:
+ # this 4 tiles can only be used in kan, no other options
+ previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
+ previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34)
+ else:
+ tiles.append(tile)
+ closed_hand_tiles.append(tile)
+
+ previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
+ tiles,
+ closed_hand_tiles,
+ self.player.melds
+ )
+
+ # shanten calculator doesn't like working with kans, so we pretend it's a pon
+ melds_34 += [[tile_34, tile_34, tile_34]]
+ new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
+
+ closed_hand_34[tile_34] = 4
+ new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34)
+
+ # it is possible that we don't have results here
+ # when we are in agari state (but without yaku)
+ if previous_shanten is None:
+ return None
+
+ # it is not possible to reduce number of shanten by calling a kan
+ assert new_shanten >= previous_shanten
- melds += [[tile_34, tile_34, tile_34]]
- new_shanten = self.shanten.calculate_shanten(tiles_34, melds)
+ # if shanten number is the same, we should only call kan if ukeire didn't become worse
+ if new_shanten == previous_shanten:
+ # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall)
+ assert new_waits_count <= previous_waits_count
- # called kan will not ruin our hand
- if new_shanten <= previous_shanten:
- return Meld.KAN
+ if new_waits_count == previous_waits_count:
+ return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN
return None
@@ -420,8 +325,10 @@ def enemy_called_riichi(self, enemy_seat):
it is affect open hand decisions
:return:
"""
+
if self.defence.should_go_to_defence_mode():
self.in_defence = True
+ DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
@property
def enemy_players(self):
@@ -429,3 +336,21 @@ def enemy_players(self):
Return list of players except our bot
"""
return self.player.table.players[1:]
+
+ def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds):
+ previous_results, previous_shanten = self.hand_builder.find_discard_options(
+ tiles,
+ closed_hand_tiles,
+ melds
+ )
+
+ previous_results = [x for x in previous_results if x.shanten == previous_shanten]
+
+ # it is possible that we don't have results here
+ # when we are in agari state (but without yaku)
+ if not previous_results:
+ return None, None
+
+ previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire
+
+ return previous_shanten, previous_waits_cnt
diff --git a/project/game/ai/first_version/riichi.py b/project/game/ai/first_version/riichi.py
new file mode 100644
index 00000000..321037f0
--- /dev/null
+++ b/project/game/ai/first_version/riichi.py
@@ -0,0 +1,224 @@
+from mahjong.tile import TilesConverter
+from mahjong.utils import is_honor, simplify, is_pair, is_chi
+
+
+class Riichi:
+
+ def __init__(self, player):
+ self.player = player
+
+ def should_call_riichi(self):
+ # empty waiting can be found in some cases
+ if not self.player.ai.waiting:
+ return False
+
+ if self.player.ai.in_defence:
+ return False
+
+ # don't call karaten riichi
+ count_tiles = self.player.ai.hand_builder.count_tiles(
+ self.player.ai.waiting,
+ TilesConverter.to_34_array(self.player.closed_hand)
+ )
+ if count_tiles == 0:
+ return False
+
+ # It is daburi!
+ first_discard = self.player.round_step == 1
+ if first_discard and not self.player.table.meld_was_called:
+ return True
+
+ if len(self.player.ai.waiting) == 1:
+ return self._should_call_riichi_one_sided()
+
+ return self._should_call_riichi_many_sided()
+
+ def _should_call_riichi_one_sided(self):
+ count_tiles = self.player.ai.hand_builder.count_tiles(
+ self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand)
+ )
+ waiting = self.player.ai.waiting[0]
+ hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)
+
+ tiles = self.player.closed_hand.copy()
+ closed_melds = [x for x in self.player.melds if not x.opened]
+ for meld in closed_melds:
+ tiles.extend(meld.tiles[:3])
+
+ results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting)
+ result = results[0]
+
+ closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
+
+ have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting)
+
+ # what if we have yaku
+ if hand_value.yaku is not None and hand_value.cost is not None:
+ min_cost = hand_value.cost['main']
+
+ # tanki honor is a good wait, let's damaten only if hand is already expensive
+ if is_honor(waiting):
+ if self.player.is_dealer and min_cost < 12000:
+ return True
+
+ if not self.player.is_dealer and min_cost < 8000:
+ return True
+
+ return False
+
+ is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7
+ simplified_waiting = simplify(waiting)
+
+ for hand_set in result:
+ if waiting not in hand_set:
+ continue
+
+ # tanki wait but not chiitoitsu
+ if is_pair(hand_set) and not is_chiitoitsu:
+ # let's not riichi tanki 4, 5, 6
+ if 3 <= simplified_waiting <= 5:
+ return False
+
+ # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile
+ if count_tiles == 1:
+ return False
+
+ # don't riichi 2378 tanki if hand has good value
+ if simplified_waiting != 0 and simplified_waiting != 8:
+ if self.player.is_dealer and min_cost >= 7700:
+ return False
+
+ if not self.player.is_dealer and min_cost >= 5200:
+ return False
+
+ # only riichi if we have suji-trab or there is kabe
+ if not have_suji and not have_kabe:
+ return False
+
+ return True
+
+ # tanki wait with chiitoitsu
+ if is_pair(hand_set) and is_chiitoitsu:
+ # chiitoitsu on last suit tile is no the best
+ if count_tiles == 1:
+ return False
+
+ # only riichi if we have suji-trab or there is kabe
+ if not have_suji and not have_kabe:
+ return False
+
+ return True
+
+ # 1-sided wait means kanchan or penchan
+ if is_chi(hand_set):
+ # let's not riichi kanchan on 4, 5, 6
+ if 3 <= simplified_waiting <= 5:
+ return False
+
+ # now checking waiting for 2, 3, 7, 8
+ # if we only have 1 tile to wait for, let's damaten
+ if count_tiles == 1:
+ return False
+
+ # if we have 2 tiles to wait for and hand cost is good without riichi,
+ # let's damaten
+ if count_tiles == 2:
+ if self.player.is_dealer and min_cost >= 7700:
+ return False
+
+ if not self.player.is_dealer and min_cost >= 5200:
+ return False
+
+ # only riichi if we have suji-trab or there is kabe
+ if not have_suji and not have_kabe:
+ return False
+
+ return True
+
+ # what if we don't have yaku
+ # our tanki wait is good, let's riichi
+ if is_honor(waiting):
+ return True
+
+ simplified_waiting = simplify(waiting)
+
+ for hand_set in result:
+ if waiting not in hand_set:
+ continue
+
+ if is_pair(hand_set):
+ # let's not riichi tanki wait without suji-trap or kabe
+ if not have_suji and not have_kabe:
+ return False
+
+ # let's not riichi tanki on last suit tile if it's early
+ if count_tiles == 1 and self.player.round_step < 6:
+ return False
+
+ # let's not riichi tanki 4, 5, 6 if it's early
+ if 3 <= simplified_waiting <= 5 and self.player.round_step < 6:
+ return False
+
+ # 1-sided wait means kanchan or penchan
+ if is_chi(hand_set):
+ # let's only riichi this bad wait if
+ # it has all 4 tiles available or it
+ # it's not too early
+ if 4 <= simplified_waiting <= 6:
+ return count_tiles == 4 or self.player.round_step >= 6
+
+ return True
+
+ def _should_call_riichi_many_sided(self):
+ count_tiles = self.player.ai.hand_builder.count_tiles(
+ self.player.ai.waiting,
+ TilesConverter.to_34_array(self.player.closed_hand)
+ )
+ hand_costs = []
+ waits_with_yaku = 0
+ for waiting in self.player.ai.waiting:
+ hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)
+ if hand_value.error is None:
+ hand_costs.append(hand_value.cost['main'])
+ if hand_value.yaku is not None and hand_value.cost is not None:
+ waits_with_yaku += 1
+
+ # if we have yaku on every wait
+ if waits_with_yaku == len(self.player.ai.waiting):
+ min_cost = min(hand_costs)
+
+ # let's not riichi this bad wait
+ if count_tiles <= 2:
+ return False
+
+ # if wait is slighly better, we will riichi only a cheap hand
+ if count_tiles <= 4:
+ if self.player.is_dealer and min_cost >= 7700:
+ return False
+
+ if not self.player.is_dealer and min_cost >= 5200:
+ return False
+
+ return True
+
+ # wait is even better, but still don't call riichi on damaten mangan
+ if count_tiles <= 6:
+ if self.player.is_dealer and min_cost >= 11600:
+ return False
+
+ if not self.player.is_dealer and min_cost >= 7700:
+ return False
+
+ return True
+
+ # if wait is good we only damaten haneman
+ if self.player.is_dealer and min_cost >= 18000:
+ return False
+
+ if not self.player.is_dealer and min_cost >= 12000:
+ return False
+
+ return True
+
+ # if we don't have yaku on every wait and it's two-sided or more, we call riichi
+ return True
diff --git a/project/game/ai/first_version/strategies/chiitoitsu.py b/project/game/ai/first_version/strategies/chiitoitsu.py
new file mode 100644
index 00000000..fb6c1d14
--- /dev/null
+++ b/project/game/ai/first_version/strategies/chiitoitsu.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+from mahjong.tile import TilesConverter
+
+from game.ai.first_version.strategies.main import BaseStrategy
+
+
+class ChiitoitsuStrategy(BaseStrategy):
+ min_shanten = 2
+
+ def should_activate_strategy(self, tiles_136):
+ """
+ We can go for chiitoitsu strategy if we have 5 pairs
+ """
+
+ result = super(ChiitoitsuStrategy, self).should_activate_strategy(tiles_136)
+ if not result:
+ return False
+
+ tiles_34 = TilesConverter.to_34_array(self.player.tiles)
+
+ num_pairs = len([x for x in range(0, 34) if tiles_34[x] == 2])
+ num_pons = len([x for x in range(0, 34) if tiles_34[x] == 3])
+
+ # for now we don't consider chiitoitsu with less than 5 pair
+ if num_pairs < 5:
+ return False
+
+ # if we have 5 pairs and tempai, this is obviously not chiitoitsu
+ if num_pairs == 5 and self.player.ai.shanten == 0:
+ return False
+
+ # for now we won't go for chiitoitsu if we have 5 pairs and pon
+ if num_pairs == 5 and num_pons > 0:
+ return False
+
+ return True
+
+ def is_tile_suitable(self, tile):
+ """
+ For chiitoitsu we don't have any limits
+ :param tile: 136 tiles format
+ :return: True
+ """
+ return True
+
+ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles):
+ """
+ Never meld with chiitoitsu
+ :param tile: 136 format tile
+ :param is_kamicha_discard: boolean
+ :param new_tiles:
+ :return: Meld and DiscardOption objects
+ """
+ return None, None
diff --git a/project/game/ai/first_version/strategies/chinitsu.py b/project/game/ai/first_version/strategies/chinitsu.py
new file mode 100644
index 00000000..7de7074c
--- /dev/null
+++ b/project/game/ai/first_version/strategies/chinitsu.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+from mahjong.tile import TilesConverter
+from mahjong.utils import count_tiles_by_suits, is_tile_strictly_isolated
+from mahjong.utils import is_man, is_pin, is_sou, plus_dora, is_aka_dora, is_honor
+
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.ai.first_version.strategies.honitsu import HonitsuStrategy
+
+
+class ChinitsuStrategy(BaseStrategy):
+ min_shanten = 4
+
+ chosen_suit = None
+
+ dora_count_suitable = 0
+ dora_count_not_suitable = 0
+
+ def should_activate_strategy(self, tiles_136):
+ """
+ We can go for chinitsu strategy if we have prevalence of one suit
+ """
+
+ result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136)
+ if not result:
+ return False
+
+ # when making decisions about chinitsu, we should consider
+ # the state of our own hand,
+ tiles_34 = TilesConverter.to_34_array(self.player.tiles)
+ suits = count_tiles_by_suits(tiles_34)
+
+ suits = [x for x in suits if x['name'] != 'honor']
+ suits = sorted(suits, key=lambda x: x['count'], reverse=True)
+ suit = suits[0]
+
+ count_of_shuntsu_other_suits = 0
+ count_of_koutsu_other_suits = 0
+
+ count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[1]['function'])
+ count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[2]['function'])
+
+ count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[1]['function'])
+ count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[2]['function'])
+
+ # we need to have at least 9 tiles of one suit to fo for chinitsu
+ if suit['count'] < 9:
+ return False
+
+ # here we only check doras in different suits, we will deal
+ # with honors later
+ self._initialize_chinitsu_dora_count(tiles_136, suit)
+
+ # 3 non-isolated doras in other suits is too much
+ # to even try
+ if self.dora_count_not_suitable >= 3:
+ return False
+
+ if self.dora_count_not_suitable == 2:
+ # 2 doras in other suits, no doras in our suit
+ # let's not consider chinitsu
+ if self.dora_count_suitable == 0:
+ return False
+
+ # we have 2 doras in other suits and we
+ # are 1 shanten, let's not rush chinitsu
+ if self.player.ai.shanten == 1:
+ return False
+
+ # too late to get rid of doras in other suits
+ if self.player.round_step > 8:
+ return False
+
+ # we are almost tempai, chinitsu is slower
+ if suit['count'] == 9 and self.player.ai.shanten == 1:
+ return False
+
+ # only 10 tiles by 8th turn is too slow, considering alternative
+ if suit['count'] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8:
+ return False
+
+ # if we have a pon of honors, let's not go for chinitsu
+ honor_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3])
+ if honor_pons >= 1:
+ return False
+
+ # if we have a valued pair, let's not go for chinitsu
+ valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2])
+ if valued_pairs >= 1:
+ return False
+
+ # if we have a pair of honor doras, let's not go for chinitsu
+ honor_doras_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2
+ and plus_dora(x * 4, self.player.table.dora_indicators)])
+ if honor_doras_pairs >= 1:
+ return False
+
+ # if we have a honor pair, we will only throw them away if it's early in the game
+ # and if we have lots of tiles in our suit
+ honor_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2])
+ if honor_pairs >= 2:
+ return False
+ if honor_pairs == 1:
+ if suit['count'] < 11:
+ return False
+ if self.player.round_step > 8:
+ return False
+
+ # if we have a complete set in other suits, we can only throw it away if it's early in the game
+ if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1:
+ # too late to throw away chi after 8 step
+ if self.player.round_step > 8:
+ return False
+
+ # already 1 shanten, no need to throw away complete set
+ if self.player.round_step > 5 and self.player.ai.shanten == 1:
+ return False
+
+ # dora is not isolated and we have a complete set, let's not go for chinitsu
+ if self.dora_count_not_suitable >= 1:
+ return False
+
+ self.chosen_suit = suit['function']
+
+ return True
+
+ def is_tile_suitable(self, tile):
+ """
+ We can use only tiles of chosen suit and honor tiles
+ :param tile: 136 tiles format
+ :return: True
+ """
+ tile //= 4
+ return self.chosen_suit(tile)
+
+ def _initialize_chinitsu_dora_count(self, tiles_136, suit):
+ tiles_34 = TilesConverter.to_34_array(tiles_136)
+
+ dora_count_man = 0
+ dora_count_man_not_isolated = 0
+ dora_count_pin = 0
+ dora_count_pin_not_isolated = 0
+ dora_count_sou = 0
+ dora_count_sou_not_isolated = 0
+
+ for tile_136 in tiles_136:
+ tile_34 = tile_136 // 4
+
+ dora_count = plus_dora(tile_136, self.player.table.dora_indicators)
+
+ if is_aka_dora(tile_136, self.player.table.has_aka_dora):
+ dora_count += 1
+
+ if is_man(tile_34):
+ dora_count_man += dora_count
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_man_not_isolated += dora_count
+
+ if is_pin(tile_34):
+ dora_count_pin += dora_count
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_pin_not_isolated += dora_count
+
+ if is_sou(tile_34):
+ dora_count_sou += dora_count
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_sou_not_isolated += dora_count
+
+ if suit['name'] == 'pin':
+ self.dora_count_suitable = dora_count_pin
+ self.dora_count_not_suitable = dora_count_man_not_isolated + dora_count_sou_not_isolated
+ elif suit['name'] == 'sou':
+ self.dora_count_suitable = dora_count_sou
+ self.dora_count_not_suitable = dora_count_man_not_isolated + dora_count_pin_not_isolated
+ elif suit['name'] == 'man':
+ self.dora_count_suitable = dora_count_man
+ self.dora_count_not_suitable = dora_count_sou_not_isolated + dora_count_pin_not_isolated
diff --git a/project/game/ai/first_version/strategies/formal_tempai.py b/project/game/ai/first_version/strategies/formal_tempai.py
new file mode 100644
index 00000000..6b40dce0
--- /dev/null
+++ b/project/game/ai/first_version/strategies/formal_tempai.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+from game.ai.first_version.strategies.main import BaseStrategy
+
+
+class FormalTempaiStrategy(BaseStrategy):
+
+ def should_activate_strategy(self, tiles_136):
+ """
+ When we get closer to the end of the round, we start to consider
+ going for formal tempai.
+ """
+
+ result = super(FormalTempaiStrategy, self).should_activate_strategy(tiles_136)
+ if not result:
+ return False
+
+ # if we already in tempai, we don't need this strategy
+ if self.player.in_tempai:
+ return False
+
+ # it's too early to go for formal tempai before 11th turn
+ if self.player.round_step < 11:
+ return False
+
+ # it's 11th turn or later and we still have 3 shanten or more,
+ # let's try to go for formal tempai at least
+ if self.player.ai.shanten >= 3:
+ return True
+
+ if self.player.ai.shanten == 2:
+ if self.dora_count_total < 2:
+ # having 0 or 1 dora and 2 shanten, let's go for formal tempai
+ # starting from 11th turn
+ return True
+ # having 2 or more doras and 2 shanten, let's go for formal
+ # tempai starting from 12th turn
+ return self.player.round_step >= 12
+
+ # for 1 shanten we check number of doras and ukeire to determine
+ # correct time to go for formal tempai
+ if self.player.ai.shanten == 1:
+ if self.dora_count_total == 0:
+ if self.player.ai.ukeire <= 16:
+ return True
+
+ if self.player.ai.ukeire <= 28:
+ return self.player.round_step >= 12
+
+ return self.player.round_step >= 13
+
+ if self.dora_count_total == 1:
+ if self.player.ai.ukeire <= 16:
+ return self.player.round_step >= 12
+
+ if self.player.ai.ukeire <= 28:
+ return self.player.round_step >= 13
+
+ return self.player.round_step >= 14
+
+ if self.player.ai.ukeire <= 16:
+ return self.player.round_step >= 13
+
+ return self.player.round_step >= 14
+
+ # we actually never reach here
+ return False
+
+ def is_tile_suitable(self, tile):
+ """
+ All tiles are suitable for formal tempai.
+ :param tile: 136 tiles format
+ :return: True
+ """
+ return True
diff --git a/project/game/ai/first_version/strategies/honitsu.py b/project/game/ai/first_version/strategies/honitsu.py
index a9048194..dd95fdd5 100644
--- a/project/game/ai/first_version/strategies/honitsu.py
+++ b/project/game/ai/first_version/strategies/honitsu.py
@@ -1,56 +1,119 @@
# -*- coding: utf-8 -*-
from mahjong.tile import TilesConverter
-from mahjong.utils import count_tiles_by_suits, is_honor, simplify
+from mahjong.utils import count_tiles_by_suits, simplify, is_tile_strictly_isolated
+from mahjong.utils import is_man, is_pin, is_sou, plus_dora, is_aka_dora, is_honor
from game.ai.first_version.strategies.main import BaseStrategy
class HonitsuStrategy(BaseStrategy):
- REQUIRED_TILES = 10
min_shanten = 4
chosen_suit = None
- def should_activate_strategy(self):
+ dora_count_other_suits_not_isolated = 0
+ tiles_count_other_suits = 0
+ tiles_count_other_suits_not_isolated = 0
+
+ def should_activate_strategy(self, tiles_136):
"""
- We can go for honitsu/chinitsu strategy if we have prevalence of one suit and honor tiles
- :return: boolean
+ We can go for honitsu strategy if we have prevalence of one suit and honor tiles
"""
- result = super(HonitsuStrategy, self).should_activate_strategy()
+ result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136)
if not result:
return False
- tiles_34 = TilesConverter.to_34_array(self.player.tiles)
+ tiles_34 = TilesConverter.to_34_array(tiles_136)
suits = count_tiles_by_suits(tiles_34)
- honor = [x for x in suits if x['name'] == 'honor'][0]
suits = [x for x in suits if x['name'] != 'honor']
suits = sorted(suits, key=lambda x: x['count'], reverse=True)
suit = suits[0]
- count_of_pairs = 0
- for x in range(0, 34):
- if tiles_34[x] >= 2:
- count_of_pairs += 1
- suits.remove(suit)
- count_of_ryanmens = self._find_ryanmen_waits(tiles_34, suits[0]['function'])
- count_of_ryanmens += self._find_ryanmen_waits(tiles_34, suits[1]['function'])
+ count_of_shuntsu_other_suits = 0
+ count_of_koutsu_other_suits = 0
+
+ count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]['function'])
+ count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]['function'])
+
+ count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]['function'])
+ count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]['function'])
- # it is a bad idea go for honitsu with ryanmen in other suit
- if count_of_ryanmens > 0 and not self.player.is_open_hand:
+ self._calculate_not_suitable_tiles_cnt(tiles_34, suit['function'])
+ self._initialize_honitsu_dora_count(tiles_136, suit)
+
+ # let's not go for honitsu if we have 5 or more non-isolated
+ # tiles in other suits
+ if self.tiles_count_other_suits >= 5:
return False
- # we need to have prevalence of one suit and completed forms in the hand
- # for now let's check only pairs in the hand
- # TODO check ryanmen forms as well and honor tiles count
- if suit['count'] + honor['count'] >= HonitsuStrategy.REQUIRED_TILES:
- self.chosen_suit = suit['function']
- return count_of_pairs > 0
- else:
+ # let's not go for honitsu if we have 2 or more non-isolated doras
+ # in other suits
+ if self.dora_count_other_suits_not_isolated >= 2:
return False
+ # if we have a pon of valued doras, let's not go for honitsu
+ # we have a mangan anyway, let's go for fastest hand
+ valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3]
+ for pon in valued_pons:
+ dora_count = plus_dora(pon * 4, self.player.table.dora_indicators)
+ if dora_count > 0:
+ return False
+
+ valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2])
+ honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2])
+ honor_doras_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2
+ and plus_dora(x * 4, self.player.table.dora_indicators)])
+ unvalued_singles = len([x for x in range(0, 34) if is_honor(x)
+ and x not in self.player.valued_honors
+ and tiles_34[x] == 1])
+
+ # if we have some decent amount of not isolated tiles in other suits
+ # we may not rush for honitsu considering other conditions
+ if self.tiles_count_other_suits_not_isolated >= 3:
+ # if we don't have pair or pon of honored doras
+ if honor_doras_pairs_or_pons == 0:
+ # we need to either have a valued pair or have at least two honor
+ # pairs to consider honitsu
+ if valued_pairs == 0 and honor_pairs_or_pons < 2:
+ return False
+
+ # doesn't matter valued or not, if we have just one honor pair
+ # and have some single unvalued tiles, let's throw them away
+ # first
+ if honor_pairs_or_pons == 1 and unvalued_singles >= 2:
+ return False
+
+ # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn
+ # let's not consider honitsu here
+ if self.player.ai.shanten == 1 and self.player.round_step > 8:
+ return False
+ else:
+ # we have a pon of unvalued honor doras, but it looks like
+ # it's faster to build our hand without honitsu
+ if self.player.ai.shanten == 1:
+ return False
+
+ # if we have a complete set in other suits, we can only throw it away if it's early in the game
+ if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1:
+ # too late to throw away chi after 8 step
+ if self.player.round_step > 8:
+ return False
+
+ # already 1 shanten, no need to throw away complete set
+ if self.player.ai.shanten == 1:
+ return False
+
+ # dora is not isolated and we have a complete set, let's not go for honitsu
+ if self.dora_count_other_suits_not_isolated >= 1:
+ return False
+
+ self.chosen_suit = suit['function']
+
+ return True
+
def is_tile_suitable(self, tile):
"""
We can use only tiles of chosen suit and honor tiles
@@ -60,7 +123,71 @@ def is_tile_suitable(self, tile):
tile //= 4
return self.chosen_suit(tile) or is_honor(tile)
- def _find_ryanmen_waits(self, tiles, suit):
+ def meld_had_to_be_called(self, tile):
+ has_not_suitable_tiles = False
+
+ for hand_tile in self.player.tiles:
+ if not self.is_tile_suitable(hand_tile):
+ has_not_suitable_tiles = True
+ break
+
+ # if we still have unsuitable tiles, let's call honor pons
+ # even if they don't change number of shanten
+ if has_not_suitable_tiles and is_honor(tile // 4):
+ return True
+
+ return False
+
+ def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit):
+ self.tiles_count_other_suits = 0
+ self.tiles_count_other_suits_not_isolated = 0
+
+ for x in range(0, 34):
+ tile = tiles_34[x]
+ if not tile:
+ continue
+
+ if not suit(x) and not is_honor(x):
+ self.tiles_count_other_suits += tile
+ if not is_tile_strictly_isolated(tiles_34, x):
+ self.tiles_count_other_suits_not_isolated += tile
+
+ def _initialize_honitsu_dora_count(self, tiles_136, suit):
+ tiles_34 = TilesConverter.to_34_array(tiles_136)
+
+ dora_count_man_not_isolated = 0
+ dora_count_pin_not_isolated = 0
+ dora_count_sou_not_isolated = 0
+
+ for tile_136 in tiles_136:
+ tile_34 = tile_136 // 4
+
+ dora_count = plus_dora(tile_136, self.player.table.dora_indicators)
+
+ if is_aka_dora(tile_136, self.player.table.has_aka_dora):
+ dora_count += 1
+
+ if is_man(tile_34):
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_man_not_isolated += dora_count
+
+ if is_pin(tile_34):
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_pin_not_isolated += dora_count
+
+ if is_sou(tile_34):
+ if not is_tile_strictly_isolated(tiles_34, tile_34):
+ dora_count_sou_not_isolated += dora_count
+
+ if suit['name'] == 'pin':
+ self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
+ elif suit['name'] == 'sou':
+ self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
+ elif suit['name'] == 'man':
+ self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
+
+ @staticmethod
+ def _find_ryanmen_waits(tiles, suit):
suit_tiles = []
for x in range(0, 34):
tile = tiles[x]
@@ -86,3 +213,52 @@ def _find_ryanmen_waits(self, tiles, suit):
count_of_ryanmen_waits += 1
return count_of_ryanmen_waits
+
+ # we know we have no more that 5 tiles of other suit,
+ # so this is a simplified version
+ # be aware, that it will return 2 for 2345 form so use with care
+ @staticmethod
+ def _count_of_shuntsu(tiles, suit):
+ suit_tiles = []
+ for x in range(0, 34):
+ tile = tiles[x]
+ if not tile:
+ continue
+
+ if suit(x):
+ suit_tiles.append(x)
+
+ count_of_left_tiles = 0
+ count_of_middle_tiles = 0
+ count_of_right_tiles = 0
+
+ simple_tiles = [simplify(x) for x in suit_tiles]
+ for x in range(0, len(simple_tiles)):
+ tile = simple_tiles[x]
+
+ if tile + 1 in simple_tiles and tile + 2 in simple_tiles:
+ count_of_left_tiles += 1
+
+ if tile - 1 in simple_tiles and tile + 1 in simple_tiles:
+ count_of_middle_tiles += 1
+
+ if tile - 2 in simple_tiles and tile - 1 in simple_tiles:
+ count_of_right_tiles += 1
+
+ return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3
+
+ # we know we have no more that 5 tiles of other suit,
+ # so this is a simplified version
+ @staticmethod
+ def _count_of_koutsu(tiles, suit):
+ count_of_koutsu = 0
+
+ for x in range(0, 34):
+ tile = tiles[x]
+ if not tile:
+ continue
+
+ if suit(x) and tile >= 3:
+ count_of_koutsu += 1
+
+ return count_of_koutsu
diff --git a/project/game/ai/first_version/strategies/main.py b/project/game/ai/first_version/strategies/main.py
index f062d6fe..37824471 100644
--- a/project/game/ai/first_version/strategies/main.py
+++ b/project/game/ai/first_version/strategies/main.py
@@ -1,46 +1,76 @@
# -*- coding: utf-8 -*-
+import utils.decisions_constants as log
+
from mahjong.meld import Meld
from mahjong.tile import TilesConverter
-from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi
+from mahjong.utils import is_man, is_pin, is_sou, is_pon, is_chi, plus_dora, is_aka_dora, is_honor, is_terminal
+
+from utils.decisions_logger import DecisionsLogger
class BaseStrategy(object):
YAKUHAI = 0
HONITSU = 1
TANYAO = 2
+ FORMAL_TEMPAI = 3
+ CHINITSU = 4
+ CHIITOITSU = 5
TYPES = {
YAKUHAI: 'Yakuhai',
HONITSU: 'Honitsu',
TANYAO: 'Tanyao',
+ FORMAL_TEMPAI: 'Formal Tempai',
+ CHINITSU: 'Chinitsu',
+ CHIITOITSU: 'Chiitoitsu'
}
+ not_suitable_tiles = []
player = None
type = None
# number of shanten where we can start to open hand
min_shanten = 7
+ go_for_atodzuke = False
+
+ dora_count_total = 0
+ dora_count_central = 0
+ dora_count_not_central = 0
+ aka_dora_count = 0
+ dora_count_honor = 0
def __init__(self, strategy_type, player):
self.type = strategy_type
self.player = player
+ self.go_for_atodzuke = False
def __str__(self):
return self.TYPES[self.type]
- def should_activate_strategy(self):
+ def should_activate_strategy(self, tiles_136):
"""
Based on player hand and table situation
we can determine should we use this strategy or not.
- For now default rule for all strategies: don't open hand with 5+ pairs
+ :param: tiles_136
:return: boolean
"""
- if self.player.is_open_hand:
- return True
+ self.calculate_dora_count(tiles_136)
- tiles_34 = TilesConverter.to_34_array(self.player.tiles)
- count_of_pairs = len([x for x in range(0, 34) if tiles_34[x] >= 2])
+ return True
+
+ def can_meld_into_agari(self):
+ """
+ Is melding into agari allowed with this strategy
+ :return: boolean
+ """
+ # By default, the logic is the following: if we have any
+ # non-suitable tiles, we can meld into agari state, because we'll
+ # throw them away after meld.
+ # Otherwise, there is no point.
+ for tile in self.player.tiles:
+ if not self.is_tile_suitable(tile):
+ return True
- return count_of_pairs < 5
+ return False
def is_tile_suitable(self, tile):
"""
@@ -50,45 +80,29 @@ def is_tile_suitable(self, tile):
"""
raise NotImplemented()
- def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand,
- hand_was_open=False):
- """
-
- "for_open_hand" and "tile_for_open_hand" we had to use when we want to
- determine what melds will be open
-
- "hand_was_open" we will use in rare cases
- when we open hand and before meld was added to the player
- it happens between we send a message to tenhou and tenhou send confirmation message to us
- sometimes we failed to call a meld because of other player
-
- :param closed_hand: array of 136 tiles format
- :param outs_results: dict
- :param shanten: number of shanten
- :param for_open_hand: boolean
- :param tile_for_open_hand: 136 tile format
- :param hand_was_open: boolean
- :return: array of DiscardOption
- """
+ def determine_what_to_discard(self, discard_options, hand, open_melds):
+ first_option = sorted(discard_options, key=lambda x: x.shanten)[0]
+ shanten = first_option.shanten
# for riichi we don't need to discard useful tiles
if shanten == 0 and not self.player.is_open_hand:
- return outs_results
+ return discard_options
# mark all not suitable tiles as ready to discard
# even if they not should be discarded by uke-ire
- for j in outs_results:
- if not self.is_tile_suitable(j.tile_to_discard * 4):
- j.had_to_be_discarded = True
+ for x in discard_options:
+ if not self.is_tile_suitable(x.tile_to_discard * 4):
+ x.had_to_be_discarded = True
- return outs_results
+ return discard_options
- def try_to_call_meld(self, tile, is_kamicha_discard):
+ def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles):
"""
Determine should we call a meld or not.
If yes, it will return Meld object and tile to discard
:param tile: 136 format tile
:param is_kamicha_discard: boolean
+ :param new_tiles:
:return: Meld and DiscardOption objects
"""
if self.player.in_riichi:
@@ -108,7 +122,6 @@ def try_to_call_meld(self, tile, is_kamicha_discard):
return None, None
discarded_tile = tile // 4
- new_tiles = self.player.tiles[:] + [tile]
closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])
combinations = []
@@ -139,9 +152,12 @@ def try_to_call_meld(self, tile, is_kamicha_discard):
if second_limit > second_index:
second_limit = second_index
- combinations = self.player.ai.hand_divider.find_valid_combinations(closed_hand_34,
- first_limit,
- second_limit, True)
+ combinations = self.player.ai.hand_divider.find_valid_combinations(
+ closed_hand_34,
+ first_limit,
+ second_limit,
+ True
+ )
if combinations:
combinations = combinations[0]
@@ -170,93 +186,106 @@ def try_to_call_meld(self, tile, is_kamicha_discard):
if not possible_melds:
return None, None
- best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles)
- if best_meld_34:
- # we need to calculate count of shanten with supposed meld
- # to prevent bad hand openings
- melds = self.player.open_hand_34_tiles + [best_meld_34]
- outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds)
+ chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile)
+ selected_tile = chosen_meld['discard_tile']
+ meld = chosen_meld['meld']
- # each strategy can use their own value to min shanten number
- if shanten > self.min_shanten:
- return None, None
+ shanten = selected_tile.shanten
+ had_to_be_called = self.meld_had_to_be_called(tile)
+ had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded
- # we can't improve hand, so we don't need to open it
- if not outs_results:
- return None, None
+ # each strategy can use their own value to min shanten number
+ if shanten > self.min_shanten:
+ return None, None
+
+ # sometimes we had to call tile, even if it will not improve our hand
+ # otherwise we can call only with improvements of shanten
+ if not had_to_be_called and shanten >= self.player.ai.shanten:
+ return None, None
+
+ return meld, selected_tile
+
+ def meld_had_to_be_called(self, tile):
+ """
+ For special cases meld had to be called even if shanten number will not be increased
+ :param tile: in 136 tiles format
+ :return: boolean
+ """
+ return False
- # sometimes we had to call tile, even if it will not improve our hand
- # otherwise we can call only with improvements of shanten
- if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.previous_shanten:
- return None, None
+ def calculate_dora_count(self, tiles_136):
+ self.dora_count_central = 0
+ self.dora_count_not_central = 0
+ self.aka_dora_count = 0
- meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON
- best_meld_34.remove(discarded_tile)
+ for tile_136 in tiles_136:
+ tile_34 = tile_136 // 4
- first_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[0], closed_hand)
- closed_hand.remove(first_tile)
+ dora_count = plus_dora(tile_136, self.player.table.dora_indicators)
- second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand)
- closed_hand.remove(second_tile)
+ if is_aka_dora(tile_136, self.player.table.has_aka_dora):
+ self.aka_dora_count += 1
+
+ if not dora_count:
+ continue
+
+ if is_honor(tile_34):
+ self.dora_count_not_central += dora_count
+ self.dora_count_honor += dora_count
+ elif is_terminal(tile_34):
+ self.dora_count_not_central += dora_count
+ else:
+ self.dora_count_central += dora_count
+
+ self.dora_count_central += self.aka_dora_count
+ self.dora_count_total = self.dora_count_central + self.dora_count_not_central
+
+ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile):
+ discarded_tile_34 = discarded_tile // 4
+
+ final_results = []
+ for meld_34 in possible_melds:
+ meld_34_copy = meld_34.copy()
+ closed_hand_copy = closed_hand.copy()
+
+ meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
+ meld_34_copy.remove(discarded_tile_34)
+
+ first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy)
+ closed_hand_copy.remove(first_tile)
+
+ second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy)
+ closed_hand_copy.remove(second_tile)
tiles = [
first_tile,
second_tile,
- tile
+ discarded_tile
]
meld = Meld()
meld.type = meld_type
meld.tiles = sorted(tiles)
- # we had to be sure that all our discard results exists in the closed hand
- filtered_results = []
- for result in outs_results:
- if result.find_tile_in_hand(closed_hand):
- filtered_results.append(result)
-
- # we can't discard anything, so let's not open our hand
- if not filtered_results:
- return None, None
+ melds = self.player.melds + [meld]
- selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard(
- filtered_results,
- shanten,
- had_was_open=True
+ selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(
+ new_tiles,
+ closed_hand_copy,
+ melds,
+ print_log=False
)
- return meld, selected_tile
+ final_results.append({
+ 'discard_tile': selected_tile,
+ 'meld_print': TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]),
+ 'meld': meld
+ })
- return None, None
+ final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten,
+ -x['discard_tile'].ukeire,
+ x['discard_tile'].valuation))
- def meld_had_to_be_called(self, tile):
- """
- For special cases meld had to be called even if shanten number will not be increased
- :param tile: in 136 tiles format
- :return: boolean
- """
- return False
-
- def _find_best_meld_to_open(self, possible_melds, completed_hand):
- """
- :param possible_melds:
- :param completed_hand:
- :return:
- """
-
- if len(possible_melds) == 1:
- return possible_melds[0]
-
- # We will replace possible set with one completed pon set
- # and we will calculate remaining shanten in the hand
- # and chose the hand with min shanten count
- completed_hand_34 = TilesConverter.to_34_array(completed_hand)
-
- results = []
- for meld in possible_melds:
- melds = self.player.open_hand_34_tiles + [meld]
- shanten = self.player.ai.shanten.calculate_shanten(completed_hand_34, melds)
- results.append({'shanten': shanten, 'meld': meld})
+ DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results)
- results = sorted(results, key=lambda i: i['shanten'])
- return results[0]['meld']
+ return final_results[0]
diff --git a/project/game/ai/first_version/strategies/tanyao.py b/project/game/ai/first_version/strategies/tanyao.py
index ec88af1b..0c6e1d50 100644
--- a/project/game/ai/first_version/strategies/tanyao.py
+++ b/project/game/ai/first_version/strategies/tanyao.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from mahjong.constants import TERMINAL_INDICES, HONOR_INDICES
from mahjong.tile import TilesConverter
+from mahjong.utils import is_honor
+from mahjong.utils import is_tile_strictly_isolated
from game.ai.first_version.strategies.main import BaseStrategy
@@ -9,21 +11,28 @@ class TanyaoStrategy(BaseStrategy):
min_shanten = 3
not_suitable_tiles = TERMINAL_INDICES + HONOR_INDICES
- def should_activate_strategy(self):
+ def should_activate_strategy(self, tiles_136):
"""
Tanyao hand is a hand without terminal and honor tiles, to achieve this
we will use different approaches
:return: boolean
"""
- result = super(TanyaoStrategy, self).should_activate_strategy()
+ result = super(TanyaoStrategy, self).should_activate_strategy(tiles_136)
if not result:
return False
tiles = TilesConverter.to_34_array(self.player.tiles)
+
+ closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
+ isolated_tiles = [x // 4 for x in self.player.tiles
+ if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4)]
+
count_of_terminal_pon_sets = 0
count_of_terminal_pairs = 0
count_of_valued_pairs = 0
+ count_of_not_suitable_tiles = 0
+ count_of_not_suitable_not_isolated_tiles = 0
for x in range(0, 34):
tile = tiles[x]
if not tile:
@@ -38,6 +47,16 @@ def should_activate_strategy(self):
if x in self.player.valued_honors:
count_of_valued_pairs += 1
+ if x in self.not_suitable_tiles:
+ count_of_not_suitable_tiles += tile
+
+ if x in self.not_suitable_tiles and x not in isolated_tiles:
+ count_of_not_suitable_not_isolated_tiles += tile
+
+ # we have too much terminals and honors
+ if count_of_not_suitable_tiles >= 5:
+ return False
+
# if we already have pon of honor\terminal tiles
# we don't need to open hand for tanyao
if count_of_terminal_pon_sets > 0:
@@ -53,6 +72,17 @@ def should_activate_strategy(self):
if count_of_terminal_pairs > 1:
return False
+ # 3 or more not suitable tiles that
+ # are not isolated is too much
+ if count_of_not_suitable_not_isolated_tiles >= 3:
+ return False
+
+ # if we are 1 shanten, even 2 tiles
+ # that are not suitable and not isolated
+ # is too much
+ if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.shanten == 1:
+ return False
+
# 123 and 789 indices
indices = [
[0, 1, 2], [6, 7, 8],
@@ -67,36 +97,66 @@ def should_activate_strategy(self):
if first >= 1 and second >= 1 and third >= 1:
return False
- return True
+ # if we have 2 or more non-central doras
+ # we don't want to go for tanyao
+ if self.dora_count_not_central >= 2:
+ return False
+
+ # if we have less than two central doras
+ # let's not consider open tanyao
+ if self.dora_count_central < 2:
+ return False
+
+ # if we have only two central doras let's
+ # wait for 5th turn before opening our hand
+ if self.dora_count_central == 2 and self.player.round_step < 5:
+ return False
- def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand,
- hand_was_open=False):
- if tile_for_open_hand:
- tile_for_open_hand //= 4
+ return True
- is_open_hand = self.player.is_open_hand or hand_was_open
+ def determine_what_to_discard(self, discard_options, hand, open_melds):
+ is_open_hand = len(open_melds) > 0
+
+ # our hand is closed, we don't need to discard terminal tiles here
+ if not is_open_hand:
+ return discard_options
+
+ first_option = sorted(discard_options, key=lambda x: x.shanten)[0]
+ shanten = first_option.shanten
+
+ if shanten > 1:
+ return super(TanyaoStrategy, self).determine_what_to_discard(
+ discard_options,
+ hand,
+ open_melds
+ )
+
+ results = []
+ not_suitable_tiles = []
+ for item in discard_options:
+ if not self.is_tile_suitable(item.tile_to_discard * 4):
+ item.had_to_be_discarded = True
+ not_suitable_tiles.append(item)
+ continue
- if shanten == 0 and is_open_hand:
- results = []
# there is no sense to wait 1-4 if we have open hand
- for item in outs_results:
- all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting])
+ # but let's only avoid atodzuke tiles in tempai, the rest will be dealt with in
+ # generic logic
+ if item.shanten == 0:
+ all_waiting_are_fine = all(
+ [(self.is_tile_suitable(x * 4) or item.wait_to_ukeire[x] == 0) for x in item.waiting])
if all_waiting_are_fine:
results.append(item)
- # we don't have a choice
- # we had to have on bad wait
- if not results:
- return outs_results
-
- return results
- else:
- return super(TanyaoStrategy, self).determine_what_to_discard(closed_hand,
- outs_results,
- shanten,
- for_open_hand,
- tile_for_open_hand,
- hand_was_open)
+ if not_suitable_tiles:
+ return not_suitable_tiles
+
+ # we don't have a choice
+ # we had to have on bad wait
+ if not results:
+ return discard_options
+
+ return results
def is_tile_suitable(self, tile):
"""
diff --git a/project/game/ai/first_version/strategies/yakuhai.py b/project/game/ai/first_version/strategies/yakuhai.py
index 6d9d941f..a6a10a92 100644
--- a/project/game/ai/first_version/strategies/yakuhai.py
+++ b/project/game/ai/first_version/strategies/yakuhai.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from mahjong.constants import EAST, SOUTH
from mahjong.meld import Meld
from mahjong.tile import TilesConverter
@@ -6,27 +7,139 @@
class YakuhaiStrategy(BaseStrategy):
+ valued_pairs = None
+ has_valued_pon = None
- def should_activate_strategy(self):
+ def __init__(self, strategy_type, player):
+ super().__init__(strategy_type, player)
+
+ self.valued_pairs = []
+ self.has_valued_pon = False
+ self.last_chance_calls = []
+
+ def should_activate_strategy(self, tiles_136):
"""
We can go for yakuhai strategy if we have at least one yakuhai pair in the hand
:return: boolean
"""
- result = super(YakuhaiStrategy, self).should_activate_strategy()
+ result = super(YakuhaiStrategy, self).should_activate_strategy(tiles_136)
if not result:
return False
- tiles_34 = TilesConverter.to_34_array(self.player.tiles)
- valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] >= 2]
+ tiles_34 = TilesConverter.to_34_array(tiles_136)
+ player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
+ player_closed_hand_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
+ self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2]
+
+ is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2
+ is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2
+
+ self.valued_pairs = list(set(self.valued_pairs))
+ self.has_valued_pon = len([x for x in self.player.valued_honors if player_hand_tiles_34[x] >= 3]) >= 1
+
+ opportunity_to_meld_yakuhai = False
- for pair in valued_pairs:
- # we have valued pair in the hand and there is enough tiles
+ for x in range(0, 34):
+ if x in self.valued_pairs and tiles_34[x] - player_hand_tiles_34[x] == 1:
+ opportunity_to_meld_yakuhai = True
+
+ has_valued_pair = False
+
+ for pair in self.valued_pairs:
+ # we have valued pair in the hand and there are enough tiles
# in the wall
- if self.player.total_tiles(pair, tiles_34) < 4:
+ if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_closed_hand_tiles_34) < 4:
+ has_valued_pair = True
+ break
+
+ # we don't have valuable pair or pon to open our hand
+ if not has_valued_pair and not self.has_valued_pon:
+ return False
+
+ # let's always open double east
+ if is_double_east_wind:
+ return True
+
+ # let's open double south if we have a dora in the hand
+ # or we have other valuable pairs
+ if is_double_south_wind and (self.dora_count_total >= 1 or len(self.valued_pairs) >= 2):
+ return True
+
+ # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand
+ if len(self.valued_pairs) >= 2 and self.dora_count_total >= 1:
+ return True
+
+ # If we have 2+ dora in the hand let's open hand
+ if self.dora_count_total >= 2:
+ for x in range(0, 34):
+ # we have other pair in the hand
+ # so we can open hand for atodzuke
+ if player_hand_tiles_34[x] >= 2 and x not in self.valued_pairs:
+ self.go_for_atodzuke = True
+ return True
+
+ # If we have 1+ dora in the hand and there is 5+ round step let's open hand
+ if self.dora_count_total >= 1 and self.player.round_step > 5:
+ return True
+
+ for pair in self.valued_pairs:
+ # last chance to get that yakuhai, let's go for it
+ if (opportunity_to_meld_yakuhai and
+ self.player.total_tiles(pair, player_closed_hand_tiles_34) == 3 and
+ self.player.ai.shanten >= 1):
+
+ if pair not in self.last_chance_calls:
+ self.last_chance_calls.append(pair)
+
return True
return False
+ def determine_what_to_discard(self, discard_options, hand, open_melds):
+ is_open_hand = len(open_melds) > 0
+
+ tiles_34 = TilesConverter.to_34_array(hand)
+
+ valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]
+
+ # closed pon sets
+ valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3]
+ # open pon sets
+ valued_pons += [x for x in open_melds if x.type == Meld.PON and x.tiles[0] // 4 in self.player.valued_honors]
+
+ acceptable_options = []
+ for item in discard_options:
+ if is_open_hand:
+ if len(valued_pons) == 0:
+ # don't destroy our only yakuhai pair
+ if len(valued_pairs) == 1 and item.tile_to_discard in valued_pairs:
+ continue
+ elif len(valued_pons) == 1:
+ # don't destroy our only yakuhai pon
+ if item.tile_to_discard in valued_pons:
+ continue
+
+ acceptable_options.append(item)
+
+ # we don't have a choice
+ if not acceptable_options:
+ return discard_options
+
+ preferred_options = []
+ for item in acceptable_options:
+ # ignore wait without yakuhai yaku if possible
+ if is_open_hand and len(valued_pons) == 0 and len(valued_pairs) == 1:
+ if item.shanten == 0:
+ if valued_pairs[0] not in item.waiting:
+ continue
+
+ preferred_options.append(item)
+
+ if not preferred_options:
+ return acceptable_options
+
+ return preferred_options
+
def is_tile_suitable(self, tile):
"""
For yakuhai we don't have any limits
@@ -35,66 +148,49 @@ def is_tile_suitable(self, tile):
"""
return True
- def determine_what_to_discard(self, closed_hand, outs_results, shanten, for_open_hand, tile_for_open_hand,
- hand_was_open=False):
- if tile_for_open_hand:
- tile_for_open_hand //= 4
-
- tiles_34 = TilesConverter.to_34_array(self.player.tiles)
- valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]
-
- # when we trying to open hand with tempai state, we need to chose a valued pair waiting
- if shanten == 0 and valued_pairs and for_open_hand and tile_for_open_hand not in valued_pairs:
- valued_pair = valued_pairs[0]
-
- results = []
- for item in outs_results:
- if valued_pair in item.waiting:
- results.append(item)
- return results
-
- if self.player.is_open_hand:
- has_yakuhai_pon = any([self._is_yakuhai_pon(meld) for meld in self.player.melds])
- # we opened our hand for atodzuke
- if not has_yakuhai_pon:
- for item in outs_results:
- for valued_pair in valued_pairs:
- if valued_pair == item.tile_to_discard:
- item.had_to_be_saved = True
-
- return super(YakuhaiStrategy, self).determine_what_to_discard(closed_hand,
- outs_results,
- shanten,
- for_open_hand,
- tile_for_open_hand,
- hand_was_open)
-
def meld_had_to_be_called(self, tile):
- # for closed hand we don't need to open hand with special conditions
- if not self.player.is_open_hand:
- return False
-
tile //= 4
tiles_34 = TilesConverter.to_34_array(self.player.tiles)
valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]
- for meld in self.player.melds:
- # for big shanten number we don't need to check already opened pon set,
- # because it will improve pur hand anyway
- if self.player.ai.previous_shanten >= 1:
- break
+ # for big shanten number we don't need to check already opened pon set,
+ # because it will improve our hand anyway
+ if self.player.ai.shanten < 2:
+ for meld in self.player.melds:
+ # we have already opened yakuhai pon
+ # so we don't need to open hand without shanten improvement
+ if self._is_yakuhai_pon(meld):
+ return False
- # we have already opened yakuhai pon
- # so we don't need to open hand without shanten improvement
- if self._is_yakuhai_pon(meld):
- return False
+ # if we don't have any yakuhai pon and this is our last chance, we must call this tile
+ if tile in self.last_chance_calls:
+ return True
+
+ # in all other cases for closed hand we don't need to open hand with special conditions
+ if not self.player.is_open_hand:
+ return False
- # open hand for valued pon
+ # we have opened the hand already and don't yet have yakuhai pon
+ # so we now must get it
for valued_pair in valued_pairs:
if valued_pair == tile:
return True
return False
+ def try_to_call_meld(self, tile, is_kamicha_discard, tiles_136):
+ if self.has_valued_pon:
+ return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
+
+ tile_34 = tile // 4
+ # we will open hand for atodzuke only in the special cases
+ if not self.player.is_open_hand and tile_34 not in self.valued_pairs:
+ if self.go_for_atodzuke:
+ return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
+
+ return None, None
+
+ return super(YakuhaiStrategy, self).try_to_call_meld(tile, is_kamicha_discard, tiles_136)
+
def _is_yakuhai_pon(self, meld):
return meld.type == Meld.PON and meld.tiles[0] // 4 in self.player.valued_honors
diff --git a/project/game/ai/first_version/tests/strategies/__init__.py b/project/game/ai/first_version/tests/strategies/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py
new file mode 100644
index 00000000..07a4e23f
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_chiitoitsu.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.tests_mixin import TestMixin
+
+from game.ai.first_version.strategies.chiitoitsu import ChiitoitsuStrategy
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.table import Table
+
+
+class ChiitoitsuStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def test_should_activate_strategy(self):
+ table = Table()
+ player = table.player
+ strategy = ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, player)
+
+ # obvious chiitoitsu, let's activate
+ tiles = self._string_to_136_array(sou='2266', man='3399', pin='289', honors='11')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(honors='6'))
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # less than 5 pairs, don't activate
+ tiles = self._string_to_136_array(sou='2266', man='3389', pin='289', honors='11')
+ player.draw_tile(self._string_to_136_tile(honors='6'))
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # 5 pairs, but we are already tempai, let's no consider this hand as chiitoitsu
+ tiles = self._string_to_136_array(sou='234', man='223344', pin='5669')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='5'))
+ player.discard_tile()
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234', man='22334455669')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ def test_dont_call_meld(self):
+ table = Table()
+ player = table.player
+ strategy = ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, player)
+
+ tiles = self._string_to_136_array(sou='112234', man='2334499')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='9')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_keep_chiitoitsu_tempai(self):
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(sou='113355', man='22669', pin='99')
+ player.init_hand(tiles)
+
+ player.draw_tile(self._string_to_136_tile(man='6'))
+
+ discard = player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '6m')
+
+ def test_5_pairs_yakuhai_not_chiitoitsu(self):
+ table = Table()
+ player = table.player
+
+ table.add_dora_indicator(self._string_to_136_tile(sou='9'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='1'))
+
+ tiles = self._string_to_136_array(sou='112233', pin='16678', honors='66')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='6')
+ meld, _ = player.try_to_call_meld(tile, True)
+
+ self.assertNotEqual(player.ai.current_strategy.type, BaseStrategy.CHIITOITSU)
+
+ self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI)
+
+ self.assertNotEqual(meld, None)
+
+ def chiitoitsu_tanyao_tempai(self):
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(sou='223344', pin='788', man='4577')
+ player.init_hand(tiles)
+
+ player.draw_tile(self._string_to_136_tile(man='4'))
+
+ discard = player.discard_tile()
+ discard_correct = self._to_string([discard]) == '7p' or self._to_string([discard]) == '5m'
+ self.assertEqual(discard_correct, True)
diff --git a/project/game/ai/first_version/tests/strategies/tests_chinitsu.py b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py
new file mode 100644
index 00000000..d571b4f5
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_chinitsu.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.tests_mixin import TestMixin
+from mahjong.meld import Meld
+
+from game.ai.first_version.strategies.chinitsu import ChinitsuStrategy
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.table import Table
+
+
+class ChinitsuStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def test_should_activate_strategy(self):
+ table = Table()
+ player = table.player
+ strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player)
+
+ table.add_dora_indicator(self._string_to_136_tile(pin='1'))
+ table.add_dora_indicator(self._string_to_136_tile(man='1'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='8'))
+
+ tiles = self._string_to_136_array(sou='12355', man='34589', honors='1234')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='12355', man='458', honors='112345')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # we shouldn't go for chinitsu if we have a valued pair or pon
+ tiles = self._string_to_136_array(sou='111222578', man='8', honors='5556')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='1112227788', man='7', honors='556')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # if we have a pon of non-valued honors, this is not chinitsu
+ tiles = self._string_to_136_array(sou='1112224688', honors='2224')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # if we have just a pair of non-valued tiles, we can go for chinitsu
+ # if we have 11 chinitsu tiles and it's early
+ tiles = self._string_to_136_array(sou='11122234688', honors='224')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # if we have a complete set with dora, we shouldn't go for chinitsu
+ tiles = self._string_to_136_array(sou='1112223688', pin='1239')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # even if the set may be interpreted as two forms
+ tiles = self._string_to_136_array(sou='111223688', pin='23349')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # even if the set may be interpreted as two forms v2
+ tiles = self._string_to_136_array(sou='111223688', pin='23459')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # if we have a long form with dora, we shouldn't go for chinitsu
+ tiles = self._string_to_136_array(sou='111223688', pin='23339')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # buf it it's just a ryanmen - no problem
+ tiles = self._string_to_136_array(sou='1112223688', pin='238', man='9')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # we have three non-isolated doras in other suits, this is not chinitsu
+ tiles = self._string_to_136_array(sou='111223688', man='22', pin='239')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # we have two non-isolated doras in other suits and no doras in our suit
+ # this is not chinitsu
+ tiles = self._string_to_136_array(sou='111223688', man='24', pin='249')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # we have two non-isolated doras in other suits and 1 shanten, not chinitsu
+ tiles = self._string_to_136_array(sou='111222789', man='23', pin='239')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # we don't want to open on 9th tile into chinitsu, but it's ok to
+ # switch to chinitsu if we get in from the wall
+ tiles = self._string_to_136_array(sou='11223578', man='57', pin='4669')
+ player.init_hand(tiles)
+ # plus one tile to open hand
+ tiles = self._string_to_136_array(sou='112223578', man='57', pin='466')
+ self.assertEqual(strategy.should_activate_strategy(tiles), False)
+ # but now let's init hand with these tiles, we can now slowly move to chinitsu
+ tiles = self._string_to_136_array(sou='112223578', man='57', pin='466')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(tiles), True)
+
+ def test_suitable_tiles(self):
+ table = Table()
+ player = table.player
+ strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player)
+
+ tiles = self._string_to_136_array(sou='111222479', man='78', honors='12')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(pin='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(sou='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ tile = self._string_to_136_tile(honors='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ def test_open_suit_same_shanten(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='1134556999', pin='3', sou='78')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='345')
+ player.add_called_meld(meld)
+
+ strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='1')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '111m')
+
+ def test_correct_discard_agari_no_yaku(self):
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(man='111234677889', sou='1', pin='')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='789')
+ player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(sou='1')
+ player.draw_tile(tile)
+ discard = player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '1s')
+
+ def test_open_suit_agari_no_yaku(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='11123455589', pin='22')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='234')
+ player.add_called_meld(meld)
+
+ strategy = ChinitsuStrategy(BaseStrategy.CHINITSU, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='7')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '789m')
diff --git a/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py
new file mode 100644
index 00000000..197c5244
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_formal_tempai.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.tests_mixin import TestMixin
+from mahjong.tile import Tile
+from mahjong.meld import Meld
+
+from game.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.table import Table
+
+
+class FormalTempaiStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def setUp(self):
+ self.table = Table()
+ self.player = self.table.player
+ self.player.dealer_seat = 3
+
+ def test_should_activate_strategy(self):
+ strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)
+
+ tiles = self._string_to_136_array(sou='12355689', man='89', pin='339')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ # Let's move to 10th round step
+ for _ in range(0, 10):
+ self.player.add_discarded_tile(Tile(0, False))
+
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ # Now we move to 11th turn, we have 2 shanten and no doras,
+ # we should go for formal tempai
+ self.player.add_discarded_tile(Tile(0, True))
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_get_tempai(self):
+ tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299')
+ self.player.init_hand(tiles)
+
+ # Let's move to 15th round step
+ for _ in range(0, 15):
+ self.player.add_discarded_tile(Tile(0, False))
+
+ tile = self._string_to_136_tile(man='8')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '789m')
+
+ tile_to_discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([tile_to_discard]), '8s')
+
+ # we shouldn't open when we are already in tempai expect for some
+ # special cases
+ def test_dont_meld_agari(self):
+ strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)
+
+ tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299')
+ self.player.init_hand(tiles)
+
+ # Let's move to 15th round step
+ for _ in range(0, 15):
+ self.player.add_discarded_tile(Tile(0, False))
+
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ tiles = self._string_to_136_array(man='23789', sou='456', pin='22299')
+ self.player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='789')
+ self.player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
diff --git a/project/game/ai/first_version/tests/strategies/tests_honitsu.py b/project/game/ai/first_version/tests/strategies/tests_honitsu.py
new file mode 100644
index 00000000..16873f6d
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_honitsu.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.tests_mixin import TestMixin
+from mahjong.meld import Meld
+
+from game.ai.first_version.strategies.honitsu import HonitsuStrategy
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.table import Table
+
+
+class HonitsuStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def test_should_activate_strategy(self):
+ table = Table()
+ player = table.player
+ strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
+
+ table.add_dora_indicator(self._string_to_136_tile(pin='1'))
+ table.add_dora_indicator(self._string_to_136_tile(honors='5'))
+
+ tiles = self._string_to_136_array(sou='12355', man='12389', honors='123')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # many tiles in one suit and yakuhai pair, but still many useless winds
+ tiles = self._string_to_136_array(sou='12355', man='23', pin='68', honors='2355')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # many tiles in one suit and yakuhai pair and another honor pair, so
+ # now this is honitsu
+ tiles = self._string_to_136_array(sou='12355', man='238', honors='22355')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # same conditions, but ready suit with dora in another suit, so no honitsu
+ tiles = self._string_to_136_array(sou='12355', pin='234', honors='22355')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # same conditions, but we have a pon of yakuhai doras, we shouldn't
+ # force honitsu with this hand
+ tiles = self._string_to_136_array(sou='12355', pin='238', honors='22666')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # if we have a complete set with dora, we shouldn't go for honitsu
+ tiles = self._string_to_136_array(sou='11123688', pin='123', honors='55')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # even if the set may be interpreted as two forms
+ tiles = self._string_to_136_array(sou='1223688', pin='2334', honors='55')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # even if the set may be interpreted as two forms v2
+ tiles = self._string_to_136_array(sou='1223688', pin='2345', honors='55')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ # if we have a long form with dora, we shouldn't go for honitsu
+ tiles = self._string_to_136_array(sou='1223688', pin='2333', honors='55')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), False)
+
+ def test_suitable_tiles(self):
+ table = Table()
+ player = table.player
+ strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
+
+ tiles = self._string_to_136_array(sou='12355', man='238', honors='23455')
+ player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(pin='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(sou='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ tile = self._string_to_136_tile(honors='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ def test_open_hand_and_discard_tiles_logic(self):
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(sou='112235589', man='23', honors='22')
+ player.init_hand(tiles)
+
+ # we don't need to call meld even if it improves our hand,
+ # because we are aim for honitsu
+ tile = self._string_to_136_tile(man='1')
+ meld, _ = player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ # any honor tile is suitable
+ tile = self._string_to_136_tile(honors='2')
+ meld, discard_option = player.try_to_call_meld(tile, False)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string([discard_option.tile_to_discard * 4]), '2m')
+
+ player.discard_tile(discard_option)
+
+ tile = self._string_to_136_tile(man='1')
+ player.draw_tile(tile)
+ tile_to_discard = player.discard_tile()
+
+ # we are in honitsu mode, so we should discard man suits
+ self.assertEqual(self._to_string([tile_to_discard]), '1m')
+
+ def test_riichi_and_tiles_from_another_suit_in_the_hand(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='33345678', pin='22', honors='155')
+ player.init_hand(tiles)
+
+ player.draw_tile(self._string_to_136_tile(man='9'))
+ tile_to_discard = player.discard_tile()
+
+ # we don't need to go for honitsu here
+ # we already in tempai
+ self.assertEqual(self._to_string([tile_to_discard]), '1z')
+
+ def test_discard_not_needed_winds(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='24', pin='4', sou='12344668', honors='36')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='5'))
+
+ table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+ table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+ table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+
+ tile_to_discard = player.discard_tile()
+
+ # west was discarded three times, we don't need it
+ self.assertEqual(self._to_string([tile_to_discard]), '3z')
+
+ def test_discard_not_effective_tiles_first(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='33', pin='12788999', sou='5', honors='23')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(honors='6'))
+ tile_to_discard = player.discard_tile()
+
+ self.assertEqual(self._to_string([tile_to_discard]), '5s')
+
+ def test_open_yakuhai_same_shanten(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='34556778', pin='3', sou='78', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='345')
+ player.add_called_meld(meld)
+
+ strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(honors='7')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '777z')
+
+ def test_open_hand_and_not_go_for_chiitoitsu(self):
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(sou='1122559', honors='134557')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+
+ tile = player.discard_tile()
+ self.assertEqual(self._to_string([tile]), '4p')
+
+ tile = self._string_to_136_tile(honors='5')
+ meld, _ = player.try_to_call_meld(tile, False)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '555z')
+
+ def test_open_suit_same_shanten(self):
+ table = Table()
+ player = table.player
+ player.scores = 25000
+ table.count_of_remaining_tiles = 100
+
+ tiles = self._string_to_136_array(man='1134556', pin='3', sou='78', honors='777')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='345')
+ player.add_called_meld(meld)
+
+ strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='1')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '111m')
diff --git a/project/game/ai/first_version/tests/strategies/tests_tanyao.py b/project/game/ai/first_version/tests/strategies/tests_tanyao.py
new file mode 100644
index 00000000..6884c81f
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_tanyao.py
@@ -0,0 +1,380 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.constants import FIVE_RED_PIN
+from mahjong.meld import Meld
+from mahjong.tests_mixin import TestMixin
+
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.ai.first_version.strategies.tanyao import TanyaoStrategy
+from game.table import Table
+
+
+class TanyaoStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def setUp(self):
+ self.table = self._make_table()
+ self.player = self.table.player
+
+ def test_should_activate_strategy_and_terminal_pon_sets(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tiles = self._string_to_136_array(sou='222', man='3459', pin='233', honors='111')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='222', man='3459', pin='233999')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='222', man='3459', pin='233444')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_should_activate_strategy_and_terminal_pairs(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tiles = self._string_to_136_array(sou='222', man='3459', pin='2399', honors='11')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='22258', man='3566', pin='2399')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_should_activate_strategy_and_valued_pair(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tiles = self._string_to_136_array(man='23446679', sou='222', honors='55')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(man='23446679', sou='222', honors='22')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_should_activate_strategy_and_chitoitsu_like_hand(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tiles = self._string_to_136_array(sou='223388', man='2244', pin='6687')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_should_activate_strategy_and_already_completed_sided_set(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tiles = self._string_to_136_array(sou='123234', man='2349', pin='234')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234789', man='2349', pin='234')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234', man='1233459', pin='234')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234', man='2227899', pin='234')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234', man='2229', pin='122334')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='234', man='2229', pin='234789')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ tiles = self._string_to_136_array(sou='223344', man='2229', pin='234')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ def test_suitable_tiles(self):
+ strategy = TanyaoStrategy(BaseStrategy.TANYAO, self.player)
+
+ tile = self._string_to_136_tile(man='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(pin='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(sou='9')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(honors='1')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(honors='6')
+ self.assertEqual(strategy.is_tile_suitable(tile), False)
+
+ tile = self._string_to_136_tile(man='2')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ tile = self._string_to_136_tile(pin='5')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ tile = self._string_to_136_tile(sou='8')
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ def test_dont_open_hand_with_high_shanten(self):
+ # with 4 shanten we don't need to aim for open tanyao
+ tiles = self._string_to_136_array(man='269', pin='247', sou='2488', honors='123')
+ tile = self._string_to_136_tile(sou='3')
+ self.player.init_hand(tiles)
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ # with 3 shanten we can open a hand
+ tiles = self._string_to_136_array(man='236', pin='247', sou='2488', honors='123')
+ tile = self._string_to_136_tile(sou='3')
+ self.player.init_hand(tiles)
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_dont_open_hand_with_not_suitable_melds(self):
+ tiles = self._string_to_136_array(man='22255788', sou='3479', honors='3')
+ tile = self._string_to_136_tile(sou='8')
+ self.player.init_hand(tiles)
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_and_discard_tiles_logic(self):
+ tiles = self._string_to_136_array(man='22234', sou='238', pin='256', honors='44')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='4')
+ meld, discard_option = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string([discard_option.tile_to_discard * 4]), '4z')
+
+ self.player.discard_tile(discard_option)
+
+ tile = self._string_to_136_tile(pin='5')
+ self.player.draw_tile(tile)
+ tile_to_discard = self.player.discard_tile()
+
+ self.assertEqual(self._to_string([tile_to_discard]), '4z')
+
+ def test_dont_count_pairs_in_already_opened_hand(self):
+ tiles = self._string_to_136_array(man='33556788', sou='22266')
+ self.player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, sou='222')
+ self.player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(sou='6')
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ # even if it looks like chitoitsu we can open hand and get tempai here
+ self.assertNotEqual(meld, None)
+
+ def test_we_cant_win_with_this_hand(self):
+ tiles = self._string_to_136_array(man='22277', sou='23', pin='233445')
+ self.player.init_hand(tiles)
+ meld = self._make_meld(Meld.CHI, pin='234')
+ self.player.add_called_meld(meld)
+
+ self.player.draw_tile(self._string_to_136_tile(sou='1'))
+ discard = self.player.discard_tile()
+ # but for already open hand we cant do tsumo
+ # because we don't have a yaku here
+ # so, let's do tsumogiri
+ self.assertEqual(self.player.ai.shanten, 0)
+ self.assertEqual(self._to_string([discard]), '1s')
+
+ def test_choose_correct_waiting(self):
+ tiles = self._string_to_136_array(man='222678', sou='234', pin='3588')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(pin='2'))
+
+ self._assert_tanyao(self.player)
+
+ # discard 5p and riichi
+ discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '5p')
+
+ meld = self._make_meld(Meld.CHI, man='234')
+ self.player.add_called_meld(meld)
+
+ tiles = self._string_to_136_array(man='234888', sou='234', pin='3588')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(pin='2'))
+
+ # it is not a good idea to wait on 1-4, since we can't win on 1 with open hand
+ # so let's continue to wait on 4 only
+ discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '2p')
+
+ table = self._make_table()
+ player = table.player
+
+ meld = self._make_meld(Meld.CHI, man='678')
+ player.add_called_meld(meld)
+
+ tiles = self._string_to_136_array(man='222678', sou='234', pin='2388')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='7'))
+
+ # we can wait only on 1-4, so let's do it even if we can't get yaku on 1
+ discard = player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '7s')
+
+ def test_choose_balanced_ukeire_in_1_shanten(self):
+ table = self._make_table()
+ player = table.player
+
+ meld = self._make_meld(Meld.CHI, man='678')
+ player.add_called_meld(meld)
+
+ tiles = self._string_to_136_array(man='22678', sou='234568', pin='45')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(man='2'))
+
+ self._assert_tanyao(player)
+
+ # there are lost of options to avoid atodzuke and even if it is atodzuke,
+ # it is still a good one, so let's choose more efficient 8s discard instead of 2s
+ discard = player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '8s')
+
+ def test_choose_pseudo_atodzuke(self):
+ table = self._make_table()
+ table.has_aka_dora = False
+ player = table.player
+
+ # one tile is dora indicator and 3 are out
+ # so this 1-4 wait is not atodzuke
+ for _ in range(0, 3):
+ table.add_discarded_tile(1, self._string_to_136_tile(pin='1'), False)
+
+ meld = self._make_meld(Meld.CHI, man='678')
+ player.add_called_meld(meld)
+
+ tiles = self._string_to_136_array(man='222678', sou='23488', pin='35')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='2'))
+
+ self._assert_tanyao(player)
+
+ discard = player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '5p')
+
+ def test_choose_correct_waiting_and_first_opened_meld(self):
+ tiles = self._string_to_136_array(man='2337788', sou='222', pin='234')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(man='8')
+ meld, tile_to_discard = self.player.try_to_call_meld(tile, False)
+
+ self._assert_tanyao(self.player)
+
+ discard = self.player.discard_tile(tile_to_discard)
+ self.assertEqual(self._to_string([discard]), '2m')
+
+ def test_we_dont_need_to_discard_terminals_from_closed_hand(self):
+ tiles = self._string_to_136_array(man='22234', sou='13588', pin='558')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(pin='5')
+ self.player.draw_tile(tile)
+ tile_to_discard = self.player.discard_tile()
+
+ # our hand is closed, let's keep terminal for now
+ self.assertEqual(self._to_string([tile_to_discard]), '8p')
+
+ def test_dont_open_tanyao_with_two_non_central_doras(self):
+ self.table.add_dora_indicator(self._string_to_136_tile(pin='8'))
+
+ tiles = self._string_to_136_array(man='22234', sou='6888', pin='5599')
+ self.table.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(pin='5')
+ meld, _ = self.table.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ def test_dont_open_tanyao_with_three_not_isolated_terminals(self):
+ tiles = self._string_to_136_array(man='22256', sou='2799', pin='5579')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(pin='5')
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ def test_dont_open_tanyao_with_two_not_isolated_terminals_one_shanten(self):
+ tiles = self._string_to_136_array(man='22234', sou='379', pin='55579')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(man='5')
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ def test_dont_count_terminal_tiles_in_ukeire(self):
+ # for closed hand let's chose tile with best ukeire
+ tiles = self._string_to_136_array(man='234578', sou='235', pin='2246')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(pin='5'))
+ discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '5m')
+
+ # but with opened hand we don't need to count not suitable tiles as ukeire
+ tiles = self._string_to_136_array(man='234578', sou='235', pin='2246')
+ self.player.init_hand(tiles)
+ self.player.add_called_meld(self._make_meld(Meld.CHI, man='234'))
+ self.player.draw_tile(self._string_to_136_tile(pin='5'))
+ discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '8m')
+
+ def test_determine_strategy_when_we_try_to_call_meld(self):
+ self.table.has_aka_dora = True
+
+ self.table.add_dora_indicator(self._string_to_136_tile(sou='5'))
+ tiles = self._string_to_136_array(man='66678', sou='6888', pin='5588')
+ self.table.player.init_hand(tiles)
+
+ # with this red five we will have 2 dora in the hand
+ # and in that case we can open our hand
+ meld, _ = self.table.player.try_to_call_meld(FIVE_RED_PIN, False)
+ self.assertNotEqual(meld, None)
+
+ self._assert_tanyao(self.player)
+
+ def test_correct_discard_agari_no_yaku(self):
+ tiles = self._string_to_136_array(man='23567', sou='456', pin='22244')
+ self.player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='567')
+ self.player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(man='1')
+ self.player.draw_tile(tile)
+ discard = self.player.discard_tile()
+ self.assertEqual(self._to_string([discard]), '1m')
+
+ # In case we are in temporary furiten, we can't call ron, but can still
+ # make chi. We assume this chi to be bad, so let's not call it.
+ def test_dont_meld_agari(self):
+ tiles = self._string_to_136_array(man='23567', sou='456', pin='22244')
+ self.player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, man='567')
+ self.player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def _make_table(self):
+ table = Table()
+ table.has_open_tanyao = True
+
+ # add doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(sou='1'))
+ table.add_dora_indicator(self._string_to_136_tile(man='1'))
+ table.add_dora_indicator(self._string_to_136_tile(pin='1'))
+
+ return table
+
+ def _assert_tanyao(self, player):
+ self.assertNotEqual(player.ai.current_strategy, None)
+ self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
diff --git a/project/game/ai/first_version/tests/strategies/tests_yakuhai.py b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py
new file mode 100644
index 00000000..543dc2a9
--- /dev/null
+++ b/project/game/ai/first_version/tests/strategies/tests_yakuhai.py
@@ -0,0 +1,545 @@
+# -*- coding: utf-8 -*-
+import unittest
+
+from mahjong.constants import WEST, EAST, SOUTH
+from mahjong.meld import Meld
+from mahjong.tests_mixin import TestMixin
+from mahjong.tile import Tile
+
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy
+from game.table import Table
+
+
+class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin):
+
+ def setUp(self):
+ self.table = Table()
+ self.player = self.table.player
+ self.player.dealer_seat = 3
+
+ def test_should_activate_strategy(self):
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)
+
+ tiles = self._string_to_136_array(sou='12355689', man='89', honors='123')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ self.table.dora_indicators.append(self._string_to_136_tile(honors='7'))
+ tiles = self._string_to_136_array(sou='12355689', man='899', honors='55')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ # with chitoitsu-like hand we don't need to go for yakuhai
+ tiles = self._string_to_136_array(sou='1235566', man='8899', honors='66')
+ self.player.init_hand(tiles)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ # don't count tile discarded by other player as our pair
+ tiles = self._string_to_136_array(sou='12355689', man='899', honors='25')
+ self.player.init_hand(tiles)
+ tiles = self._string_to_136_array(sou='12355689', man='899', honors='255')
+ self.assertEqual(strategy.should_activate_strategy(tiles), False)
+
+ def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self):
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)
+
+ self.table.dora_indicators.append(self._string_to_136_tile(honors='7'))
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='55')
+ self.player.init_hand(tiles)
+
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False)
+ self.table.add_discarded_tile(3, self._string_to_136_tile(honors='5'), False)
+
+ # we can't complete yakuhai, because there is not enough honor tiles
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ def test_suitable_tiles(self):
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)
+
+ # for yakuhai we can use any tile
+ for tile in range(0, 136):
+ self.assertEqual(strategy.is_tile_suitable(tile), True)
+
+ def test_force_yakuhai_pair_waiting_for_tempai_hand(self):
+ """
+ If hand shanten = 1 don't open hand except the situation where is
+ we have tempai on yakuhai tile after open set
+ """
+ self.table.dora_indicators.append(self._string_to_136_tile(man='3'))
+ tiles = self._string_to_136_array(sou='123', pin='678', man='34468', honors='66')
+ self.player.init_hand(tiles)
+
+ # we will not get tempai on yakuhai pair with this hand, so let's skip this call
+ tile = self._string_to_136_tile(man='5')
+ meld, _ = self.player.try_to_call_meld(tile, False)
+ self.assertEqual(meld, None)
+
+ # but here we will have atodzuke tempai
+ tile = self._string_to_136_tile(man='7')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '678m')
+
+ self.table = Table()
+ self.player = self.table.player
+
+ # we can open hand in that case
+ self.table.dora_indicators.append(self._string_to_136_tile(sou='5'))
+ tiles = self._string_to_136_array(man='44556', sou='366789', honors='77')
+ self.player.init_hand(tiles)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), True)
+
+ tile = self._string_to_136_tile(honors='7')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '777z')
+
+ def test_tempai_without_yaku(self):
+ tiles = self._string_to_136_array(sou='678', pin='12355', man='456', honors='77')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(pin='5')
+ self.player.draw_tile(tile)
+ meld = self._make_meld(Meld.CHI, sou='678')
+ self.player.add_called_meld(meld)
+
+ discard = self.player.discard_tile()
+ self.assertNotEqual(self._to_string([discard]), '7z')
+ self.assertNotEqual(self._to_string([discard]), '5p')
+
+ def test_wrong_shanten_improvements_detection(self):
+ """
+ With hand 2345s1p11z bot wanted to open set on 2s,
+ so after opened set we will get 25s1p11z
+ it is not correct logic, because we ruined our hand
+ :return:
+ """
+ tiles = self._string_to_136_array(sou='2345999', honors='114446')
+ self.player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, sou='999')
+ self.player.add_called_meld(meld)
+ meld = self._make_meld(Meld.PON, honors='444')
+ self.player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(sou='2')
+ meld, _ = self.table.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_with_doras_in_the_hand(self):
+ """
+ If we have valuable pair in the hand, and 2+ dora let's open on this
+ valuable pair
+ """
+
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='11')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # add doras to the hand
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='7'))
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='8'))
+ self.player.init_hand(tiles)
+
+ # and now we can open hand on the valuable pair
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ # but we don't need to open hand for atodzuke here
+ tile = self._string_to_136_tile(pin='3')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_with_doras_in_the_hand_and_atodzuke(self):
+ """
+ If we have valuable pair in the hand, and 2+ dora we can open hand on any tile
+ but only if we have other pair in the hand
+ """
+
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='11')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(pin='3')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # add doras to the hand
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='7'))
+ self.player.init_hand(tiles)
+
+ # we have other pair in the hand, so we can open atodzuke here
+ tile = self._string_to_136_tile(pin='3')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_open_hand_on_fifth_round_step(self):
+ """
+ If we have valuable pair in the hand, 1+ dora and 5+ round step
+ let's open on this valuable pair
+ """
+
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12789', honors='11')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # add doras to the hand
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='7'))
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # one discard == one round step
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.add_discarded_tile(Tile(0, False))
+ self.player.init_hand(tiles)
+
+ # after 5 round step we can open hand
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ # but we don't need to open hand for atodzuke here
+ tile = self._string_to_136_tile(pin='3')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_with_two_valuable_pairs(self):
+ """
+ If we have two valuable pairs in the hand and 1+ dora
+ let's open on one of this valuable pairs
+ """
+
+ tiles = self._string_to_136_array(man='159', sou='128', pin='789', honors='5566')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='5')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # add doras to the hand
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='7'))
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='5')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ tile = self._string_to_136_tile(honors='6')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ # but we don't need to open hand for atodzuke here
+ tile = self._string_to_136_tile(pin='3')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_and_once_discarded_tile(self):
+ """
+ If we have valuable pair in the hand, this tile was discarded once and we have 1+ shanten
+ let's open on this valuable pair
+ """
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)
+
+ tiles = self._string_to_136_array(sou='678', pin='14689', man='456', honors='77')
+ self.player.init_hand(tiles)
+
+ # we don't activate strategy yet
+ self.assertEqual(strategy.should_activate_strategy(self.player.tiles), False)
+
+ # let's skip first yakuhai early in the game
+ tile = self._string_to_136_tile(honors='7')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # now one is out
+ self.table.add_discarded_tile(1, tile, False)
+
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(self._to_string(meld.tiles), '777z')
+
+ # but we don't need to open hand for atodzuke here
+ tile = self._string_to_136_tile(pin='7')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ def test_open_hand_when_yakuhai_already_in_the_hand(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(honors='5'))
+
+ tiles = self._string_to_136_array(man='46', pin='4679', sou='1348', honors='666')
+ player.init_hand(tiles)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(sou='2')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_always_open_double_east_wind(self):
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='11')
+ self.player.init_hand(tiles)
+
+ # player is is not east
+ self.player.dealer_seat = 2
+ self.assertEqual(self.player.player_wind, WEST)
+
+ self.player.init_hand(tiles)
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # player is is east
+ self.player.dealer_seat = 0
+ self.assertEqual(self.player.player_wind, EAST)
+
+ self.player.init_hand(tiles)
+ tile = self._string_to_136_tile(honors='1')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_open_double_south_wind(self):
+ tiles = self._string_to_136_array(man='59', sou='1235', pin='12788', honors='22')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='2')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # player is south and round is south
+ self.table.round_wind_number = 5
+ self.player.dealer_seat = 3
+ self.assertEqual(self.player.player_wind, SOUTH)
+
+ self.player.init_hand(tiles)
+ tile = self._string_to_136_tile(honors='2')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # add dora in the hand and after that we can open a hand
+ self.table.dora_indicators.append(self._string_to_136_tile(pin='6'))
+
+ self.player.init_hand(tiles)
+ tile = self._string_to_136_tile(honors='2')
+ meld, _ = self.player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_keep_yakuhai_in_closed_hand(self):
+ tiles = self._string_to_136_array(man='14', sou='15', pin='113347', honors='777')
+ self.player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(honors='3')
+ self.player.draw_tile(tile)
+
+ discard = self.player.discard_tile()
+ self.assertNotEqual(self._to_string([discard]), '7z')
+
+ def test_keep_only_yakuhai_pon(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+ table.add_dora_indicator(self._string_to_136_tile(man='3'))
+
+ tiles = self._string_to_136_array(man='11144', sou='567', pin='56', honors='777')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ player.draw_tile(self._string_to_136_tile(man='4'))
+ discarded_tile = player.discard_tile()
+ self.assertNotEqual(self._to_string([discarded_tile]), '7z')
+
+ def test_keep_only_yakuhai_pair(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+ table.add_dora_indicator(self._string_to_136_tile(man='3'))
+
+ table.add_discarded_tile(1, self._string_to_136_tile(honors='7'), False)
+
+ tiles = self._string_to_136_array(man='11144', sou='567', pin='156', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ player.draw_tile(self._string_to_136_tile(pin='1'))
+ discarded_tile = player.discard_tile()
+ self.assertNotEqual(self._to_string([discarded_tile]), '7z')
+
+ def test_atodzuke_keep_yakuhai_wait(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+
+ tiles = self._string_to_136_array(man='11144', sou='567', pin='567', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ # two of 4 man tiles are already out, so it would seem our wait is worse, but we know
+ # we must keep two pairs in order to be atodzuke tempai
+ table.add_discarded_tile(1, self._string_to_136_tile(man='4'), False)
+ table.add_discarded_tile(1, self._string_to_136_tile(man='4'), False)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ player.draw_tile(self._string_to_136_tile(man='2'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2m')
+
+ # issue #98
+ @unittest.expectedFailure
+ def test_atodzuke_dont_destroy_second_pair(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+
+ tiles = self._string_to_136_array(man='111445', sou='468', pin='56', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # 6 man is bad meld, we lose our second pair and so is 4 man
+ tile = self._string_to_136_tile(man='6')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ # but if we have backup pair it's ok
+ tiles = self._string_to_136_array(man='111445', sou='468', pin='88', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # 6 man is bad meld, we lose our second pair and so is 4 man
+ tile = self._string_to_136_tile(man='6')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ def test_atodzuke_dont_open_no_yaku_tempai(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+
+ tiles = self._string_to_136_array(man='111445', sou='567', pin='56', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ # 6 man is bad meld, we lose our second pair and so is 4 man
+ tile = self._string_to_136_tile(man='6')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertEqual(meld, None)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ # 7 pin is a good meld, we get to tempai keeping yakuhai wait
+ tile = self._string_to_136_tile(pin='7')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ def test_atodzuke_choose_hidden_syanpon(self):
+ # make sure yakuhai strategy is activated by adding 3 doras to the hand
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='9'))
+
+ tiles = self._string_to_136_array(man='111678', sou='56678', honors='77')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, man='111')
+ player.add_called_meld(meld)
+
+ strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+ self.assertEqual(strategy.should_activate_strategy(player.tiles), True)
+
+ for _ in range(0, 4):
+ table.add_discarded_tile(1, self._string_to_136_tile(sou='9'), False)
+
+ player.draw_tile(self._string_to_136_tile(man='6'))
+ discarded_tile = player.discard_tile()
+ self.assertNotEqual(self._to_string([discarded_tile]), '6m')
+
+ def test_tempai_with_open_yakuhai_meld_and_yakuhai_pair_in_the_hand(self):
+ """
+ there was a bug where bot didn't handle tempai properly
+ with opened yakuhai pon and pair in the hand
+ 56m555p6678s55z + [777z]
+ """
+ table = Table()
+ player = table.player
+
+ tiles = self._string_to_136_array(man='56', pin='555', sou='667', honors='55777')
+ player.init_hand(tiles)
+ player.add_called_meld(self._make_meld(Meld.PON, honors='777'))
+ player.draw_tile(self._string_to_136_tile(sou='8'))
+
+ player.ai.current_strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
+
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '6s')
diff --git a/project/game/ai/first_version/tests/tests_ai.py b/project/game/ai/first_version/tests/tests_ai.py
index 8bb30aa7..94ab478a 100644
--- a/project/game/ai/first_version/tests/tests_ai.py
+++ b/project/game/ai/first_version/tests/tests_ai.py
@@ -18,16 +18,16 @@ def test_set_is_tempai_flag_to_the_player(self):
tile = self._string_to_136_array(man='9')[0]
player.init_hand(tiles)
player.draw_tile(tile)
-
player.discard_tile()
+
self.assertEqual(player.in_tempai, False)
tiles = self._string_to_136_array(sou='11145677', pin='345', man='56')
tile = self._string_to_136_array(man='9')[0]
player.init_hand(tiles)
player.draw_tile(tile)
-
player.discard_tile()
+
self.assertEqual(player.in_tempai, True)
def test_not_open_hand_in_riichi(self):
@@ -64,6 +64,10 @@ def test_chose_right_set_to_open_hand(self):
table.has_open_tanyao = True
player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(pin='2'))
+ table.add_dora_indicator(self._string_to_136_tile(pin='3'))
+
tiles = self._string_to_136_array(man='23455', pin='3445678', honors='1')
tile = self._string_to_136_tile(man='5')
player.init_hand(tiles)
@@ -75,6 +79,8 @@ def test_chose_right_set_to_open_hand(self):
table = Table()
player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='5'))
tiles = self._string_to_136_array(man='335666', pin='22', sou='345', honors='55')
player.init_hand(tiles)
@@ -87,6 +93,9 @@ def test_chose_right_set_to_open_hand(self):
table = Table()
table.has_open_tanyao = True
player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='1'))
+ table.add_dora_indicator(self._string_to_136_tile(man='4'))
tiles = self._string_to_136_array(man='23557', pin='556788', honors='22')
player.init_hand(tiles)
@@ -96,6 +105,137 @@ def test_chose_right_set_to_open_hand(self):
self.assertEqual(meld.type, Meld.PON)
self.assertEqual(self._to_string(meld.tiles), '555p')
+ table = Table()
+ table.has_open_tanyao = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='4'))
+ table.add_dora_indicator(self._string_to_136_tile(pin='5'))
+ tiles = self._string_to_136_array(man='3556', pin='234668', sou='248')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345m')
+
+ table = Table()
+ table.has_open_tanyao = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='4'))
+ table.add_dora_indicator(self._string_to_136_tile(pin='5'))
+ tiles = self._string_to_136_array(man='3445', pin='234668', sou='248')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(man='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345m')
+
+ table = Table()
+ table.has_open_tanyao = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ tiles = self._string_to_136_array(man='567888', pin='788', sou='3456')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '456s')
+
+ tile = self._string_to_136_tile(sou='5')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345s')
+
+ table = Table()
+ table.has_open_tanyao = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ tiles = self._string_to_136_array(man='567888', pin='788', sou='2345')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='4')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '234s')
+
+ def test_chose_right_set_to_open_hand_dora(self):
+ table = Table()
+ table.has_open_tanyao = True
+ table.has_aka_dora = False
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='1'))
+ tiles = self._string_to_136_array(man='3456788', sou='245888')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='3')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '234s')
+
+ table = Table()
+ table.has_open_tanyao = True
+ table.has_aka_dora = False
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+ tiles = self._string_to_136_array(man='3456788', sou='245888')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='3')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345s')
+
+ table = Table()
+ table.has_open_tanyao = True
+ table.has_aka_dora = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ # 5 from string is always aka
+ tiles = self._string_to_136_array(man='3456788', sou='245888')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='3')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345s')
+
+ table = Table()
+ table.has_open_tanyao = True
+ table.has_aka_dora = True
+ player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='7'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='1'))
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+ # double dora versus regular dora, we should keep double dora
+ tiles = self._string_to_136_array(man='3456788', sou='245888')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='3')
+ meld, _ = player.try_to_call_meld(tile, True)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.CHI)
+ self.assertEqual(self._to_string(meld.tiles), '345s')
+
def test_not_open_hand_for_not_needed_set(self):
"""
We don't need to open hand if it is not improve the hand.
@@ -104,6 +244,7 @@ def test_not_open_hand_for_not_needed_set(self):
table = Table()
player = table.player
+ table.dora_indicators.append(self._string_to_136_tile(honors='7'))
tiles = self._string_to_136_array(man='22457', sou='12234', pin='9', honors='55')
player.init_hand(tiles)
@@ -123,25 +264,44 @@ def test_chose_strategy_and_reset_strategy(self):
table.has_open_tanyao = True
player = table.player
+ # add 3 doras so we are sure to go for tanyao
+ table.add_dora_indicator(self._string_to_136_tile(man='2'))
+
+ # we draw a tile that will set tanyao as our selected strategy
tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3')
player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='7')
+ player.draw_tile(tile)
+ self.assertNotEqual(player.ai.current_strategy, None)
self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
# we draw a tile that will change our selected strategy
+ tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3')
+ player.init_hand(tiles)
+
+ tile = self._string_to_136_tile(sou='2')
+ meld, _ = player.try_to_call_meld(tile, False)
+ self.assertNotEqual(player.ai.current_strategy, None)
+ self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
+ self.assertEqual(meld, None)
+
tile = self._string_to_136_tile(sou='8')
player.draw_tile(tile)
self.assertEqual(player.ai.current_strategy, None)
+ # for already opened hand we don't need to give up on selected strategy
tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3')
player.init_hand(tiles)
- self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
+ player.draw_tile(self._string_to_136_tile(honors='5'))
+ player.discard_tile()
- # for already opened hand we don't need to give up on selected strategy
- meld = Meld()
- meld.tiles = [1, 2, 3]
+ meld = self._make_meld(Meld.PON, man='333')
player.add_called_meld(meld)
tile = self._string_to_136_tile(sou='8')
player.draw_tile(tile)
+
+ self.assertNotEqual(player.ai.current_strategy, None)
self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
def test_remaining_tiles_and_enemy_discard(self):
@@ -151,22 +311,22 @@ def test_remaining_tiles_and_enemy_discard(self):
tiles = self._string_to_136_array(man='123456789', sou='167', honors='77')
player.init_hand(tiles)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 8)
+ self.assertEqual(result.ukeire, 8)
player.table.add_discarded_tile(1, self._string_to_136_tile(sou='5'), False)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 7)
+ self.assertEqual(result.ukeire, 7)
player.table.add_discarded_tile(2, self._string_to_136_tile(sou='5'), False)
player.table.add_discarded_tile(3, self._string_to_136_tile(sou='8'), False)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 5)
+ self.assertEqual(result.ukeire, 5)
def test_remaining_tiles_and_opened_meld(self):
table = Table()
@@ -175,9 +335,9 @@ def test_remaining_tiles_and_opened_meld(self):
tiles = self._string_to_136_array(man='123456789', sou='167', honors='77')
player.init_hand(tiles)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 8)
+ self.assertEqual(result.ukeire, 8)
# was discard and set was opened
tile = self._string_to_136_tile(sou='8')
@@ -186,9 +346,9 @@ def test_remaining_tiles_and_opened_meld(self):
meld.called_tile = tile
player.table.add_called_meld(3, meld)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 5)
+ self.assertEqual(result.ukeire, 5)
# was discard and set was opened
tile = self._string_to_136_tile(sou='3')
@@ -197,9 +357,9 @@ def test_remaining_tiles_and_opened_meld(self):
meld.called_tile = tile
player.table.add_called_meld(2, meld)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 4)
+ self.assertEqual(result.ukeire, 4)
def test_remaining_tiles_and_dora_indicators(self):
table = Table()
@@ -208,15 +368,15 @@ def test_remaining_tiles_and_dora_indicators(self):
tiles = self._string_to_136_array(man='123456789', sou='167', honors='77')
player.init_hand(tiles)
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 8)
+ self.assertEqual(result.ukeire, 8)
table.add_dora_indicator(self._string_to_136_tile(sou='8'))
- results, shanten = player.ai.calculate_outs(tiles, tiles)
+ results, shanten = player.ai.hand_builder.find_discard_options(tiles, tiles)
result = [x for x in results if x.tile_to_discard == self._string_to_34_tile(sou='1')][0]
- self.assertEqual(result.tiles_count, 7)
+ self.assertEqual(result.ukeire, 7)
def test_using_tiles_of_different_suit_for_chi(self):
"""
@@ -226,6 +386,7 @@ def test_using_tiles_of_different_suit_for_chi(self):
player = table.player
# 16m2679p1348s111z
+ table.dora_indicators.append(self._string_to_136_tile(honors='4'))
tiles = [0, 21, 41, 56, 61, 70, 74, 80, 84, 102, 108, 110, 111]
player.init_hand(tiles)
@@ -234,132 +395,238 @@ def test_using_tiles_of_different_suit_for_chi(self):
meld, _ = player.try_to_call_meld(tile, True)
self.assertIsNotNone(meld)
- def test_upgrade_opened_pon_to_kan(self):
+ def test_call_upgrade_pon_and_bad_ukeire_after_call(self):
table = Table()
- player = table.player
+ table.count_of_remaining_tiles = 10
tiles = self._string_to_136_array(man='34445', sou='123456', pin='89')
- player.init_hand(tiles)
+ table.player.init_hand(tiles)
tile = self._string_to_136_tile(man='4')
- player.draw_tile(tile)
+
+ self.assertEqual(table.player.should_call_kan(tile, False), None)
+
+ table.player.add_called_meld(self._make_meld(Meld.PON, man='444'))
+
+ self.assertEqual(len(table.player.melds), 1)
+ self.assertEqual(len(table.player.tiles), 13)
+ self.assertEqual(table.player.should_call_kan(tile, False), None)
+
+ def test_call_upgrade_pon_and_bad_ukeire_after_call_second_case(self):
+ table = Table()
+ table.add_dora_indicator(self._string_to_136_tile(honors='5'))
+ table.count_of_remaining_tiles = 10
+ player = table.player
+
+ tiles = self._string_to_136_array(man='3455567', sou='222', honors='666')
+ player.init_hand(tiles)
+ player.add_called_meld(self._make_meld(Meld.PON, man='555'))
+ player.add_called_meld(self._make_meld(Meld.PON, honors='666'))
+
+ tile = self._string_to_136_tile(man='5')
self.assertEqual(player.should_call_kan(tile, False), None)
- player.add_called_meld(self._make_meld(Meld.PON, man='444'))
+ player.draw_tile(tile)
+ discarded_tile = player.discard_tile()
- self.assertEqual(len(player.melds), 1)
- self.assertEqual(len(player.tiles), 14)
- self.assertEqual(player.should_call_kan(tile, False), Meld.CHANKAN)
+ self.assertEqual(self._to_string([discarded_tile]), '2s')
+
+ def test_call_upgrade_pon_and_bad_ukeire_after_call_third_case(self):
+ table = Table()
+ table.count_of_remaining_tiles = 10
+ player = table.player
+
+ tiles = self._string_to_136_array(man='67', pin='6', sou='1344478999')
+ table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, sou='444'))
+
+ tile = self._string_to_136_tile(sou='4')
+
+ # we don't want to call shouminkan here
+ self.assertEqual(table.player.should_call_kan(tile, False), None)
- player.discard_tile()
player.draw_tile(tile)
- player.add_called_meld(self._make_meld(Meld.CHANKAN, man='4444'))
+ discarded_tile = player.discard_tile()
- self.assertEqual(len(player.melds), 1)
- self.assertEqual(player.melds[0].type, Meld.CHANKAN)
- self.assertEqual(len(player.tiles), 13)
+ self.assertEqual(self._to_string([discarded_tile]), '6p')
+
+ def test_call_shouminkan(self):
+ table = Table()
+ table.count_of_remaining_tiles = 10
+
+ tiles = self._string_to_136_array(man='3455567', sou='222', honors='666')
+ table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, honors='666'))
+
+ tile = self._string_to_136_tile(honors='6')
+
+ self.assertEqual(table.player.should_call_kan(tile, False), Meld.CHANKAN)
def test_call_closed_kan(self):
table = Table()
- player = table.player
+ table.count_of_remaining_tiles = 10
tiles = self._string_to_136_array(man='12223', sou='111456', pin='12')
- player.init_hand(tiles)
+ table.player.init_hand(tiles)
tile = self._string_to_136_tile(man='2')
- player.draw_tile(tile)
# it is pretty stupid to call closed kan with 2m
- self.assertEqual(player.should_call_kan(tile, False), None)
+ self.assertEqual(table.player.should_call_kan(tile, False), None)
tiles = self._string_to_136_array(man='12223', sou='111456', pin='12')
- player.init_hand(tiles)
+ table.player.init_hand(tiles)
tile = self._string_to_136_tile(sou='1')
- player.draw_tile(tile)
# call closed kan with 1s is fine
- self.assertEqual(player.should_call_kan(tile, False), Meld.KAN)
+ self.assertEqual(table.player.should_call_kan(tile, False), Meld.KAN)
def test_opened_kan(self):
table = Table()
- player = table.player
+ table.count_of_remaining_tiles = 10
tiles = self._string_to_136_array(man='299', sou='111456', pin='1', honors='111')
- player.init_hand(tiles)
+ table.player.init_hand(tiles)
# to rebuild all caches
- player.draw_tile(self._string_to_136_tile(pin='9'))
- player.discard_tile()
+ table.player.draw_tile(self._string_to_136_tile(pin='9'))
+ table.player.discard_tile()
# our hand is closed, we don't need to call opened kan here
tile = self._string_to_136_tile(sou='1')
- self.assertEqual(player.should_call_kan(tile, True), None)
+ self.assertEqual(table.player.should_call_kan(tile, True), None)
- player.add_called_meld(self._make_meld(Meld.PON, honors='111'))
+ table.player.add_called_meld(self._make_meld(Meld.PON, honors='111'))
# our hand is open, but it is not tempai
# we don't need to open kan here
tile = self._string_to_136_tile(sou='1')
- self.assertEqual(player.should_call_kan(tile, True), None)
+ self.assertEqual(table.player.should_call_kan(tile, True), None)
+ def test_opened_kan_second_case(self):
table = Table()
- player = table.player
+ table.count_of_remaining_tiles = 10
tiles = self._string_to_136_array(man='2399', sou='111456', honors='111')
- player.init_hand(tiles)
- player.add_called_meld(self._make_meld(Meld.PON, honors='111'))
+ table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, honors='111'))
# to rebuild all caches
- player.draw_tile(self._string_to_136_tile(pin='9'))
- player.discard_tile()
+ table.player.draw_tile(self._string_to_136_tile(pin='9'))
+ table.player.discard_tile()
# our hand is open, in tempai and with a good wait
tile = self._string_to_136_tile(sou='1')
- self.assertEqual(player.should_call_kan(tile, True), Meld.KAN)
+ self.assertEqual(table.player.should_call_kan(tile, True), Meld.KAN)
+
+ def test_opened_kan_third_case(self):
+ # we are in tempai already and there was a crash on 5s meld suggestion
+
+ table = Table()
+ table.count_of_remaining_tiles = 10
+ table.add_dora_indicator(self._string_to_136_tile(honors='5'))
+
+ tiles = self._string_to_136_array(man='456', sou='55567678', honors='66')
+ table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.CHI, sou='678'))
+
+ # to rebuild all caches
+ table.player.draw_tile(self._string_to_136_tile(pin='9'))
+ table.player.discard_tile()
+
+ tile = self._string_to_136_tile(sou='5')
+ self.assertEqual(table.player.should_call_kan(tile, True), None)
+ self.assertEqual(table.player.try_to_call_meld(tile, True), (None, None))
+
+ def test_dont_call_kan_in_defence_mode(self):
+ table = Table()
+
+ tiles = self._string_to_136_array(man='12589', sou='111459', pin='12')
+ table.player.init_hand(tiles)
+
+ table.add_called_riichi(1)
+
+ tile = self._string_to_136_tile(sou='1')
+ self.assertEqual(table.player.should_call_kan(tile, False), None)
- def test_closed_kan_and_riichi(self):
+ def test_closed_kan_and_wrong_shanten_number_calculation(self):
+ """
+ Bot tried to call riichi with 567m666p14578s + [9999s] hand
+ """
table = Table()
- table.count_of_remaining_tiles = 60
player = table.player
- player.scores = 25000
- kan_tiles = self._string_to_136_array(pin='7777')
- tiles = self._string_to_136_array(pin='568', sou='1235788') + kan_tiles[:3]
+ tiles = self._string_to_136_array(man='56', sou='14578999', pin='666')
player.init_hand(tiles)
-
- # +3 to avoid tile duplication of 7 pin
- tile = kan_tiles[3]
+ tile = self._string_to_136_tile(man='7')
+ player.melds.append(self._make_meld(Meld.KAN, False, sou='9999'))
player.draw_tile(tile)
+ player.discard_tile()
- kan_type = player.should_call_kan(tile, False)
- self.assertEqual(kan_type, Meld.KAN)
+ # bot not in the tempai, because all 9s in the closed kan
+ self.assertEqual(player.ai.shanten, 1)
- meld = Meld()
- meld.type = Meld.KAN
- meld.tiles = kan_tiles
- meld.called_tile = tile
- meld.who = 0
- meld.from_who = 0
- meld.opened = False
+ def test_closed_kan_and_not_necessary_call(self):
+ """
+ Bot tried to call closed kan with 568m669p1478999s + 9s hand
+ """
+ table = Table()
+ player = table.player
- # replacement from the dead wall
- player.draw_tile(self._string_to_136_tile(pin='4'))
- table.add_called_meld(meld.who, meld)
- discard = player.discard_tile()
+ tiles = self._string_to_136_array(man='568', sou='1478999', pin='669')
+ player.init_hand(tiles)
+ tile = self._string_to_136_tile(sou='9')
- self.assertEqual(self._to_string([discard]), '8p')
- self.assertEqual(player.can_call_riichi(), True)
+ self.assertEqual(player.should_call_kan(tile, False), None)
- # with closed kan we can't call riichi
- player.melds[0].opened = True
- self.assertEqual(player.can_call_riichi(), False)
+ def test_closed_kan_same_shanten_bad_ukeire(self):
+ """
+ Bot tried to call closed kan with 4557888899m2z + 333m melded hand
+ Shanten number is the same, but ukeire becomes much worse
+ """
+ table = Table()
+ player = table.player
- def test_dont_call_kan_in_defence_mode(self):
+ table.add_dora_indicator(self._string_to_136_tile(honors='2'))
+ table.add_dora_indicator(self._string_to_136_tile(honors='4'))
+
+ table.count_of_remaining_tiles = 10
+
+ tiles = self._string_to_136_array(man='333455788899', honors='3')
+ player.init_hand(tiles)
+ player.melds.append(self._make_meld(Meld.PON, man='333'))
+
+ tile = self._string_to_136_tile(man='8')
+
+ self.assertEqual(player.should_call_kan(tile, False), None)
+
+ def test_closed_kan_same_shanten_same_ukeire(self):
table = Table()
+ player = table.player
- tiles = self._string_to_136_array(man='12589', sou='111459', pin='12')
- table.player.init_hand(tiles)
+ table.add_dora_indicator(self._string_to_136_tile(honors='2'))
+ table.add_dora_indicator(self._string_to_136_tile(honors='4'))
- table.add_called_riichi(1)
+ table.count_of_remaining_tiles = 10
+
+ tiles = self._string_to_136_array(man='3334557889', honors='333')
+ player.init_hand(tiles)
+ player.melds.append(self._make_meld(Meld.PON, man='333'))
+
+ tile = self._string_to_136_tile(honors='3')
+
+ self.assertEqual(player.should_call_kan(tile, False), Meld.KAN)
+
+ def test_kan_crash(self):
+ """
+ This was a crash in real game
+ related with open kan logic and agari without yaku state
+ """
+ table = Table()
+ table.count_of_remaining_tiles = 10
+
+ tiles = self._string_to_136_array(man='456', pin='78999', sou='666', honors='33')
+ table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, sou='666'))
+ tile = self._string_to_136_tile(pin='9')
- tile = self._string_to_136_tile(sou='1')
self.assertEqual(table.player.should_call_kan(tile, False), None)
diff --git a/project/game/ai/first_version/tests/tests_defence.py b/project/game/ai/first_version/tests/tests_defence.py
index 17ce0e02..8bbe4a0c 100644
--- a/project/game/ai/first_version/tests/tests_defence.py
+++ b/project/game/ai/first_version/tests/tests_defence.py
@@ -150,12 +150,6 @@ def test_should_go_for_defence_and_good_hand_with_drawn_tile(self):
table.add_called_riichi(3)
- results, shanten = table.player.ai.calculate_outs(table.player.tiles,
- table.player.closed_hand,
- table.player.open_hand_34_tiles)
- selected_tile = table.player.ai.process_discard_options_and_select_tile_to_discard(results, shanten)
-
- self.assertEqual(table.player.ai.defence.should_go_to_defence_mode(selected_tile), False)
result = table.player.discard_tile()
self.assertEqual(self._to_string([result]), '8m')
@@ -251,9 +245,13 @@ def test_try_to_discard_less_valuable_tiles_first_in_defence_mode(self):
def test_find_impossible_waits_and_honor_tiles(self):
table = Table()
- tiles = self._string_to_136_array(honors='1133')
+ tiles = self._string_to_136_array(honors='1133', man='123', sou='456', pin='999')
table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.CHI, man='123'))
+ table.player.add_called_meld(self._make_meld(Meld.CHI, sou='456'))
+ table.player.add_called_meld(self._make_meld(Meld.PON, pin='999'))
+
table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False)
table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
@@ -268,9 +266,11 @@ def test_find_impossible_waits_and_honor_tiles(self):
def test_find_impossible_waits_and_kabe_technique(self):
table = Table()
- tiles = self._string_to_136_array(pin='11122777799')
+ tiles = self._string_to_136_array(pin='11122777799', man='999')
table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, man='999'))
+
table.add_discarded_tile(1, self._string_to_136_tile(pin='2'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='2'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='9'), False)
@@ -284,9 +284,12 @@ def test_find_impossible_waits_and_kabe_technique(self):
self.assertEqual(self._to_string([x.value * 4 for x in result]), '19p')
table = Table()
- tiles = self._string_to_136_array(pin='33337777')
+ tiles = self._string_to_136_array(pin='33337777', man='888999')
table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, man='888'))
+ table.player.add_called_meld(self._make_meld(Meld.PON, man='999'))
+
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
@@ -300,9 +303,11 @@ def test_find_impossible_waits_and_kabe_technique(self):
self.assertEqual(self._to_string([x.value * 4 for x in result]), '5p')
table = Table()
- tiles = self._string_to_136_array(pin='33334446666')
+ tiles = self._string_to_136_array(pin='33334446666', man='999')
table.player.init_hand(tiles)
+ table.player.add_called_meld(self._make_meld(Meld.PON, man='999'))
+
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
table.add_discarded_tile(1, self._string_to_136_tile(pin='5'), False)
@@ -423,7 +428,7 @@ def test_defence_against_honitsu_first_case(self):
def test_defence_against_honitsu_second_case(self):
table = Table()
- tiles = self._string_to_136_array(sou='4', pin='223456', man='678', honors='66')
+ tiles = self._string_to_136_array(sou='4', pin='2223456', man='678', honors='66')
table.player.init_hand(tiles)
table.add_called_meld(1, self._make_meld(Meld.CHI, sou='789'))
diff --git a/project/game/ai/first_version/tests/tests_discards.py b/project/game/ai/first_version/tests/tests_discards.py
index 88aa1d52..ea29fbbf 100644
--- a/project/game/ai/first_version/tests/tests_discards.py
+++ b/project/game/ai/first_version/tests/tests_discards.py
@@ -3,8 +3,12 @@
from mahjong.constants import EAST, SOUTH, WEST, NORTH, HAKU, HATSU, CHUN, FIVE_RED_SOU, FIVE_RED_PIN
from mahjong.tests_mixin import TestMixin
+from mahjong.meld import Meld
+from mahjong.tile import Tile
from game.ai.discard import DiscardOption
+from game.ai.first_version.strategies.main import BaseStrategy
+from game.ai.first_version.strategies.tanyao import TanyaoStrategy
from game.table import Table
@@ -21,22 +25,22 @@ def test_discard_tile(self):
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '9m')
- self.assertEqual(player.ai.previous_shanten, 2)
+ self.assertEqual(player.ai.shanten, 2)
player.draw_tile(self._string_to_136_tile(pin='4'))
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '1p')
- self.assertEqual(player.ai.previous_shanten, 2)
+ self.assertEqual(player.ai.shanten, 2)
player.draw_tile(self._string_to_136_tile(pin='3'))
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '9p')
- self.assertEqual(player.ai.previous_shanten, 1)
+ self.assertEqual(player.ai.shanten, 1)
player.draw_tile(self._string_to_136_tile(man='4'))
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '5m')
- self.assertEqual(player.ai.previous_shanten, 0)
+ self.assertEqual(player.ai.shanten, 0)
def test_discard_tile_force_tsumogiri(self):
table = Table()
@@ -44,7 +48,8 @@ def test_discard_tile_force_tsumogiri(self):
player = table.player
tiles = self._string_to_136_array(sou='11134567', pin='456', man='45')
- tile = 57 # 6p
+ # 6p
+ tile = 57
player.init_hand(tiles)
player.draw_tile(tile)
@@ -52,7 +57,8 @@ def test_discard_tile_force_tsumogiri(self):
discarded_tile = player.discard_tile()
self.assertEqual(discarded_tile, tile)
- tiles = self._string_to_136_array(sou='11134567', pin='46', man='45') + [53] # simple five pin
+ # add not red five pin
+ tiles = self._string_to_136_array(sou='11134567', pin='46', man='45') + [53]
tile = FIVE_RED_PIN
player.init_hand(tiles)
@@ -66,6 +72,32 @@ def test_calculate_suit_tiles_value(self):
table = Table()
player = table.player
+ # 0 - 8 man
+ # 9 - 17 pin
+ # 18 - 26 sou
+ results = [
+ [0, 110], [9, 110], [18, 110],
+ [1, 120], [10, 120], [19, 120],
+ [2, 140], [11, 140], [20, 140],
+ [3, 150], [12, 150], [21, 150],
+ [4, 130], [13, 130], [22, 130],
+ [5, 150], [14, 150], [23, 150],
+ [6, 140], [15, 140], [24, 140],
+ [7, 120], [16, 120], [25, 120],
+ [8, 110], [17, 110], [26, 110]
+ ]
+
+ for item in results:
+ tile = item[0]
+ value = item[1]
+ option = DiscardOption(player, tile, 0, [], 0)
+ self.assertEqual(option.valuation, value)
+
+ def test_calculate_suit_tiles_value_and_tanyao_hand(self):
+ table = Table()
+ player = table.player
+ player.ai.current_strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
+
# 0 - 8 man
# 9 - 17 pin
# 18 - 26 sou
@@ -73,9 +105,9 @@ def test_calculate_suit_tiles_value(self):
[0, 110], [9, 110], [18, 110],
[1, 120], [10, 120], [19, 120],
[2, 130], [11, 130], [20, 130],
- [3, 140], [12, 140], [21, 140],
- [4, 150], [13, 150], [22, 150],
- [5, 140], [14, 140], [23, 140],
+ [3, 150], [12, 150], [21, 150],
+ [4, 140], [13, 140], [22, 140],
+ [5, 150], [14, 150], [23, 150],
[6, 130], [15, 130], [24, 130],
[7, 120], [16, 120], [25, 120],
[8, 110], [17, 110], [26, 110]
@@ -133,19 +165,37 @@ def test_calculate_suit_tiles_value_and_dora(self):
tile = self._string_to_34_tile(sou='1')
option = DiscardOption(player, tile, 0, [], 0)
- self.assertEqual(option.valuation, 160)
+ self.assertEqual(option.valuation, DiscardOption.DORA_VALUE + 110)
# double dora
table.dora_indicators = [self._string_to_136_tile(sou='9'), self._string_to_136_tile(sou='9')]
tile = self._string_to_34_tile(sou='1')
option = DiscardOption(player, tile, 0, [], 0)
- self.assertEqual(option.valuation, 210)
+ self.assertEqual(option.valuation, DiscardOption.DORA_VALUE * 2 + 110)
+
+ # tile close to dora
+ table.dora_indicators = [self._string_to_136_tile(sou='9')]
+ tile = self._string_to_34_tile(sou='2')
+ option = DiscardOption(player, tile, 0, [], 0)
+ self.assertEqual(option.valuation, DiscardOption.DORA_FIRST_NEIGHBOUR + 120)
+
+ # tile not far away from dora
+ table.dora_indicators = [self._string_to_136_tile(sou='9')]
+ tile = self._string_to_34_tile(sou='3')
+ option = DiscardOption(player, tile, 0, [], 0)
+ self.assertEqual(option.valuation, DiscardOption.DORA_SECOND_NEIGHBOUR + 140)
+
+ # tile from other suit
+ table.dora_indicators = [self._string_to_136_tile(sou='9')]
+ tile = self._string_to_34_tile(man='3')
+ option = DiscardOption(player, tile, 0, [], 0)
+ self.assertEqual(option.valuation, 140)
def test_discard_not_valuable_honor_first(self):
table = Table()
player = table.player
- tiles = self._string_to_136_array(sou='123456', pin='123456', man='9', honors='2')
+ tiles = self._string_to_136_array(sou='123456', pin='123455', man='9', honors='2')
player.init_hand(tiles)
discarded_tile = player.discard_tile()
@@ -192,7 +242,7 @@ def test_dont_keep_honor_with_small_number_of_shanten(self):
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '7z')
- def test_prefer_valuable_tiles_with_almost_same_tiles_count(self):
+ def test_prefer_valuable_tiles_with_almost_same_ukeire(self):
table = Table()
player = table.player
table.add_dora_indicator(self._string_to_136_tile(sou='4'))
@@ -203,3 +253,581 @@ def test_prefer_valuable_tiles_with_almost_same_tiles_count(self):
discarded_tile = player.discard_tile()
self.assertEqual(self._to_string([discarded_tile]), '1s')
+
+ def test_discard_less_valuable_isolated_tile_first(self):
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+
+ tiles = self._string_to_136_array(sou='2456', pin='129', man='234458')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='7'))
+
+ discarded_tile = player.discard_tile()
+ # we have a choice what to discard: 9p or 8m
+ # 9p is less valuable
+ self.assertEqual(self._to_string([discarded_tile]), '9p')
+
+ table.dora_indicators.append(self._string_to_136_tile(pin='8'))
+ tiles = self._string_to_136_array(sou='2456', pin='129', man='234458')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='7'))
+ discarded_tile = player.discard_tile()
+ # but if 9p is dora
+ # let's discard 8m instead
+ self.assertEqual(self._to_string([discarded_tile]), '8m')
+
+ def test_discard_tile_with_max_ukeire_second_level(self):
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+
+ tiles = self._string_to_136_array(sou='11367', pin='45', man='344778')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='6'))
+
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '3s')
+
+ # There was a bug with count of live tiles that are used in melds,
+ # hence this test
+ def test_choose_best_option_with_melds(self):
+ table = Table()
+ player = table.player
+ table.has_aka_dora = False
+
+ tiles = self._string_to_136_array(sou='245666789', honors='2266')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, sou='666')
+ player.add_called_meld(meld)
+ meld = self._make_meld(Meld.CHI, sou='789')
+ player.add_called_meld(meld)
+
+ player.draw_tile(self._string_to_136_tile(sou='5'))
+
+ discarded_tile = player.discard_tile()
+ # we should discard best ukeire option here - 2s
+ self.assertEqual(self._to_string([discarded_tile]), '2s')
+
+ def test_choose_best_wait_with_melds(self):
+ table = Table()
+ player = table.player
+ table.has_aka_dora = False
+
+ tiles = self._string_to_136_array(sou='1222233455599')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.CHI, sou='123')
+ player.add_called_meld(meld)
+ meld = self._make_meld(Meld.PON, sou='222')
+ player.add_called_meld(meld)
+ meld = self._make_meld(Meld.PON, sou='555')
+ player.add_called_meld(meld)
+
+ player.draw_tile(self._string_to_136_tile(sou='4'))
+
+ discarded_tile = player.discard_tile()
+ # double-pairs wait becomes better, because it has 4 tiles to wait for
+ # against just 1 in ryanmen
+ self.assertEqual(self._to_string([discarded_tile]), '3s')
+
+ def test_discard_tile_with_better_wait_in_iishanten(self):
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+
+ tiles = self._string_to_136_array(man='123567', pin='113788', sou='99')
+ player.init_hand(tiles)
+
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '8p')
+
+ def test_discard_tile_and_wrong_tiles_valuation(self):
+ """
+ Bot wanted to discard 5m from the first hand,
+ because valuation for 2p was miscalculated (too high)
+
+ Same issue with wrong valuation was with second hand
+ """
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(honors='2'))
+
+ tiles = self._string_to_136_array(man='445567', pin='245678', sou='67')
+ player.init_hand(tiles)
+
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2p')
+
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(man='5'))
+
+ tiles = self._string_to_136_array(man='45667', pin='34677', sou='38', honors='22')
+ player.init_hand(tiles)
+
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '8s')
+
+ def test_choose_correct_wait_finished_yaku(self):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+
+ tiles = self._string_to_136_array(man='23478', sou='23488', pin='235')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '5p')
+
+ tiles = self._string_to_136_array(man='34578', sou='34588', pin='235')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2p')
+
+ tiles = self._string_to_136_array(man='34578', sou='34588', pin='235')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2p')
+
+ tiles = self._string_to_136_array(man='3457', sou='233445588')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(man='8'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2s')
+
+ tiles = self._string_to_136_array(man='3457', sou='223344588')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(man='8'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '5s')
+
+ def test_choose_correct_wait_yaku_versus_dora(self):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+
+ table.add_dora_indicator(self._string_to_136_tile(pin='4'))
+
+ tiles = self._string_to_136_array(man='23478', sou='23488', pin='235')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '5p')
+
+ table = Table()
+ player = table.player
+ player.round_step = 2
+
+ table.add_dora_indicator(self._string_to_136_tile(pin='1'))
+
+ tiles = self._string_to_136_array(man='34578', sou='34588', pin='235')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(pin='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '2p')
+
+ def test_choose_correct_wait_yaku_potentially(self):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+
+ tiles = self._string_to_136_array(man='1134578', sou='567788')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(man='9'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '5s')
+
+ tiles = self._string_to_136_array(man='1134578', sou='556678')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(man='9'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '8s')
+
+ def test_choose_better_tanki_honor(self):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+ player.dealer_seat = 3
+
+ table.add_dora_indicator(self._string_to_136_tile(man='8'))
+
+ tiles = self._string_to_136_array(man='11447799', sou='556', honors='45')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(honors='4'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '6s')
+
+ tiles = self._string_to_136_array(man='11447799', sou='556', honors='45')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(honors='5'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '6s')
+
+ tiles = self._string_to_136_array(man='11447799', sou='556', honors='45')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='6'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '5z')
+
+ tiles = self._string_to_136_array(man='11447799', sou='556', honors='34')
+ player.init_hand(tiles)
+ player.draw_tile(self._string_to_136_tile(sou='6'))
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), '3z')
+
+ def _choose_tanki_with_kabe_helper(self, tiles, kabe_tiles, tile_to_draw, tile_to_discard_str):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+ player.dealer_seat = 3
+
+ for tile in kabe_tiles:
+ for _ in range(0, 4):
+ table.add_discarded_tile(1, tile, False)
+
+ player.init_hand(tiles)
+ player.draw_tile(tile_to_draw)
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str)
+
+ def test_choose_tanki_with_kabe(self):
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='119', pin='224477', man='5669'),
+ [self._string_to_136_tile(sou='8')],
+ self._string_to_136_tile(man='5'),
+ '9m'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='119', pin='224477', man='5669'),
+ [self._string_to_136_tile(man='8')],
+ self._string_to_136_tile(man='5'),
+ '9s'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='118', pin='224477', man='5668'),
+ [self._string_to_136_tile(sou='7')],
+ self._string_to_136_tile(man='5'),
+ '8m'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='118', pin='224477', man='5668'),
+ [self._string_to_136_tile(man='7')],
+ self._string_to_136_tile(man='5'),
+ '8s'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='117', pin='224477', man='1157'),
+ [self._string_to_136_tile(sou='6'), self._string_to_136_tile(sou='9')],
+ self._string_to_136_tile(man='5'),
+ '7m'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='117', pin='224477', man='1157'),
+ [self._string_to_136_tile(man='6'), self._string_to_136_tile(man='9')],
+ self._string_to_136_tile(man='5'),
+ '7s'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='116', pin='224477', man='1126'),
+ [self._string_to_136_tile(sou='5'), self._string_to_136_tile(sou='7')],
+ self._string_to_136_tile(man='2'),
+ '6m'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='116', pin='224477', man='1126'),
+ [self._string_to_136_tile(man='5'), self._string_to_136_tile(man='7')],
+ self._string_to_136_tile(man='2'),
+ '6s'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='115', pin='224477', man='1125'),
+ [self._string_to_136_tile(sou='4'), self._string_to_136_tile(sou='6')],
+ self._string_to_136_tile(man='2'),
+ '5m'
+ )
+
+ self._choose_tanki_with_kabe_helper(
+ self._string_to_136_array(sou='115', pin='224477', man='1125'),
+ [self._string_to_136_tile(man='4'), self._string_to_136_tile(man='6')],
+ self._string_to_136_tile(man='2'),
+ '5s'
+ )
+
+ def _choose_tanki_with_suji_helper(self, tiles, suji_tiles, tile_to_draw, tile_to_discard_str):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+ player.dealer_seat = 3
+
+ player.init_hand(tiles)
+
+ for tile in suji_tiles:
+ player.add_discarded_tile(Tile(tile, True))
+
+ player.draw_tile(tile_to_draw)
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str)
+
+ def test_choose_tanki_with_suji(self):
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='19', pin='99', honors='2'),
+ [self._string_to_136_tile(sou='6')],
+ self._string_to_136_tile(honors='2'),
+ '1s'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='19', pin='99', honors='2'),
+ [self._string_to_136_tile(sou='4')],
+ self._string_to_136_tile(honors='2'),
+ '9s'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='2', pin='299', honors='2'),
+ [self._string_to_136_tile(sou='5')],
+ self._string_to_136_tile(honors='2'),
+ '2p'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='2', pin='299', honors='2'),
+ [self._string_to_136_tile(pin='5')],
+ self._string_to_136_tile(honors='2'),
+ '2s'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='3', pin='399', honors='2'),
+ [self._string_to_136_tile(sou='6')],
+ self._string_to_136_tile(honors='2'),
+ '3p'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='3', pin='399', honors='2'),
+ [self._string_to_136_tile(pin='6')],
+ self._string_to_136_tile(honors='2'),
+ '3s'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='4', pin='499', honors='2'),
+ [self._string_to_136_tile(sou='1'), self._string_to_136_tile(sou='7')],
+ self._string_to_136_tile(honors='2'),
+ '4p'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='4', pin='499', honors='2'),
+ [self._string_to_136_tile(pin='1'), self._string_to_136_tile(pin='7')],
+ self._string_to_136_tile(honors='2'),
+ '4s'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='5', pin='599', honors='2'),
+ [self._string_to_136_tile(sou='2'), self._string_to_136_tile(sou='8')],
+ self._string_to_136_tile(honors='2'),
+ '5p'
+ )
+
+ self._choose_tanki_with_suji_helper(
+ self._string_to_136_array(man='22336688', sou='5', pin='599', honors='2'),
+ [self._string_to_136_tile(pin='2'), self._string_to_136_tile(pin='8')],
+ self._string_to_136_tile(honors='2'),
+ '5s'
+ )
+
+ def _avoid_furiten_helper(self, tiles, furiten_tile, other_tile, tile_to_draw, tile_to_discard_str):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+ player.dealer_seat = 3
+
+ player.init_hand(tiles)
+
+ player.add_discarded_tile(Tile(furiten_tile, True))
+
+ for _ in range(0, 2):
+ table.add_discarded_tile(1, other_tile, False)
+
+ player.draw_tile(tile_to_draw)
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str)
+
+ def test_avoid_furiten(self):
+ self._avoid_furiten_helper(
+ self._string_to_136_array(man='22336688', pin='99', honors='267'),
+ self._string_to_136_tile(honors='6'),
+ self._string_to_136_tile(honors='7'),
+ self._string_to_136_tile(honors='2'),
+ '6z'
+ )
+
+ self._avoid_furiten_helper(
+ self._string_to_136_array(man='22336688', pin='99', honors='267'),
+ self._string_to_136_tile(honors='7'),
+ self._string_to_136_tile(honors='6'),
+ self._string_to_136_tile(honors='2'),
+ '7z'
+ )
+
+ def _choose_furiten_over_karaten_helper(self, tiles, furiten_tile, karaten_tile, tile_to_draw, tile_to_discard_str):
+ table = Table()
+ player = table.player
+ player.round_step = 2
+ player.dealer_seat = 3
+
+ player.init_hand(tiles)
+
+ player.add_discarded_tile(Tile(furiten_tile, True))
+
+ for _ in range(0, 3):
+ table.add_discarded_tile(1, karaten_tile, False)
+
+ player.draw_tile(tile_to_draw)
+ discarded_tile = player.discard_tile()
+ self.assertEqual(self._to_string([discarded_tile]), tile_to_discard_str)
+
+ def test_choose_furiten_over_karaten(self):
+ self._choose_furiten_over_karaten_helper(
+ self._string_to_136_array(man='22336688', pin='99', honors='267'),
+ self._string_to_136_tile(honors='6'),
+ self._string_to_136_tile(honors='7'),
+ self._string_to_136_tile(honors='2'),
+ '7z'
+ )
+
+ self._choose_furiten_over_karaten_helper(
+ self._string_to_136_array(man='22336688', pin='99', honors='267'),
+ self._string_to_136_tile(honors='7'),
+ self._string_to_136_tile(honors='6'),
+ self._string_to_136_tile(honors='2'),
+ '6z'
+ )
+
+ def test_discard_tile_based_on_second_level_ukeire_and_cost(self):
+ table = Table()
+ player = table.player
+
+ table.add_dora_indicator(self._string_to_136_tile(man='2'))
+ table.add_discarded_tile(1, self._string_to_136_tile(man='2'), False)
+
+ tiles = self._string_to_136_array(man='34678', pin='2356', sou='4467')
+ tile = self._string_to_136_tile(sou='8')
+
+ player.init_hand(tiles)
+ player.draw_tile(tile)
+
+ discarded_tile = player.discard_tile()
+ discard_correct = self._to_string([discarded_tile]) == '2p' or self._to_string([discarded_tile]) == '3p'
+ self.assertEqual(discard_correct, True)
+
+ def test_calculate_second_level_ukeire(self):
+ """
+ There was a bug with 2356 form and second level ukeire
+ """
+ table = Table()
+ player = table.player
+
+ table.add_dora_indicator(self._string_to_136_tile(man='2'))
+ table.add_discarded_tile(1, self._string_to_136_tile(man='2'), False)
+ table.add_discarded_tile(1, self._string_to_136_tile(pin='3'), False)
+ table.add_discarded_tile(1, self._string_to_136_tile(pin='3'), False)
+
+ tiles = self._string_to_136_array(man='34678', pin='2356', sou='4467')
+ tile = self._string_to_136_tile(sou='8')
+
+ player.init_hand(tiles)
+ player.draw_tile(tile)
+
+ discard_options, _ = player.ai.hand_builder.find_discard_options(
+ player.tiles,
+ player.closed_hand,
+ player.melds
+ )
+
+ tile = self._string_to_136_tile(man='4')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 108)
+
+ tile = self._string_to_136_tile(man='3')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 108)
+
+ tile = self._string_to_136_tile(pin='2')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 96)
+
+ tile = self._string_to_136_tile(pin='3')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 96)
+
+ tile = self._string_to_136_tile(pin='5')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 96)
+
+ tile = self._string_to_136_tile(pin='6')
+ discard_option = [x for x in discard_options if x.tile_to_discard == tile // 4][0]
+ player.ai.hand_builder.calculate_second_level_ukeire(discard_option, player.tiles, player.melds)
+ self.assertEqual(discard_option.ukeire_second, 96)
+
+ def test_choose_1_shanten_with_cost_possibility_draw(self):
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+
+ tiles = self._string_to_136_array(man='557', pin='468', sou='55577', honors='66')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, sou='555')
+ player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(sou='7')
+ player.draw_tile(tile)
+ discarded_tile = player.discard_tile()
+ self.assertNotEqual(player.ai.current_strategy, None)
+ self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI)
+ self.assertEqual(self._to_string([discarded_tile]), '7m')
+
+ def test_choose_1_shanten_with_cost_possibility_meld(self):
+ table = Table()
+ player = table.player
+ table.add_dora_indicator(self._string_to_136_tile(sou='4'))
+
+ tiles = self._string_to_136_array(man='557', pin='468', sou='55577', honors='66')
+ player.init_hand(tiles)
+
+ meld = self._make_meld(Meld.PON, sou='555')
+ player.add_called_meld(meld)
+
+ tile = self._string_to_136_tile(sou='7')
+ meld, discard_option = player.try_to_call_meld(tile, False)
+ self.assertNotEqual(meld, None)
+ self.assertEqual(meld.type, Meld.PON)
+ self.assertEqual(self._to_string(meld.tiles), '777s')
+
+ self.assertNotEqual(player.ai.current_strategy, None)
+ self.assertEqual(player.ai.current_strategy.type, BaseStrategy.YAKUHAI)
+
+ discarded_tile = discard_option.find_tile_in_hand(player.closed_hand)
+
+ self.assertEqual(self._to_string([discarded_tile]), '7m')
diff --git a/project/game/ai/first_version/tests/tests_riichi.py b/project/game/ai/first_version/tests/tests_riichi.py
index bdf65a73..574e0f25 100644
--- a/project/game/ai/first_version/tests/tests_riichi.py
+++ b/project/game/ai/first_version/tests/tests_riichi.py
@@ -2,50 +2,199 @@
import unittest
from mahjong.tests_mixin import TestMixin
+from mahjong.tile import Tile
from game.table import Table
class CallRiichiTestCase(unittest.TestCase, TestMixin):
+ def _make_table(self, dora_indicators=None):
+ self.table = Table()
+ self.table.count_of_remaining_tiles = 60
+ self.player = self.table.player
+ self.player.scores = 25000
- def test_dont_call_riichi_with_tanki_wait(self):
- table = Table()
- table.count_of_remaining_tiles = 60
- player = table.player
- player.scores = 25000
+ # with that we don't have daburi anymore
+ self.player.round_step = 1
- tiles = self._string_to_136_array(sou='123456', pin='123456', man='3')
- player.init_hand(tiles)
+ if dora_indicators:
+ for x in dora_indicators:
+ self.table.add_dora_indicator(x)
- player.draw_tile(self._string_to_136_tile(man='4'))
- player.discard_tile()
+ def test_dont_call_riichi_with_yaku_and_central_tanki_wait(self):
+ self._make_table()
- self.assertEqual(player.can_call_riichi(), False)
+ tiles = self._string_to_136_array(sou='234567', pin='234567', man='4')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(man='5'))
+ self.player.discard_tile()
- table = Table()
- table.count_of_remaining_tiles = 60
- player = table.player
- player.scores = 25000
+ self.assertEqual(self.player.can_call_riichi(), False)
- tiles = self._string_to_136_array(sou='1133557799', pin='113')
- tile = self._string_to_136_tile(pin='6')
- player.init_hand(tiles)
- player.draw_tile(tile)
- player.discard_tile()
+ def test_dont_call_riichi_expensive_damaten_with_yaku(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(man='7'),
+ self._string_to_136_tile(man='5'),
+ self._string_to_136_tile(sou='1'),
+ ])
- # for chitoitsu it is ok to have a pair wait
- self.assertEqual(player.can_call_riichi(), True)
+ # tanyao pinfu sanshoku dora 4 - this is damaten baiman, let's not riichi it
+ tiles = self._string_to_136_array(man='67888', sou='678', pin='34678')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
- def test_call_riichi_and_penchan_wait(self):
- table = Table()
- table.count_of_remaining_tiles = 60
- player = table.player
- player.scores = 25000
+ # let's test lots of doras hand, tanyao dora 8, also damaten baiman
+ tiles = self._string_to_136_array(man='666888', sou='22', pin='34678')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
+
+ # chuuren
+ tiles = self._string_to_136_array(man='1112345678999')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
+
+ def test_riichi_expensive_hand_without_yaku(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(man='1'),
+ self._string_to_136_tile(sou='1'),
+ self._string_to_136_tile(pin='1')
+ ])
+
+ tiles = self._string_to_136_array(man='222', sou='22278', pin='22789')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_riichi_tanki_honor_without_yaku(self):
+ self._make_table(
+ dora_indicators=[
+ self._string_to_136_tile(man='2'),
+ self._string_to_136_tile(sou='6')
+ ]
+ )
+
+ tiles = self._string_to_136_array(man='345678', sou='789', pin='123', honors='2')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_riichi_tanki_honor_chiitoitsu(self):
+ self._make_table()
+
+ tiles = self._string_to_136_array(man='22336688', sou='99', pin='99', honors='2')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_always_call_daburi(self):
+ self._make_table()
+ self.player.round_step = 0
+
+ tiles = self._string_to_136_array(sou='234567', pin='234567', man='4')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(man='5'))
+ self.player.discard_tile()
+
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_dont_call_karaten_tanki_riichi(self):
+ self._make_table()
+
+ tiles = self._string_to_136_array(man='22336688', sou='99', pin='99', honors='2')
+ self.player.init_hand(tiles)
+
+ for _ in range(0, 3):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(honors='2'), False)
+ self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
+
+ def test_dont_call_karaten_ryanmen_riichi(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(man='1'),
+ self._string_to_136_tile(sou='1'),
+ self._string_to_136_tile(pin='1')
+ ])
+
+ tiles = self._string_to_136_array(man='222', sou='22278', pin='22789')
+ self.player.init_hand(tiles)
+
+ for _ in range(0, 4):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(sou='6'), False)
+ self.table.add_discarded_tile(1, self._string_to_136_tile(sou='9'), False)
+
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
+
+ def test_call_riichi_penchan_with_suji(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(pin='1'),
+ ])
tiles = self._string_to_136_array(sou='11223', pin='234567', man='66')
- tile = self._string_to_136_tile(man='9')
- player.init_hand(tiles)
- player.draw_tile(tile)
- player.discard_tile()
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(sou='6'))
+ self.player.discard_tile()
+
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_call_riichi_tanki_with_kabe(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(pin='1'),
+ ])
+
+ for _ in range(0, 3):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(honors='1'), False)
+
+ for _ in range(0, 4):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(sou='8'), False)
+
+ tiles = self._string_to_136_array(sou='1119', pin='234567', man='666')
+ self.player.init_hand(tiles)
+ self.player.draw_tile(self._string_to_136_tile(honors='1'))
+ self.player.discard_tile()
+
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_call_riichi_chiitoitsu_with_suji(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(man='1'),
+ ])
+
+ for _ in range(0, 3):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+
+ tiles = self._string_to_136_array(man='22336688', sou='9', pin='99', honors='22')
+ self.player.init_hand(tiles)
+ self.player.add_discarded_tile(Tile(self._string_to_136_tile(sou='6'), True))
+
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), True)
+
+ def test_dont_call_riichi_chiitoitsu_bad_wait(self):
+ self._make_table(dora_indicators=[
+ self._string_to_136_tile(man='1'),
+ ])
+
+ for _ in range(0, 3):
+ self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
+
+ tiles = self._string_to_136_array(man='22336688', sou='4', pin='99', honors='22')
+ self.player.init_hand(tiles)
- self.assertEqual(player.can_call_riichi(), True)
+ self.player.draw_tile(self._string_to_136_tile(honors='3'))
+ self.player.discard_tile()
+ self.assertEqual(self.player.can_call_riichi(), False)
diff --git a/project/game/ai/first_version/tests/tests_strategies.py b/project/game/ai/first_version/tests/tests_strategies.py
deleted file mode 100644
index d9bae4b7..00000000
--- a/project/game/ai/first_version/tests/tests_strategies.py
+++ /dev/null
@@ -1,650 +0,0 @@
-# -*- coding: utf-8 -*-
-import unittest
-
-from mahjong.meld import Meld
-from mahjong.tests_mixin import TestMixin
-
-from game.ai.first_version.strategies.honitsu import HonitsuStrategy
-from game.ai.first_version.strategies.main import BaseStrategy
-from game.ai.first_version.strategies.tanyao import TanyaoStrategy
-from game.ai.first_version.strategies.yakuhai import YakuhaiStrategy
-from game.table import Table
-
-
-class YakuhaiStrategyTestCase(unittest.TestCase, TestMixin):
-
- def test_should_activate_strategy(self):
- table = Table()
- player = table.player
- strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
-
- tiles = self._string_to_136_array(sou='12355689', man='89', honors='123')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='12355689', man='89', honors='44')
- player.init_hand(tiles)
- player.dealer_seat = 1
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- tiles = self._string_to_136_array(sou='12355689', man='89', honors='666')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- # with chitoitsu-like hand we don't need to go for yakuhai
- tiles = self._string_to_136_array(sou='1235566', man='8899', honors='66')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- def test_dont_activate_strategy_if_we_dont_have_enough_tiles_in_the_wall(self):
- table = Table()
- player = table.player
- strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
-
- tiles = self._string_to_136_array(sou='12355689', man='89', honors='44')
- player.init_hand(tiles)
- player.dealer_seat = 1
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- table.add_discarded_tile(3, self._string_to_136_tile(honors='4'), False)
- table.add_discarded_tile(3, self._string_to_136_tile(honors='4'), False)
-
- # we can't complete yakuhai, because there is not enough honor tiles
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- def test_suitable_tiles(self):
- table = Table()
- player = table.player
- strategy = YakuhaiStrategy(BaseStrategy.YAKUHAI, player)
-
- # for yakuhai we can use any tile
- for tile in range(0, 136):
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- def test_open_hand_with_yakuhai_pair_in_hand(self):
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(sou='123678', pin='25899', honors='44')
- tile = self._string_to_136_tile(honors='4')
- player.init_hand(tiles)
-
- # we don't need to open hand with not our wind
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- # with dragon pair in hand let's open our hand
- tiles = self._string_to_136_array(sou='1689', pin='2358', man='1', honors='4455')
- tile = self._string_to_136_tile(honors='4')
- player.init_hand(tiles)
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
- player.add_called_meld(meld)
- player.tiles.append(tile)
-
- self.assertEqual(meld.type, Meld.PON)
- self.assertEqual(self._to_string(meld.tiles), '444z')
- self.assertEqual(len(player.closed_hand), 11)
- self.assertEqual(len(player.tiles), 14)
- player.discard_tile()
-
- tile = self._string_to_136_tile(honors='5')
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
- player.add_called_meld(meld)
- player.tiles.append(tile)
-
- self.assertEqual(meld.type, Meld.PON)
- self.assertEqual(self._to_string(meld.tiles), '555z')
- self.assertEqual(len(player.closed_hand), 8)
- self.assertEqual(len(player.tiles), 14)
- player.discard_tile()
-
- tile = self._string_to_136_tile(sou='7')
- # we can call chi only from left player
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- meld, _ = player.try_to_call_meld(tile, True)
- self.assertNotEqual(meld, None)
- player.add_called_meld(meld)
- player.tiles.append(tile)
-
- self.assertEqual(meld.type, Meld.CHI)
- self.assertEqual(self._to_string(meld.tiles), '678s')
- self.assertEqual(len(player.closed_hand), 5)
- self.assertEqual(len(player.tiles), 14)
-
- def test_force_yakuhai_pair_waiting_for_tempai_hand(self):
- """
- If hand shanten = 1 don't open hand except the situation where is
- we have tempai on yakuhai tile after open set
- """
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(sou='123', pin='678', man='34468', honors='66')
- player.init_hand(tiles)
-
- # we will not get tempai on yakuhai pair with this hand, so let's skip this call
- tile = self._string_to_136_tile(man='5')
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- # but here we will have atodzuke tempai
- tile = self._string_to_136_tile(man='7')
- meld, _ = player.try_to_call_meld(tile, True)
- self.assertNotEqual(meld, None)
- self.assertEqual(meld.type, Meld.CHI)
- self.assertEqual(self._to_string(meld.tiles), '678m')
-
- table = Table()
- player = table.player
-
- # we can open hand in that case
- tiles = self._string_to_136_array(man='44556', sou='366789', honors='77')
- player.init_hand(tiles)
-
- tile = self._string_to_136_tile(honors='7')
- meld, _ = player.try_to_call_meld(tile, True)
- self.assertNotEqual(meld, None)
- self.assertEqual(self._to_string(meld.tiles), '777z')
-
- def test_call_yakuhai_pair_and_special_conditions(self):
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(man='56', sou='1235', pin='12888', honors='11')
- player.init_hand(tiles)
-
- meld = self._make_meld(Meld.PON, pin='888')
- player.add_called_meld(meld)
-
- # to update previous_shanten attribute
- player.draw_tile(self._string_to_136_tile(honors='3'))
- player.discard_tile()
-
- tile = self._string_to_136_tile(honors='1')
- meld, _ = player.try_to_call_meld(tile, True)
- self.assertNotEqual(meld, None)
-
- def test_tempai_without_yaku(self):
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(sou='678', pin='12355', man='456', honors='77')
- player.init_hand(tiles)
-
- tile = self._string_to_136_tile(pin='5')
- player.draw_tile(tile)
- meld = self._make_meld(Meld.CHI, sou='678')
- player.add_called_meld(meld)
-
- discard = player.discard_tile()
- self.assertEqual(self._to_string([discard]), '1p')
-
- def test_get_more_yakuhai_sets_in_hand(self):
- table = Table()
-
- tiles = self._string_to_136_array(sou='1378', pin='67', man='68', honors='5566')
- table.player.init_hand(tiles)
-
- tile = self._string_to_136_tile(honors='5')
- meld, discard_option = table.player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
-
- table.add_called_meld(0, meld)
- table.player.tiles.append(tile)
- table.player.discard_tile(discard_option)
-
- tile = self._string_to_136_tile(honors='6')
- meld, _ = table.player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
-
- table = Table()
-
- tiles = self._string_to_136_array(sou='234', pin='788', man='567', honors='5566')
- table.player.init_hand(tiles)
-
- tile = self._string_to_136_tile(honors='5')
- meld, discard_option = table.player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
-
- table.add_called_meld(0, meld)
- table.player.tiles.append(tile)
- table.player.discard_tile(discard_option)
-
- tile = self._string_to_136_tile(honors='6')
- meld, _ = table.player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- def test_atodzuke_opened_hand(self):
- table = Table()
- player = table.player
-
- # 456m12355p22z + 5p [678s]
- tiles = self._string_to_136_array(sou='4589', pin='123', man='1236', honors='66')
- player.init_hand(tiles)
-
- tile = self._string_to_136_tile(man='6')
- player.draw_tile(tile)
- meld = self._make_meld(Meld.CHI, pin='123')
- player.add_called_meld(meld)
-
- discard = player.discard_tile()
- self.assertEqual(self._to_string([discard]), '9s')
-
- def test_wrong_shanten_improvements_detection(self):
- """
- With hand 2345s1p11z bot wanted to open set on 2s,
- so after opened set we will get 25s1p11z
- it is not correct logic, because we ruined our hand
- :return:
- """
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(sou='2345999', honors='114446')
- player.init_hand(tiles)
-
- meld = self._make_meld(Meld.PON, sou='999')
- player.add_called_meld(meld)
- meld = self._make_meld(Meld.PON, honors='444')
- player.add_called_meld(meld)
-
- # to rebuild all caches
- player.draw_tile(self._string_to_136_tile(pin='2'))
- player.discard_tile()
-
- tile = self._string_to_136_tile(sou='2')
- meld, _ = table.player.try_to_call_meld(tile, True)
- self.assertEqual(meld, None)
-
-
-class HonitsuStrategyTestCase(unittest.TestCase, TestMixin):
-
- def test_should_activate_strategy(self):
- table = Table()
- player = table.player
- strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
-
- tiles = self._string_to_136_array(sou='12355', man='12389', honors='123')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='12355', man='238', honors='11234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- # with hand without pairs we not should go for honitsu,
- # because it is far away from tempai
- tiles = self._string_to_136_array(sou='12358', man='238', honors='12345')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- # with chitoitsu-like hand we don't need to go for honitsu
- tiles = self._string_to_136_array(pin='77', man='3355677899', sou='11')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- def test_suitable_tiles(self):
- table = Table()
- player = table.player
- strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
-
- tiles = self._string_to_136_array(sou='12355', man='238', honors='11234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- tile = self._string_to_136_tile(man='1')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(pin='1')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(sou='1')
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- tile = self._string_to_136_tile(honors='1')
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- def test_open_hand_and_discard_tiles_logic(self):
- table = Table()
- player = table.player
-
- tiles = self._string_to_136_array(sou='112235589', man='24', honors='22')
- player.init_hand(tiles)
-
- # we don't need to call meld even if it improves our hand,
- # because we are collecting honitsu
- tile = self._string_to_136_tile(man='1')
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- # any honor tile is suitable
- tile = self._string_to_136_tile(honors='2')
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertNotEqual(meld, None)
-
- tile = self._string_to_136_tile(man='1')
- player.draw_tile(tile)
- tile_to_discard = player.discard_tile()
-
- # we are in honitsu mode, so we should discard man suits
- self.assertEqual(self._to_string([tile_to_discard]), '1m')
-
- def test_riichi_and_tiles_from_another_suit_in_the_hand(self):
- table = Table()
- player = table.player
- player.scores = 25000
- table.count_of_remaining_tiles = 100
-
- tiles = self._string_to_136_array(man='33345678', pin='22', honors='155')
- player.init_hand(tiles)
-
- player.draw_tile(self._string_to_136_tile(man='9'))
- tile_to_discard = player.discard_tile()
-
- # we don't need to go for honitsu here
- # we already in tempai
- self.assertEqual(self._to_string([tile_to_discard]), '1z')
-
- def test_discard_not_needed_winds(self):
- table = Table()
- player = table.player
- player.scores = 25000
- table.count_of_remaining_tiles = 100
-
- tiles = self._string_to_136_array(man='24', pin='4', sou='12344668', honors='36')
- player.init_hand(tiles)
- player.draw_tile(self._string_to_136_tile(sou='5'))
-
- table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
- table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
- table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)
-
- tile_to_discard = player.discard_tile()
-
- # west was discarded three times, we don't need it
- self.assertEqual(self._to_string([tile_to_discard]), '3z')
-
- def test_discard_not_effective_tiles_first(self):
- table = Table()
- player = table.player
- player.scores = 25000
- table.count_of_remaining_tiles = 100
-
- tiles = self._string_to_136_array(man='33', pin='12788999', sou='5', honors='23')
- player.init_hand(tiles)
- player.draw_tile(self._string_to_136_tile(honors='6'))
- tile_to_discard = player.discard_tile()
-
- self.assertEqual(self._to_string([tile_to_discard]), '5s')
-
- def test_dont_go_for_honitsu_with_ryanmen_in_other_suit(self):
- table = Table()
- player = table.player
- strategy = HonitsuStrategy(BaseStrategy.HONITSU, player)
-
- tiles = self._string_to_136_array(man='14489', sou='45', pin='67', honors='44456')
- player.init_hand(tiles)
-
- self.assertEqual(strategy.should_activate_strategy(), False)
-
-
-class TanyaoStrategyTestCase(unittest.TestCase, TestMixin):
-
- def _make_table(self):
- table = Table()
- table.has_open_tanyao = True
- return table
-
- def test_should_activate_strategy_and_terminal_pon_sets(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='233', honors='111')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='233999')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='233444')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- def test_should_activate_strategy_and_terminal_pairs(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='2399', honors='11')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='345669', pin='2399')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- def test_should_activate_strategy_and_valued_pair(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tiles = self._string_to_136_array(man='23446679', sou='345', honors='55')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(man='23446679', sou='345', honors='22')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- def test_should_activate_strategy_and_chitoitsu_like_hand(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tiles = self._string_to_136_array(sou='223388', man='3344', pin='6687')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- def test_should_activate_strategy_and_already_completed_sided_set(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tiles = self._string_to_136_array(sou='123234', man='3459', pin='234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234789', man='3459', pin='234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='1233459', pin='234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='3457899', pin='234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='122334')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='234', man='3459', pin='234789')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), False)
-
- tiles = self._string_to_136_array(sou='223344', man='3459', pin='234')
- player.init_hand(tiles)
- self.assertEqual(strategy.should_activate_strategy(), True)
-
- def test_suitable_tiles(self):
- table = self._make_table()
- player = table.player
- strategy = TanyaoStrategy(BaseStrategy.TANYAO, player)
-
- tile = self._string_to_136_tile(man='1')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(pin='1')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(sou='9')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(honors='1')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(honors='6')
- self.assertEqual(strategy.is_tile_suitable(tile), False)
-
- tile = self._string_to_136_tile(man='2')
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- tile = self._string_to_136_tile(pin='5')
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- tile = self._string_to_136_tile(sou='8')
- self.assertEqual(strategy.is_tile_suitable(tile), True)
-
- def test_dont_open_hand_with_high_shanten(self):
- table = self._make_table()
- player = table.player
-
- # with 4 shanten we don't need to aim for open tanyao
- tiles = self._string_to_136_array(man='369', pin='378', sou='3488', honors='123')
- tile = self._string_to_136_tile(sou='2')
- player.init_hand(tiles)
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- # with 3 shanten we can open a hand
- tiles = self._string_to_136_array(man='236', pin='378', sou='3488', honors='123')
- tile = self._string_to_136_tile(sou='2')
- player.init_hand(tiles)
- meld, _ = player.try_to_call_meld(tile, True)
- self.assertNotEqual(meld, None)
-
- def test_dont_open_hand_with_not_suitable_melds(self):
- table = self._make_table()
- player = table.player
-
- tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3')
- tile = self._string_to_136_tile(sou='8')
- player.init_hand(tiles)
- meld, _ = player.try_to_call_meld(tile, False)
- self.assertEqual(meld, None)
-
- def test_open_hand_and_discard_tiles_logic(self):
- table = self._make_table()
- player = table.player
-
- # 2345779m1p256s44z
- tiles = self._string_to_136_array(man='22345', sou='238', pin='256', honors='44')
- player.init_hand(tiles)
-
- # if we are in tanyao
- # we need to discard terminals and honors
- tile = self._string_to_136_tile(sou='4')
- meld, discard_option = player.try_to_call_meld(tile, True)
- discarded_tile = table.player.discard_tile(discard_option)
- self.assertNotEqual(meld, None)
- self.assertEqual(self._to_string([discarded_tile]), '4z')
-
- tile = self._string_to_136_tile(pin='5')
- player.draw_tile(tile)
- tile_to_discard = player.discard_tile()
-
- # we are in tanyao, so we should discard honors and terminals
- self.assertEqual(self._to_string([tile_to_discard]), '4z')
-
- def test_dont_count_pairs_in_already_opened_hand(self):
- table = self._make_table()
- player = table.player
-
- meld = self._make_meld(Meld.PON, sou='222')
- player.add_called_meld(meld)
-
- tiles = self._string_to_136_array(man='33556788', sou='22266')
- player.init_hand(tiles)
-
- tile = self._string_to_136_tile(sou='6')
- meld, _ = player.try_to_call_meld(tile, False)
- # even if it looks like chitoitsu we can open hand and get tempai here
- self.assertNotEqual(meld, None)
-
- def test_we_cant_win_with_this_hand(self):
- table = self._make_table()
-
- tiles = self._string_to_136_array(man='34577', sou='23', pin='233445')
- table.player.init_hand(tiles)
- meld = self._make_meld(Meld.CHI, pin='234')
- table.player.add_called_meld(meld)
-
- table.player.draw_tile(self._string_to_136_tile(sou='1'))
- discard = table.player.discard_tile()
- # but for already open hand we cant do tsumo
- # because we don't have a yaku here
- # so, let's do tsumogiri
- self.assertEqual(table.player.ai.previous_shanten, 0)
- self.assertEqual(self._to_string([discard]), '1s')
-
- def test_choose_correct_waiting(self):
- table = self._make_table()
- player = table.player
-
- tiles = self._string_to_136_array(man='234678', sou='234', pin='3588')
- player.init_hand(tiles)
- player.draw_tile(self._string_to_136_tile(pin='2'))
-
- # discard 5p and riichi
- discard = player.discard_tile()
- self.assertEqual(self._to_string([discard]), '5p')
-
- table = self._make_table()
- player = table.player
-
- meld = self._make_meld(Meld.CHI, man='234')
- player.add_called_meld(meld)
-
- tiles = self._string_to_136_array(man='234678', sou='234', pin='3588')
- player.init_hand(tiles)
- player.draw_tile(self._string_to_136_tile(pin='2'))
-
- # it is not a good idea to wait on 1-4, since we can't win on 1 with open hand
- # so let's continue to wait on 4 only
- discard = player.discard_tile()
- self.assertEqual(self._to_string([discard]), '2p')
-
- table = table = self._make_table()
- player = table.player
-
- meld = self._make_meld(Meld.CHI, man='234')
- player.add_called_meld(meld)
-
- tiles = self._string_to_136_array(man='234678', sou='234', pin='2388')
- player.init_hand(tiles)
- player.draw_tile(self._string_to_136_tile(sou='7'))
-
- # we can wait only on 1-4, so let's do it even if we can't get yaku on 1
- discard = player.discard_tile()
- self.assertEqual(self._to_string([discard]), '7s')
-
- def test_choose_correct_waiting_and_fist_opened_meld(self):
- table = self._make_table()
- player = table.player
-
- tiles = self._string_to_136_array(man='2337788', sou='345', pin='234')
- player.init_hand(tiles)
-
- tile = self._string_to_136_tile(man='8')
- meld, tile_to_discard = player.try_to_call_meld(tile, False)
-
- discard = player.discard_tile(tile_to_discard)
- self.assertEqual(self._to_string([discard]), '2m')
diff --git a/project/game/player.py b/project/game/player.py
index e020c8b8..41d47e59 100644
--- a/project/game/player.py
+++ b/project/game/player.py
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
import logging
import copy
+import utils.decisions_constants as log
from mahjong.constants import EAST, SOUTH, WEST, NORTH, CHUN, HAKU, HATSU
from mahjong.meld import Meld
from mahjong.tile import TilesConverter, Tile
+from utils.decisions_logger import DecisionsLogger
from utils.settings_handler import settings
logger = logging.getLogger('tenhou')
@@ -16,6 +18,7 @@ class PlayerInterface(object):
discards = None
melds = None
in_riichi = None
+ round_step = None
# current player seat
seat = 0
@@ -59,13 +62,19 @@ def erase_state(self):
self.position = 0
self.scores = None
self.uma = 0
+ self.round_step = 0
def add_called_meld(self, meld: Meld):
- # we already added chankan as a pon set
+ # we already added shouminkan as a pon set
if meld.type == Meld.CHANKAN:
tile_34 = meld.tiles[0] // 4
+
pon_set = [x for x in self.melds if x.type == Meld.PON and (x.tiles[0] // 4) == tile_34]
- self.melds.remove(pon_set[0])
+
+ # when we are doing reconnect and we have called shouminkan set
+ # we will not have called pon set in the hand
+ if pon_set:
+ self.melds.remove(pon_set[0])
self.melds.append(meld)
@@ -79,6 +88,9 @@ def add_discarded_tile(self, tile: Tile):
if player.in_riichi and tile not in player.safe_tiles:
player.safe_tiles.append(tile)
+ # one discard == one round step
+ self.round_step += 1
+
@property
def player_wind(self):
position = self.dealer_seat
@@ -111,6 +123,19 @@ def meld_tiles(self):
result.extend(meld.tiles)
return result
+ @property
+ def meld_34_tiles(self):
+ """
+ Array of array with 34 tiles indices
+ :return: array
+ """
+ melds = [x.tiles for x in self.melds]
+ melds = copy.deepcopy(melds)
+ results = []
+ for meld in melds:
+ results.append([meld[0] // 4, meld[1] // 4, meld[2] // 4])
+ return results
+
class Player(PlayerInterface):
ai = None
@@ -140,14 +165,24 @@ def init_hand(self, tiles):
self.ai.init_hand()
- def draw_tile(self, tile):
- self.last_draw = tile
- self.tiles.append(tile)
+ def draw_tile(self, tile_136):
+ DecisionsLogger.debug(
+ log.DRAW,
+ context=[
+ 'Step: {}'.format(self.round_step),
+ 'Hand: {}'.format(self.format_hand_for_print(tile_136)),
+ 'In defence: {}'.format(self.ai.in_defence),
+ 'Current strategy: {}'.format(self.ai.current_strategy)
+ ]
+ )
+
+ self.last_draw = tile_136
+ self.tiles.append(tile_136)
# we need sort it to have a better string presentation
self.tiles = sorted(self.tiles)
- self.ai.draw_tile(tile)
+ self.ai.draw_tile(tile_136)
def discard_tile(self, discard_tile=None):
"""
@@ -180,14 +215,8 @@ def formal_riichi_conditions(self):
self.table.count_of_remaining_tiles > 4
])
- def should_call_kan(self, tile, open_kan):
- """
- Method will decide should we call a kan,
- or upgrade pon to kan
- :param tile: 136 tile format
- :return:
- """
- return self.ai.should_call_kan(tile, open_kan)
+ def should_call_kan(self, tile, open_kan, from_riichi=False):
+ return self.ai.should_call_kan(tile, open_kan, from_riichi)
def should_call_win(self, tile, enemy_seat):
return self.ai.should_call_win(tile, enemy_seat)
@@ -205,25 +234,23 @@ def total_tiles(self, tile, tiles_34):
:param tiles_34: cached list of tiles (to not build it for each iteration)
:return: int
"""
- return tiles_34[tile] + self.table.revealed_tiles[tile]
+ revealed_tiles = tiles_34[tile] + self.table.revealed_tiles[tile]
+ assert revealed_tiles <= 4, 'we have only 4 tiles in the game'
+ return revealed_tiles
- def add_called_meld(self, meld: Meld):
- # we had to remove tile from the hand for closed kan set
- if (meld.type == Meld.KAN and not meld.opened) or meld.type == Meld.CHANKAN:
- self.tiles.remove(meld.called_tile)
+ def format_hand_for_print(self, tile_136=None):
+ hand_string = '{}'.format(TilesConverter.to_one_line_string(self.closed_hand))
- super().add_called_meld(meld)
+ if tile_136 is not None:
+ hand_string += ' + {}'.format(TilesConverter.to_one_line_string([tile_136]))
- def format_hand_for_print(self, tile):
- hand_string = '{} + {}'.format(
- TilesConverter.to_one_line_string(self.closed_hand),
- TilesConverter.to_one_line_string([tile])
- )
- if self.is_open_hand:
- melds = []
- for item in self.melds:
- melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles)))
+ melds = []
+ for item in self.melds:
+ melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles)))
+
+ if melds:
hand_string += ' [{}]'.format(', '.join(melds))
+
return hand_string
@property
@@ -231,22 +258,9 @@ def closed_hand(self):
tiles = self.tiles[:]
return [item for item in tiles if item not in self.meld_tiles]
- @property
- def open_hand_34_tiles(self):
- """
- Array of array with 34 tiles indices
- :return: array
- """
- melds = [x.tiles for x in self.melds if x.opened]
- melds = copy.deepcopy(melds)
- results = []
- for meld in melds:
- results.append([meld[0] // 4, meld[1] // 4, meld[2] // 4])
- return results
-
@property
def valued_honors(self):
- return [CHUN, HAKU, HATSU, self.table.round_wind, self.player_wind]
+ return [CHUN, HAKU, HATSU, self.table.round_wind_tile, self.player_wind]
class EnemyPlayer(PlayerInterface):
diff --git a/project/game/table.py b/project/game/table.py
index eb20ebcd..fdbf82cb 100644
--- a/project/game/table.py
+++ b/project/game/table.py
@@ -16,13 +16,16 @@ class Table(object):
dora_indicators = None
dealer_seat = 0
- round_number = 0
+ round_number = -1
+ round_wind_number = 0
count_of_riichi_sticks = 0
count_of_honba_sticks = 0
count_of_remaining_tiles = 0
count_of_players = 4
+ meld_was_called = False
+
# array of tiles in 34 format
revealed_tiles = None
@@ -36,14 +39,36 @@ def __init__(self):
def __str__(self):
dora_string = TilesConverter.to_one_line_string(self.dora_indicators)
- return 'Round: {0}, Honba: {1}, Dora Indicators: {2}'.format(self.round_number,
- self.count_of_honba_sticks,
- dora_string)
- def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks,
- dora_indicator, dealer_seat, scores):
+ round_settings = {
+ EAST: ['e', 0],
+ SOUTH: ['s', 3],
+ WEST: ['w', 7]
+ }.get(self.round_wind_tile)
+
+ round_string, round_diff = round_settings
+ display_round = '{}{}'.format(round_string, (self.round_wind_number + 1) - round_diff)
+
+ return 'Round: {}, Honba: {}, Dora Indicators: {}'.format(
+ display_round,
+ self.count_of_honba_sticks,
+ dora_string
+ )
+
+ def init_round(self,
+ round_wind_number,
+ count_of_honba_sticks,
+ count_of_riichi_sticks,
+ dora_indicator,
+ dealer_seat,
+ scores):
+
+ # we need it to properly display log for each round
+ self.round_number += 1
+
+ self.meld_was_called = False
self.dealer_seat = dealer_seat
- self.round_number = round_number
+ self.round_wind_number = round_wind_number
self.count_of_honba_sticks = count_of_honba_sticks
self.count_of_riichi_sticks = count_of_riichi_sticks
@@ -63,7 +88,7 @@ def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks
# 13 - tiles in each player hand
self.count_of_remaining_tiles = 136 - 14 - self.count_of_players * 13
- if round_number == 0 and count_of_honba_sticks == 0:
+ if round_wind_number == 0 and count_of_honba_sticks == 0:
i = 0
seats = [0, 1, 2, 3]
for player in self.players:
@@ -71,13 +96,19 @@ def init_round(self, round_number, count_of_honba_sticks, count_of_riichi_sticks
i += 1
def add_called_meld(self, player_seat, meld):
- # when opponent called meld it is means
- # that he discards tile from hand, not from wall
- self.count_of_remaining_tiles += 1
-
- # we will decrease count of remaining tiles after called kan
- # because we had to complement dead wall
- if meld.type == Meld.KAN or meld.type == meld.CHANKAN:
+ self.meld_was_called = True
+
+ # if meld was called from the other player, then we skip one draw from the wall
+ if meld.opened:
+ # but if it's an opened kan, player will get a tile from
+ # a dead wall, so total number of tiles in the wall is the same
+ # as if he just draws a tile
+ if meld.type != Meld.KAN:
+ self.count_of_remaining_tiles += 1
+ else:
+ # can't have a pon or chi from the hand
+ assert meld.type == Meld.KAN or meld.type == meld.CHANKAN
+ # player draws additional tile from the wall in case of closed kan or shouminkan
self.count_of_remaining_tiles -= 1
self.get_player(player_seat).add_called_meld(meld)
@@ -85,10 +116,10 @@ def add_called_meld(self, player_seat, meld):
tiles = meld.tiles[:]
# called tile was already added to revealed array
# because it was called on the discard
- if meld.called_tile:
+ if meld.called_tile is not None:
tiles.remove(meld.called_tile)
- # for chankan we already added 3 tiles
+ # for shouminkan we already added 3 tiles
if meld.type == meld.CHANKAN:
tiles = [meld.tiles[0]]
@@ -102,15 +133,15 @@ def add_called_riichi(self, player_seat):
if player_seat != 0:
self.player.enemy_called_riichi(player_seat)
- def add_discarded_tile(self, player_seat, tile, is_tsumogiri):
+ def add_discarded_tile(self, player_seat, tile_136, is_tsumogiri):
"""
:param player_seat:
- :param tile: 136 format tile
+ :param tile_136: 136 format tile
:param is_tsumogiri: was tile discarded from hand or not
"""
self.count_of_remaining_tiles -= 1
- tile = Tile(tile, is_tsumogiri)
+ tile = Tile(tile_136, is_tsumogiri)
self.get_player(player_seat).add_discarded_tile(tile)
# cache already revealed tiles
@@ -150,12 +181,12 @@ def get_players_sorted_by_scores(self):
return sorted(self.players, key=lambda x: (x.scores or 0, -x.first_seat), reverse=True)
@property
- def round_wind(self):
- if self.round_number < 4:
+ def round_wind_tile(self):
+ if self.round_wind_number < 4:
return EAST
- elif 4 <= self.round_number < 8:
+ elif 4 <= self.round_wind_number < 8:
return SOUTH
- elif 8 <= self.round_number < 12:
+ elif 8 <= self.round_wind_number < 12:
return WEST
else:
return NORTH
diff --git a/project/game/tests/tests_client.py b/project/game/tests/tests_client.py
index bc01eee6..faaacb67 100644
--- a/project/game/tests/tests_client.py
+++ b/project/game/tests/tests_client.py
@@ -25,7 +25,7 @@ def test_discard_tile(self):
self.assertFalse(tile in client.table.player.tiles)
self.assertEqual(client.table.count_of_remaining_tiles, 69)
- def test_call_meld(self):
+ def test_call_meld_closed_kan(self):
client = Client()
client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0])
@@ -40,12 +40,39 @@ def test_call_meld(self):
client.player.tiles = [0]
meld = Meld()
meld.type = Meld.KAN
+ # closed kan
+ meld.tiles = [0, 1, 2, 3]
+ meld.called_tile = None
+ meld.opened = False
+ client.table.add_called_meld(0, meld)
+
+ self.assertEqual(len(client.player.melds), 2)
+ # kan was closed, so -1
+ self.assertEqual(client.table.count_of_remaining_tiles, 70)
+
+ def test_call_meld_kan_from_player(self):
+ client = Client()
+
+ client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0])
+ self.assertEqual(client.table.count_of_remaining_tiles, 70)
+
+ meld = Meld()
+ client.table.add_called_meld(0, meld)
+
+ self.assertEqual(len(client.player.melds), 1)
+ self.assertEqual(client.table.count_of_remaining_tiles, 71)
+
+ client.player.tiles = [0]
+ meld = Meld()
+ meld.type = Meld.KAN
+ # closed kan
+ meld.tiles = [0, 1, 2, 3]
meld.called_tile = 0
+ meld.opened = True
client.table.add_called_meld(0, meld)
self.assertEqual(len(client.player.melds), 2)
- # +1 for called meld
- # -1 for called kan
+ # kan was called from another player, total number of remaining tiles stays the same
self.assertEqual(client.table.count_of_remaining_tiles, 71)
def test_enemy_discard(self):
diff --git a/project/game/tests/tests_player.py b/project/game/tests/tests_player.py
index 019414a3..56cad7b3 100644
--- a/project/game/tests/tests_player.py
+++ b/project/game/tests/tests_player.py
@@ -7,7 +7,6 @@
from game.player import Player
from game.table import Table
-from utils.settings_handler import settings
class PlayerTestCase(unittest.TestCase, TestMixin):
diff --git a/project/game/tests/tests_table.py b/project/game/tests/tests_table.py
index ee611fbd..70106e78 100644
--- a/project/game/tests/tests_table.py
+++ b/project/game/tests/tests_table.py
@@ -19,16 +19,23 @@ def test_init_hand(self):
def test_init_round(self):
table = Table()
- round_number = 4
+ round_wind_number = 4
count_of_honba_sticks = 2
count_of_riichi_sticks = 3
dora_indicator = 126
dealer = 3
scores = [250, 250, 250, 250]
- table.init_round(round_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer, scores)
+ table.init_round(
+ round_wind_number,
+ count_of_honba_sticks,
+ count_of_riichi_sticks,
+ dora_indicator,
+ dealer,
+ scores
+ )
- self.assertEqual(table.round_number, round_number)
+ self.assertEqual(table.round_wind_number, round_wind_number)
self.assertEqual(table.count_of_honba_sticks, count_of_honba_sticks)
self.assertEqual(table.count_of_riichi_sticks, count_of_riichi_sticks)
self.assertEqual(table.dora_indicators[0], dora_indicator)
@@ -38,7 +45,14 @@ def test_init_round(self):
dealer = 2
table.player.in_tempai = True
table.player.in_riichi = True
- table.init_round(round_number, count_of_honba_sticks, count_of_riichi_sticks, dora_indicator, dealer, scores)
+ table.init_round(
+ round_wind_number,
+ count_of_honba_sticks,
+ count_of_riichi_sticks,
+ dora_indicator,
+ dealer,
+ scores
+ )
# test that we reinit round properly
self.assertEqual(table.get_player(3).is_dealer, False)
@@ -176,16 +190,16 @@ def test_round_wind(self):
table = Table()
table.init_round(0, 0, 0, 0, 0, [])
- self.assertEqual(table.round_wind, EAST)
+ self.assertEqual(table.round_wind_tile, EAST)
table.init_round(3, 0, 0, 0, 0, [])
- self.assertEqual(table.round_wind, EAST)
+ self.assertEqual(table.round_wind_tile, EAST)
table.init_round(7, 0, 0, 0, 0, [])
- self.assertEqual(table.round_wind, SOUTH)
+ self.assertEqual(table.round_wind_tile, SOUTH)
table.init_round(11, 0, 0, 0, 0, [])
- self.assertEqual(table.round_wind, WEST)
+ self.assertEqual(table.round_wind_tile, WEST)
table.init_round(12, 0, 0, 0, 0, [])
- self.assertEqual(table.round_wind, NORTH)
+ self.assertEqual(table.round_wind_tile, NORTH)
diff --git a/project/reproducer.py b/project/reproducer.py
index 6492469b..e2f8c24f 100644
--- a/project/reproducer.py
+++ b/project/reproducer.py
@@ -41,8 +41,26 @@ def reproduce(self, dry_run=False):
table = Table()
for tag in self.round_content:
+ if player_draw_regex.match(tag) and 'UN' not in tag:
+ print('Player draw')
+ tile = self.decoder.parse_tile(tag)
+ table.player.draw_tile(tile)
+
if dry_run:
- print(tag)
+ if self._is_draw(tag):
+ print('<-', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag)
+ elif self._is_discard(tag):
+ print('->', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag)
+ elif self._is_init_tag(tag):
+ hands = {
+ 0: [int(x) for x in self._get_attribute_content(tag, 'hai0').split(',')],
+ 1: [int(x) for x in self._get_attribute_content(tag, 'hai1').split(',')],
+ 2: [int(x) for x in self._get_attribute_content(tag, 'hai2').split(',')],
+ 3: [int(x) for x in self._get_attribute_content(tag, 'hai3').split(',')],
+ }
+ print('Initial hand:', TilesConverter.to_one_line_string(hands[self.player_position]))
+ else:
+ print(tag)
if not dry_run and tag == self.stop_tag:
break
@@ -55,7 +73,7 @@ def reproduce(self, dry_run=False):
shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)])
table.init_round(
- values['round_number'],
+ values['round_wind_number'],
values['count_of_honba_sticks'],
values['count_of_riichi_sticks'],
values['dora_indicator'],
@@ -72,10 +90,6 @@ def reproduce(self, dry_run=False):
table.player.init_hand(hands[self.player_position])
- if player_draw_regex.match(tag) and 'UN' not in tag:
- tile = self.decoder.parse_tile(tag)
- table.player.draw_tile(tile)
-
if discard_regex.match(tag) and 'DORA' not in tag:
tile = self.decoder.parse_tile(tag)
player_sign = tag.upper()[1]
@@ -122,7 +136,7 @@ def _normalize_position(self, who, from_who):
def _parse_url(self, log_url):
temp = log_url.split('?')[1].split('&')
- log_id, player, round_number = '', 0, 0
+ log_id, player, round_wind = '', 0, 0
for item in temp:
item = item.split('=')
if 'log' == item[0]:
@@ -130,8 +144,8 @@ def _parse_url(self, log_url):
if 'tw' == item[0]:
player = int(item[1])
if 'ts' == item[0]:
- round_number = int(item[1])
- return log_id, player, round_number
+ round_wind = int(item[1])
+ return log_id, player, round_wind
def _download_log_content(self, log_id):
"""
@@ -198,6 +212,35 @@ def _parse_rounds(self, log_content):
return rounds[1:]
+ def _is_discard(self, tag):
+ skip_tags = [''.format(settings.LOBBY))
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS * 2)
+ self._random_sleep(1, 2)
self._send_message('')
else:
logger.info('Go to the lobby: {}'.format(settings.LOBBY))
self._send_message(''.format(quote('/lobby {}'.format(settings.LOBBY))))
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS * 2)
+ self._random_sleep(1, 2)
if self.reconnected_messages:
# we already in the game
self.looking_for_game = False
self._send_message('')
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
+ self._random_sleep(1, 2)
else:
selected_game_type = self._build_game_type()
game_type = '{},{}'.format(settings.LOBBY, selected_game_type)
@@ -144,16 +142,16 @@ def start_game(self):
start_time = datetime.datetime.now()
while self.looking_for_game:
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
+ self._random_sleep(1, 2)
messages = self._get_multiple_messages()
-
for message in messages:
if ''.format(game_type))
if '')
self._send_message('')
@@ -207,7 +205,8 @@ def start_game(self):
tile_to_discard = None
while self.game_is_continue:
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
+ self._random_sleep(1, 2)
+
messages = self._get_multiple_messages()
if self.reconnected_messages:
@@ -223,8 +222,9 @@ def start_game(self):
for message in messages:
if '')
continue
# Kyuushuu kyuuhai 「九種九牌」
# (9 kinds of honor or terminal tiles)
if 't="64"' in message:
+ self._random_sleep(1, 2)
# TODO aim for kokushi
self._send_message('')
continue
drawn_tile = self.decoder.parse_tile(message)
- if not main_player.in_riichi:
- logger.info('Hand: {}'.format(main_player.format_hand_for_print(drawn_tile)))
+ logger.info('Drawn tile: {}'.format(TilesConverter.to_one_line_string([drawn_tile])))
- self.player.draw_tile(drawn_tile)
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
-
- kan_type = self.player.should_call_kan(drawn_tile, False)
- if kan_type and self.table.count_of_remaining_tiles > 1:
- if kan_type == Meld.CHANKAN:
- meld_type = 5
- else:
- meld_type = 4
- self._send_message(''.format(meld_type, drawn_tile))
- logger.info('We called a closed kan\chankan set!')
- continue
+ kan_type = self.player.should_call_kan(drawn_tile, False, main_player.in_riichi)
+ if kan_type:
+ self._random_sleep(1, 2)
- discarded_tile = self.player.discard_tile()
- logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile])))
+ if kan_type == Meld.CHANKAN:
+ meld_type = 5
+ logger.info('We upgraded pon to kan!')
+ else:
+ meld_type = 4
+ logger.info('We called a closed kan set!')
+ self._send_message(''.format(meld_type, drawn_tile))
+
+ continue
+
+ if not main_player.in_riichi:
+ self.player.draw_tile(drawn_tile)
+ discarded_tile = self.player.discard_tile()
can_call_riichi = main_player.can_call_riichi()
# let's call riichi
if can_call_riichi:
+ self._random_sleep(1, 2)
self._send_message(''.format(discarded_tile))
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
main_player.in_riichi = True
else:
# we had to add it to discards, to calculate remaining tiles correctly
@@ -304,6 +307,7 @@ def start_game(self):
# tenhou format:
self._send_message(''.format(discarded_tile))
+ logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile])))
logger.info('Remaining tiles: {}'.format(self.table.count_of_remaining_tiles))
@@ -320,15 +324,11 @@ def start_game(self):
# the end of round
if '')
# set was called
- if ''.format(discarded_tile))
@@ -352,9 +347,16 @@ def start_game(self):
]
# we win by other player's discard
if any(i in message for i in win_suggestions):
- tile = self.decoder.parse_tile(message)
- enemy_seat = self.decoder.get_enemy_seat(message)
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
+ # enemy called shouminkan and we can win there
+ if self.decoder.is_opened_set_message(message):
+ meld = self.decoder.parse_meld(message)
+ tile = meld.called_tile
+ enemy_seat = meld.who
+ else:
+ tile = self.decoder.parse_tile(message)
+ enemy_seat = self.decoder.get_enemy_seat(message)
+
+ self._random_sleep(1, 2)
if main_player.should_call_win(tile, enemy_seat):
self._send_message('')
@@ -369,8 +371,6 @@ def start_game(self):
if_tsumogiri = message[1].islower()
player_seat = self.decoder.get_enemy_seat(message)
- self.table.add_discarded_tile(player_seat, tile, if_tsumogiri)
-
# open hand suggestions
if 't=' in message:
# Possible t="" suggestions
@@ -384,6 +384,8 @@ def start_game(self):
# should we call a kan?
if 't="3"' in message or 't="7"' in message:
if self.player.should_call_kan(tile, True):
+ self._random_sleep(1, 2)
+
# 2 is open kan
self._send_message('')
logger.info('We called an open kan set!')
@@ -396,6 +398,8 @@ def start_game(self):
meld, tile_to_discard = self.player.try_to_call_meld(tile, is_kamicha_discard)
if meld:
+ self._random_sleep(1, 2)
+
meld_tile = tile
# 1 is pon
@@ -416,9 +420,10 @@ def start_game(self):
))
# this meld will not improve our hand
else:
- sleep(TenhouClient.SLEEP_BETWEEN_ACTIONS)
self._send_message('')
+ self.table.add_discarded_tile(player_seat, tile, if_tsumogiri)
+
if 'owari' in message:
values = self.decoder.parse_final_scores_and_uma(message)
self.table.set_players_scores(values['scores'], values['uma'])
@@ -492,7 +497,7 @@ def send_request():
# we can't use sleep(15), because we want to be able
# end thread in the middle of running
seconds_to_sleep = 15
- for x in range(0, seconds_to_sleep * 2):
+ for _ in range(0, seconds_to_sleep * 2):
if self.game_is_continue:
sleep(0.5)
@@ -572,3 +577,6 @@ def _set_game_rules(self, game_type):
logger.info('Game type: {}'.format(is_hanchan and 'hanchan' or 'tonpusen'))
return True
+
+ def _random_sleep(self, min_sleep, max_sleep):
+ sleep(random.randint(min_sleep, max_sleep + 1))
diff --git a/project/tenhou/decoder.py b/project/tenhou/decoder.py
index f33a4f3e..156f3f04 100644
--- a/project/tenhou/decoder.py
+++ b/project/tenhou/decoder.py
@@ -61,7 +61,7 @@ def parse_initial_values(self, message):
seed = self.get_attribute_content(message, 'seed').split(',')
seed = [int(i) for i in seed]
- round_number = seed[0]
+ round_wind_number = seed[0]
count_of_honba_sticks = seed[1]
count_of_riichi_sticks = seed[2]
dora_indicator = seed[5]
@@ -71,7 +71,7 @@ def parse_initial_values(self, message):
scores = [int(i) for i in scores]
return {
- 'round_number': round_number,
+ 'round_wind_number': round_wind_number,
'count_of_honba_sticks': count_of_honba_sticks,
'count_of_riichi_sticks': count_of_riichi_sticks,
'dora_indicator': dora_indicator,
@@ -167,7 +167,9 @@ def parse_meld(self, message):
meld = Meld()
meld.who = int(self.get_attribute_content(message, 'who'))
- meld.from_who = data & 0x3
+ # 'from_who' is encoded relative the the 'who', so we want
+ # to convert it to be relative to our player
+ meld.from_who = ((data & 0x3) + meld.who) % 4
if data & 0x4:
self.parse_chi(data, meld)
@@ -260,6 +262,9 @@ def is_discarded_tile_message(self, message):
return False
+ def is_opened_set_message(self, message):
+ return '
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- Get:
- """
-
- self.client = TenhouClient(SocketMock(None, log))
- with self.assertRaises(KeyboardInterrupt) as context:
- self.client.connect()
- self.client.authenticate()
- self.client.start_game()
-
- # close all threads
- self.client.end_game()
-
- # end of commands is correct way to end log reproducing
- self.assertTrue('End of commands' in str(context.exception))
diff --git a/project/tenhou/tests/tests_decoder.py b/project/tenhou/tests/tests_decoder.py
index 839ec659..660750dc 100644
--- a/project/tenhou/tests/tests_decoder.py
+++ b/project/tenhou/tests/tests_decoder.py
@@ -12,7 +12,7 @@ def test_parse_initial_round_values(self):
'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>'
values = decoder.parse_initial_values(message)
- self.assertEqual(values['round_number'], 0)
+ self.assertEqual(values['round_wind_number'], 0)
self.assertEqual(values['count_of_honba_sticks'], 2)
self.assertEqual(values['count_of_riichi_sticks'], 3)
self.assertEqual(values['dora_indicator'], 126)
@@ -124,7 +124,7 @@ def test_parse_called_opened_kan(self):
meld = decoder.parse_meld('')
self.assertEqual(meld.who, 3)
- self.assertEqual(meld.from_who, 1)
+ self.assertEqual(meld.from_who, 0)
self.assertEqual(meld.type, Meld.KAN)
self.assertEqual(meld.opened, True)
self.assertEqual(meld.tiles, [52, 53, 54, 55])
@@ -174,7 +174,7 @@ def test_parse_who_called_riichi(self):
def test_reconnection_information(self):
message = '