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
5 changes: 5 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Tenhou bot code of conduct:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это я ребейзился на dev просто, по идее ничего страшного)


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.
2 changes: 2 additions & 0 deletions project/game/ai/discard.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class DiscardOption(object):
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, ukeire, danger=100, wait_to_ukeire=None):
"""
Expand Down
3 changes: 3 additions & 0 deletions project/game/ai/first_version/defence/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def should_go_to_defence_mode(self, discard_candidate=None):
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
Expand Down
1 change: 0 additions & 1 deletion project/game/ai/first_version/defence/suji.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from mahjong.utils import is_man, simplify, is_pin, is_sou, plus_dora, is_aka_dora
from mahjong.tile import TilesConverter

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

Expand Down
202 changes: 128 additions & 74 deletions project/game/ai/first_version/hand_builder.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import copy

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, is_chi
from mahjong.meld import Meld
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 utils.decisions_logger import DecisionsLogger

from game.ai.first_version.defence.kabe import KabeTile
from utils.decisions_logger import DecisionsLogger


class HandBuilder:
Expand Down Expand Up @@ -202,11 +198,7 @@ def find_discard_options(self, tiles, closed_hand, melds=None):

def count_tiles(self, waiting, tiles_34):
n = 0
not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or []
for tile_34 in waiting:
if self.player.is_open_hand and tile_34 in not_suitable_tiles:
continue

n += 4 - self.player.total_tiles(tile_34, tiles_34)
return n

Expand Down Expand Up @@ -304,15 +296,19 @@ def _choose_best_tanki_wait(self, discard_desc):
# if everything is the same we just choose the first one
return best_discard_desc[0]['discard_option']

def _is_furiten(self, tile_34):
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 _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
# only 1 option, nothing to choose
if len(discard_options) == 1:
return discard_options[0]
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

Expand All @@ -333,31 +329,12 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
discarded_tile = Tile(tile, False)
self.player.discards.append(discarded_tile)

hand_cost = 0
is_furiten = self._is_discard_option_furiten(discard_option)

if len(discard_option.waiting) == 1:
waiting = discard_option.waiting[0]
is_furiten = self._is_furiten(waiting)

hand_cost_tsumo = 0
cost_x_ukeire_tsumo = 0
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.ukeire

hand_cost_ron = 0
cost_x_ukeire_ron = 0
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.ukeire

# 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
hand_cost = hand_cost_tsumo + 3 * hand_cost_ron
cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
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)
Expand Down Expand Up @@ -416,30 +393,7 @@ def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
'tanki_type': tanki_type
})
else:
cost_x_ukeire_tsumo = 0
cost_x_ukeire_ron = 0
is_furiten = False

for waiting in discard_option.waiting:
is_furiten = is_furiten or self._is_furiten(waiting)

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:
cost_x_ukeire_tsumo += (hand_value.cost['main']
+ 2 * hand_value.cost['additional']
) * 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:
cost_x_ukeire_ron += hand_value.cost['main'] * discard_option.wait_to_ukeire[waiting]

cost_x_ukeire = cost_x_ukeire_tsumo + 3 * cost_x_ukeire_ron
cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi)

discard_desc.append({
'discard_option': discard_option,
Expand Down Expand Up @@ -544,6 +498,10 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
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]
Expand All @@ -564,8 +522,16 @@ def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):

return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0]

# we filter 10% of options here
# 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,
Expand Down Expand Up @@ -635,38 +601,89 @@ def process_discard_option(self, discard_option, closed_hand, force_discard=Fals
return discard_option.find_tile_in_hand(closed_hand)

def calculate_second_level_ukeire(self, discard_option):
closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or []
call_riichi = not self.player.is_open_hand

tiles = copy.copy(self.player.tiles)
tiles.remove(discard_option.find_tile_in_hand(self.player.closed_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.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
tiles.append(wait_136)
self.player.tiles.append(wait_136)

results, shanten = self.find_discard_options(
tiles,
self.player.tiles,
self.player.closed_hand,
self.player.melds
)
results = [x for x in results if x.shanten == discard_option.shanten - 1]

# let's take best ukeire here
if results:
best_one = sorted(results, key=lambda x: -x.ukeire)[0]
live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34)
sum_tiles += best_one.ukeire * live_tiles

tiles.remove(wait_136)
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

def _filter_list_by_percentage(self, items, attribute, percentage):
# 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)
Expand All @@ -675,7 +692,8 @@ def _filter_list_by_percentage(self, items, attribute, percentage):
filtered_options.append(x)
return filtered_options

def _choose_ukeire_borders(self, first_option, border_percentage, border_field):
@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:
Expand All @@ -688,3 +706,39 @@ def _choose_ukeire_borders(self, first_option, border_percentage, border_field):
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
10 changes: 3 additions & 7 deletions project/game/ai/first_version/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,9 @@ def init_hand(self):
'Hand: {}'.format(self.player.format_hand_for_print()),
])

# it will set correct hand shanten number and ukeire to the new hand
# tile will not be removed from the hand
self.discard_tile(None, print_log=False)
self.player.in_tempai = False

# Let's decide what we will do with our hand (like open for tanyao and etc.)
self.determine_strategy(self.player.tiles)
self.shanten = self.shanten_calculator.calculate_shanten(
TilesConverter.to_34_array(self.player.tiles)
)

def draw_tile(self, tile_136):
self.determine_strategy(self.player.tiles)
Expand Down
2 changes: 0 additions & 2 deletions project/game/ai/first_version/riichi.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from mahjong.tile import TilesConverter
from mahjong.utils import is_honor, simplify, is_pair, is_chi

from game.ai.first_version.defence.kabe import KabeTile


class Riichi:

Expand Down
1 change: 0 additions & 1 deletion project/game/ai/first_version/strategies/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discar
for meld_34 in possible_melds:
meld_34_copy = meld_34.copy()
closed_hand_copy = closed_hand.copy()
open_sets_34 = self.player.meld_34_tiles + [meld_34]

meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
meld_34_copy.remove(discarded_tile_34)
Expand Down
10 changes: 7 additions & 3 deletions project/game/ai/first_version/strategies/tanyao.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,13 @@ def determine_what_to_discard(self, discard_options, hand, open_melds):
continue

# there is no sense to wait 1-4 if we have open hand
all_waiting_are_fine = all([self.is_tile_suitable(x * 4) for x in item.waiting])
if all_waiting_are_fine:
results.append(item)
# 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)

if not_suitable_tiles:
return not_suitable_tiles
Expand Down
Loading