Skip to content
This repository was archived by the owner on Jul 8, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 120
exclude = settings.py
exclude = *settings.py,tests_validate_hand.py
16 changes: 8 additions & 8 deletions project/game/ai/discard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class DiscardOption(object):
# array of tiles that will improve our hand
waiting = None
# how much tiles will improve our hand
tiles_count = None
tiles_count_second_level = None
ukeire = None
ukeire_second = None
# number of shanten for that tile
shanten = None
# sometimes we had to force tile to be discarded
Expand All @@ -27,19 +27,19 @@ class DiscardOption(object):
# how danger this tile is
danger = 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):
"""
: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.tiles_count_second_level = 0
self.ukeire = ukeire
self.ukeire_second = 0
self.count_of_dora = 0
self.danger = danger
self.had_to_be_saved = False
Expand All @@ -51,8 +51,8 @@ def __unicode__(self):
tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4])
return 'tile={}, ukeire={}, ukeire2={}, valuation={}'.format(
tile_format_136,
self.tiles_count,
self.tiles_count_second_level,
self.ukeire,
self.ukeire_second,
self.valuation
)

Expand Down
2 changes: 1 addition & 1 deletion project/game/ai/first_version/defence/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,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]

Expand Down
4 changes: 3 additions & 1 deletion project/game/ai/first_version/defence/suji.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,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
63 changes: 38 additions & 25 deletions project/game/ai/first_version/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.ai.first_version.strategies.formal_tempai import FormalTempaiStrategy

logger = logging.getLogger('ai')

Expand All @@ -34,6 +35,8 @@ class ImplementationAI(InterfaceAI):
last_discard_option = None

previous_shanten = 7
ukeire = 0
ukeire_second = 0
in_defence = False
waiting = None

Expand All @@ -48,6 +51,8 @@ def __init__(self, player):
self.hand_divider = HandDivider()
self.finished_hand = HandCalculator()
self.previous_shanten = 7
self.ukeire = 0
self.ukeire_second = 0
self.current_strategy = None
self.waiting = []
self.in_defence = False
Expand All @@ -61,8 +66,12 @@ def init_hand(self):

def erase_state(self):
self.current_strategy = None
self.waiting = []
self.in_defence = False
self.last_discard_option = None
self.previous_shanten = 7
self.ukeire = 0
self.ukeire_second = 0

def draw_tile(self, tile):
"""
Expand Down Expand Up @@ -107,7 +116,7 @@ def process_discard_options_and_select_tile_to_discard(self, results, shanten, h
# 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.ukeire = self.count_tiles(result.waiting, tiles_34)
result.calculate_value(shanten)

# current strategy can affect on our discard options
Expand Down Expand Up @@ -159,7 +168,7 @@ def calculate_outs(self, tiles, closed_hand, open_sets_34=None):
shanten=shanten,
tile_to_discard=hand_tile,
waiting=waiting,
tiles_count=self.count_tiles(waiting, tiles_34)))
ukeire=self.count_tiles(waiting, tiles_34)))

if is_agari:
shanten = Shanten.AGARI_STATE
Expand Down Expand Up @@ -203,6 +212,8 @@ def determine_strategy(self):
if self.player.table.has_open_tanyao:
strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player))

strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player))

for strategy in strategies:
if strategy.should_activate_strategy():
self.current_strategy = strategy
Expand All @@ -227,12 +238,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:

had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded]
if had_to_be_discarded_tiles:
return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.tiles_count, x.valuation))[0]
return sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))[0]

# remove needed tiles from discard options
results = [x for x in results if not x.had_to_be_saved]

results = sorted(results, key=lambda x: (x.shanten, -x.tiles_count))
results = sorted(results, key=lambda x: (x.shanten, -x.ukeire))
first_option = results[0]
results_with_same_shanten = [x for x in results if x.shanten == first_option.shanten]

Expand All @@ -244,22 +255,22 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
continue

# we don't need to select tiles almost dead waits
if discard_option.tiles_count <= 2:
if discard_option.ukeire <= 2:
continue

tiles_count_borders = round((first_option.tiles_count / 100) * border_percentage)
ukeire_borders = round((first_option.ukeire / 100) * border_percentage)

if first_option.shanten == 0 and tiles_count_borders < 2:
tiles_count_borders = 2
if first_option.shanten == 0 and ukeire_borders < 2:
ukeire_borders = 2

if first_option.shanten == 1 and tiles_count_borders < 4:
tiles_count_borders = 4
if first_option.shanten == 1 and ukeire_borders < 4:
ukeire_borders = 4

if first_option.shanten >= 2 and tiles_count_borders < 8:
tiles_count_borders = 8
if first_option.shanten >= 2 and ukeire_borders < 8:
ukeire_borders = 8

# let's choose tiles that are close to the max ukeire tile
if discard_option.tiles_count >= first_option.tiles_count - tiles_count_borders:
if discard_option.ukeire >= first_option.ukeire - ukeire_borders:
possible_options.append(discard_option)

if first_option.shanten <= 1:
Expand All @@ -269,14 +280,14 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
# as second step
# let's choose tiles that are close to the max ukeire2 tile
for x in possible_options:
self.calculate_second_level_tiles_count(x)
self.calculate_second_level_ukeire(x)

possible_options = sorted(possible_options, key=lambda x: -x.tiles_count_second_level)
possible_options = sorted(possible_options, key=lambda x: -x.ukeire_second)

filter_percentage = 20
filtered_options = self._filter_list_by_percentage(
possible_options,
'tiles_count_second_level',
'ukeire_second',
filter_percentage
)

Expand All @@ -287,12 +298,12 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
min_dora_list = [x for x in filtered_options if x.count_of_dora == min_dora]

# let's discard tile with greater ukeire2
return sorted(min_dora_list, key=lambda x: -x.tiles_count_second_level)[0]
return sorted(min_dora_list, key=lambda x: -x.ukeire_second)[0]

second_filter_percentage = 10
filtered_options = self._filter_list_by_percentage(
filtered_options,
'tiles_count_second_level',
'ukeire_second',
second_filter_percentage
)

Expand All @@ -305,18 +316,20 @@ def choose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:

# there are no isolated tiles
# let's discard tile with greater ukeire2
return sorted(filtered_options, key=lambda x: -x.tiles_count_second_level)[0]
return sorted(filtered_options, key=lambda x: -x.ukeire_second)[0]

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
self.player.ai.ukeire = discard_option.ukeire
self.player.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
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:
Expand Down Expand Up @@ -466,7 +479,7 @@ def enemy_called_riichi(self, enemy_seat):
if self.defence.should_go_to_defence_mode():
self.in_defence = True

def calculate_second_level_tiles_count(self, discard_option):
def calculate_second_level_ukeire(self, discard_option):
tiles = copy.copy(self.player.tiles)
tiles.remove(discard_option.find_tile_in_hand(self.player.closed_hand))

Expand All @@ -481,11 +494,11 @@ def calculate_second_level_tiles_count(self, discard_option):
self.player.open_hand_34_tiles
)
results = [x for x in results if x.shanten == discard_option.shanten - 1]
sum_tiles += sum([x.tiles_count for x in results])
sum_tiles += sum([x.ukeire for x in results])

tiles.remove(wait)

discard_option.tiles_count_second_level = sum_tiles
discard_option.ukeire_second = sum_tiles

@property
def enemy_players(self):
Expand All @@ -497,8 +510,8 @@ def enemy_players(self):
def _filter_list_by_percentage(self, items, attribute, percentage):
filtered_options = []
first_option = items[0]
tiles_count_borders = round((getattr(first_option, attribute) / 100) * percentage)
ukeire_borders = round((getattr(first_option, attribute) / 100) * percentage)
for x in items:
if getattr(x, attribute) >= getattr(first_option, attribute) - tiles_count_borders:
if getattr(x, attribute) >= getattr(first_option, attribute) - ukeire_borders:
filtered_options.append(x)
return filtered_options
80 changes: 80 additions & 0 deletions project/game/ai/first_version/strategies/formal_tempai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from mahjong.utils import plus_dora, is_aka_dora

from game.ai.first_version.strategies.main import BaseStrategy


class FormalTempaiStrategy(BaseStrategy):

def should_activate_strategy(self):
"""
When we get closer to the end of the round, we start to consider
going for formal tempai.
:return: boolean
"""

result = super(FormalTempaiStrategy, self).should_activate_strategy()
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.previous_shanten >= 3:
return True

dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles])
dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)])

if self.player.ai.previous_shanten == 2:
if dora_count < 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.previous_shanten == 1:
if dora_count == 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 dora_count == 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
2 changes: 2 additions & 0 deletions project/game/ai/first_version/strategies/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ class BaseStrategy(object):
YAKUHAI = 0
HONITSU = 1
TANYAO = 2
FORMAL_TEMPAI = 3

TYPES = {
YAKUHAI: 'Yakuhai',
HONITSU: 'Honitsu',
TANYAO: 'Tanyao',
FORMAL_TEMPAI: 'Formal Tempai'
}

player = None
Expand Down
Loading