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

Commit 9822075

Browse files
authored
Merge pull request #117 from MahjongRepository/dev
Various improvements
2 parents d9e8f86 + 847f6d3 commit 9822075

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+5371
-1612
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[flake8]
22
max-line-length = 120
3-
exclude = settings.py
3+
exclude = *settings.py,tests_validate_hand.py

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ project/game/data/*
2020
project/analytics/data/*
2121

2222
# temporary files
23-
experiments
23+
experiments
24+
25+
*.prof
26+
profile.py

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
language: python
2+
dist: xenial
3+
sudo: true
24
python:
35
- "3.5"
46
- "3.6"
7+
- "3.7"
58
before_script:
69
- cd project
710
install: "pip install -r project/requirements.txt"

CODE_OF_CONDUCT.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Tenhou bot code of conduct:
2+
3+
1. A robot may not injure a human being or, through inaction, allow a human being to come to harm.
4+
2. A robot must obey orders given it by human beings except where such orders would conflict with the First Law.
5+
3. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.

bin/run.sh

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
#!/bin/sh -e
22

3-
# cron will run each 5 minutes
4-
# and will search the run process
3+
# the cron job will be executed each 5 minutes
4+
# and it will try to find bot process
55
# if there is no process, it will run it
6+
# example of usage
7+
# */5 * * * * bash /root/bot/bin/run.sh bot_settings_name
68

7-
# */5 * * * * bash /root/bot/bin/run.sh
9+
SETTINGS_NAME="$1"
810

9-
PID=`ps -eaf | grep project/main.py | grep -v grep | awk '{print $2}'`
11+
PID=`ps -eaf | grep "project/main.py -s ${SETTINGS_NAME}" | grep -v grep | awk '{print $2}'`
1012

11-
if [[ "" = "$PID" ]]; then
12-
/root/bot/env/bin/python /root/bot/project/main.py
13+
if [[ "" = "$PID" ]]; then
14+
/root/bot/env/bin/python /root/bot/project/main.py -s ${SETTINGS_NAME}
1315
else
1416
WORKED_SECONDS=`ps -p "$PID" -o etimes=`
15-
# if process run > 60 minutes, probably it hang and we need to kill it
16-
if [ ${WORKED_SECONDS} -gt "3600" ]; then
17+
# if process run > 60 minutes, probably it hangs and we need to kill it
18+
if [[ ${WORKED_SECONDS} -gt "3600" ]]; then
1719
kill ${PID}
1820
fi
1921
fi

project/game/ai/base/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ def should_call_riichi(self):
5656
"""
5757
return False
5858

59-
def should_call_kan(self, tile, is_open_kan):
59+
def should_call_kan(self, tile, is_open_kan, from_riichi=False):
6060
"""
61-
When bot can call kan or chankan this method will be called
61+
When bot can call kan or shouminkan this method will be called
6262
:param tile: 136 tile format
6363
:param is_open_kan: boolean
64+
:param from_riichi: boolean
6465
:return: kan type (Meld.KAN, Meld.CHANKAN) or None
6566
"""
6667
return False

project/game/ai/discard.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
# -*- coding: utf-8 -*-
22
from mahjong.constants import AKA_DORA_LIST
33
from mahjong.tile import TilesConverter
4-
from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora
4+
from mahjong.utils import is_honor, simplify, plus_dora, is_aka_dora, is_sou, is_man, is_pin
5+
6+
from game.ai.first_version.strategies.main import BaseStrategy
57

68

79
class DiscardOption(object):
10+
DORA_VALUE = 10000
11+
DORA_FIRST_NEIGHBOUR = 1000
12+
DORA_SECOND_NEIGHBOUR = 100
13+
814
player = None
915

1016
# in 34 tile format
1117
tile_to_discard = None
1218
# array of tiles that will improve our hand
1319
waiting = None
1420
# how much tiles will improve our hand
15-
tiles_count = None
21+
ukeire = None
22+
ukeire_second = None
1623
# number of shanten for that tile
1724
shanten = None
1825
# sometimes we had to force tile to be discarded
@@ -23,25 +30,45 @@ class DiscardOption(object):
2330
valuation = None
2431
# how danger this tile is
2532
danger = None
33+
# wait to ukeire map
34+
wait_to_ukeire = None
35+
# second level cost approximation for 1-shanten hands
36+
second_level_cost = None
2637

27-
def __init__(self, player, tile_to_discard, shanten, waiting, tiles_count, danger=100):
38+
def __init__(self, player, tile_to_discard, shanten, waiting, ukeire, danger=100, wait_to_ukeire=None):
2839
"""
2940
:param player:
3041
:param tile_to_discard: tile in 34 format
3142
:param waiting: list of tiles in 34 format
32-
:param tiles_count: count of tiles to wait after discard
43+
:param ukeire: count of tiles to wait after discard
3344
"""
3445
self.player = player
3546
self.tile_to_discard = tile_to_discard
3647
self.shanten = shanten
3748
self.waiting = waiting
38-
self.tiles_count = tiles_count
49+
self.ukeire = ukeire
50+
self.ukeire_second = 0
51+
self.count_of_dora = 0
3952
self.danger = danger
4053
self.had_to_be_saved = False
4154
self.had_to_be_discarded = False
55+
self.wait_to_ukeire = wait_to_ukeire
4256

4357
self.calculate_value()
4458

59+
def __unicode__(self):
60+
tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4])
61+
return 'tile={}, shanten={}, ukeire={}, ukeire2={}, valuation={}'.format(
62+
tile_format_136,
63+
self.shanten,
64+
self.ukeire,
65+
self.ukeire_second,
66+
self.valuation
67+
)
68+
69+
def __repr__(self):
70+
return '{}'.format(self.__unicode__())
71+
4572
def find_tile_in_hand(self, closed_hand):
4673
"""
4774
Find and return 136 tile in closed player hand
@@ -69,31 +96,66 @@ def find_tile_in_hand(self, closed_hand):
6996

7097
return TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, closed_hand)
7198

72-
def calculate_value(self, shanten=None):
99+
def calculate_value(self):
73100
# base is 100 for ability to mark tiles as not needed (like set value to 50)
74101
value = 100
75102
honored_value = 20
76103

77-
# we don't need to keep honor tiles in almost completed hand
78-
if shanten and shanten <= 2:
79-
honored_value = 0
80-
81104
if is_honor(self.tile_to_discard):
82105
if self.tile_to_discard in self.player.valued_honors:
83106
count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard]
84107
# for west-west, east-east we had to double tile value
85108
value += honored_value * len(count_of_winds)
86109
else:
87-
# suits
88-
suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10]
110+
# aim for tanyao
111+
if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO:
112+
suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10]
113+
# usual hand
114+
else:
115+
suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10]
116+
89117
simplified_tile = simplify(self.tile_to_discard)
90118
value += suit_tile_grades[simplified_tile]
91119

120+
for indicator in self.player.table.dora_indicators:
121+
indicator_34 = indicator // 4
122+
if is_honor(indicator_34):
123+
continue
124+
125+
# indicator and tile not from the same suit
126+
if is_sou(indicator_34) and not is_sou(self.tile_to_discard):
127+
continue
128+
129+
# indicator and tile not from the same suit
130+
if is_man(indicator_34) and not is_man(self.tile_to_discard):
131+
continue
132+
133+
# indicator and tile not from the same suit
134+
if is_pin(indicator_34) and not is_pin(self.tile_to_discard):
135+
continue
136+
137+
simplified_indicator = simplify(indicator_34)
138+
simplified_dora = simplified_indicator + 1
139+
# indicator is 9 man
140+
if simplified_dora == 9:
141+
simplified_dora = 0
142+
143+
# tile so close to the dora
144+
if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora:
145+
value += DiscardOption.DORA_FIRST_NEIGHBOUR
146+
147+
# tile not far away from dora
148+
if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora:
149+
value += DiscardOption.DORA_SECOND_NEIGHBOUR
150+
92151
count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators)
93-
if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao):
152+
153+
tile_136 = self.find_tile_in_hand(self.player.closed_hand)
154+
if is_aka_dora(tile_136, self.player.table.has_aka_dora):
94155
count_of_dora += 1
95156

96-
value += 50 * count_of_dora
157+
self.count_of_dora = count_of_dora
158+
value += count_of_dora * DiscardOption.DORA_VALUE
97159

98160
if is_honor(self.tile_to_discard):
99161
# depends on how much honor tiles were discarded
@@ -108,4 +170,4 @@ def calculate_value(self, shanten=None):
108170
if value == 0:
109171
self.had_to_be_discarded = True
110172

111-
self.valuation = value
173+
self.valuation = int(value)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ def find_tiles_to_discard(self, _):
1414

1515
results = []
1616
for x in HONOR_INDICES:
17-
if self.player.total_tiles(x, self.defence.hand_34) == 4:
17+
if self.player.total_tiles(x, self.defence.closed_hand_34) == 4:
1818
results.append(DefenceTile(x, DefenceTile.SAFE))
1919

20-
if self.player.total_tiles(x, self.defence.hand_34) == 3:
20+
if self.player.total_tiles(x, self.defence.closed_hand_34) == 3:
2121
results.append(DefenceTile(x, DefenceTile.ALMOST_SAFE_TILE))
2222

2323
return results

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

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,96 @@
77

88
class Kabe(Defence):
99

10-
def find_tiles_to_discard(self, _):
11-
results = []
12-
10+
def find_all_kabe(self, tiles_34):
1311
# all indices shifted to -1
1412
kabe_matrix = [
15-
{'indices': [1], 'blocked_tiles': [0]},
16-
{'indices': [2], 'blocked_tiles': [0, 1]},
17-
{'indices': [3], 'blocked_tiles': [1, 2]},
18-
{'indices': [4], 'blocked_tiles': [2, 6]},
19-
{'indices': [5], 'blocked_tiles': [6, 7]},
20-
{'indices': [6], 'blocked_tiles': [7, 8]},
21-
{'indices': [7], 'blocked_tiles': [8]},
22-
{'indices': [1, 5], 'blocked_tiles': [3]},
23-
{'indices': [2, 6], 'blocked_tiles': [4]},
24-
{'indices': [3, 7], 'blocked_tiles': [5]},
25-
{'indices': [1, 4], 'blocked_tiles': [2, 3]},
26-
{'indices': [2, 5], 'blocked_tiles': [3, 4]},
27-
{'indices': [3, 6], 'blocked_tiles': [4, 5]},
28-
{'indices': [4, 7], 'blocked_tiles': [5, 6]},
13+
{'indices': [1], 'blocked_tiles': [0], 'type': KabeTile.STRONG_KABE},
14+
{'indices': [2], 'blocked_tiles': [0, 1], 'type': KabeTile.STRONG_KABE},
15+
{'indices': [6], 'blocked_tiles': [7, 8], 'type': KabeTile.STRONG_KABE},
16+
{'indices': [7], 'blocked_tiles': [8], 'type': KabeTile.STRONG_KABE},
17+
{'indices': [0, 3], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE},
18+
{'indices': [1, 3], 'blocked_tiles': [2], 'type': KabeTile.STRONG_KABE},
19+
{'indices': [1, 4], 'blocked_tiles': [2, 3], 'type': KabeTile.STRONG_KABE},
20+
{'indices': [2, 4], 'blocked_tiles': [3], 'type': KabeTile.STRONG_KABE},
21+
{'indices': [2, 5], 'blocked_tiles': [3, 4], 'type': KabeTile.STRONG_KABE},
22+
{'indices': [3, 5], 'blocked_tiles': [4], 'type': KabeTile.STRONG_KABE},
23+
{'indices': [3, 6], 'blocked_tiles': [4, 5], 'type': KabeTile.STRONG_KABE},
24+
{'indices': [4, 6], 'blocked_tiles': [5], 'type': KabeTile.STRONG_KABE},
25+
{'indices': [4, 7], 'blocked_tiles': [5, 6], 'type': KabeTile.STRONG_KABE},
26+
{'indices': [5, 7], 'blocked_tiles': [6], 'type': KabeTile.STRONG_KABE},
27+
{'indices': [5, 8], 'blocked_tiles': [6, 7], 'type': KabeTile.STRONG_KABE},
28+
29+
{'indices': [3], 'blocked_tiles': [1, 2], 'type': KabeTile.WEAK_KABE},
30+
{'indices': [4], 'blocked_tiles': [2, 6], 'type': KabeTile.WEAK_KABE},
31+
{'indices': [5], 'blocked_tiles': [6, 7], 'type': KabeTile.WEAK_KABE},
32+
{'indices': [1, 5], 'blocked_tiles': [3], 'type': KabeTile.WEAK_KABE},
33+
{'indices': [2, 6], 'blocked_tiles': [4], 'type': KabeTile.WEAK_KABE},
34+
{'indices': [3, 7], 'blocked_tiles': [5], 'type': KabeTile.WEAK_KABE},
2935
]
3036

31-
suits = self._suits_tiles(self.defence.hand_34)
37+
kabe_tiles_strong = []
38+
kabe_tiles_weak = []
39+
kabe_tiles_partial = []
40+
41+
suits = self._suits_tiles(tiles_34)
3242
for x in range(0, 3):
3343
suit = suits[x]
3444
# "kabe" - 4 revealed tiles
3545
kabe_tiles = []
46+
partial_kabe_tiles = []
3647
for y in range(0, 9):
3748
suit_tile = suit[y]
3849
if suit_tile == 4:
3950
kabe_tiles.append(y)
51+
elif suit_tile == 3:
52+
partial_kabe_tiles.append(y)
4053

41-
blocked_indices = []
4254
for matrix_item in kabe_matrix:
43-
all_indices = len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0
44-
if all_indices:
45-
blocked_indices.extend(matrix_item['blocked_tiles'])
55+
if len(list(set(matrix_item['indices']) - set(kabe_tiles))) == 0:
56+
for tile in matrix_item['blocked_tiles']:
57+
if matrix_item['type'] == KabeTile.STRONG_KABE:
58+
kabe_tiles_strong.append(tile + x * 9)
59+
else:
60+
kabe_tiles_weak.append(tile + x * 9)
61+
62+
if len(list(set(matrix_item['indices']) - set(partial_kabe_tiles))) == 0:
63+
for tile in matrix_item['blocked_tiles']:
64+
kabe_tiles_partial.append(tile + x * 9)
65+
66+
kabe_tiles_unique = []
67+
kabe_tiles_strong = list(set(kabe_tiles_strong))
68+
kabe_tiles_weak = list(set(kabe_tiles_weak))
69+
kabe_tiles_partial = list(set(kabe_tiles_partial))
70+
71+
for tile in kabe_tiles_strong:
72+
kabe_tiles_unique.append(KabeTile(tile, KabeTile.STRONG_KABE))
73+
74+
for tile in kabe_tiles_weak:
75+
if tile not in kabe_tiles_strong:
76+
kabe_tiles_unique.append(KabeTile(tile, KabeTile.WEAK_KABE))
77+
78+
for tile in kabe_tiles_partial:
79+
if tile not in kabe_tiles_strong and tile not in kabe_tiles_weak:
80+
kabe_tiles_unique.append(KabeTile(tile, KabeTile.PARTIAL_KABE))
81+
82+
return kabe_tiles_unique
4683

47-
blocked_indices = list(set(blocked_indices))
48-
for index in blocked_indices:
49-
# let's find 34 tile index
50-
tile = index + x * 9
51-
if self.player.total_tiles(tile, self.defence.hand_34) == 4:
52-
results.append(DefenceTile(tile, DefenceTile.SAFE))
84+
def find_tiles_to_discard(self, _):
85+
all_kabe = self.find_all_kabe(self.defence.closed_hand_34)
86+
87+
results = []
88+
89+
for kabe in all_kabe:
90+
# we don't use it for defence now
91+
if kabe.kabe_type == KabeTile.PARTIAL_KABE:
92+
continue
5393

54-
if self.player.total_tiles(tile, self.defence.hand_34) == 3:
55-
results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE))
94+
tile = kabe.tile_34
95+
if self.player.total_tiles(tile, self.defence.closed_hand_34) == 4:
96+
results.append(DefenceTile(tile, DefenceTile.SAFE))
97+
98+
if self.player.total_tiles(tile, self.defence.closed_hand_34) == 3:
99+
results.append(DefenceTile(tile, DefenceTile.ALMOST_SAFE_TILE))
56100

57101
return results
58102

@@ -88,3 +132,16 @@ def _suits_tiles(self, tiles_34):
88132
suits[suit_index][simplified_tile] += total_tiles
89133

90134
return suits
135+
136+
137+
class KabeTile(object):
138+
STRONG_KABE = 0
139+
WEAK_KABE = 1
140+
PARTIAL_KABE = 2
141+
142+
tile_34 = None
143+
kabe_type = None
144+
145+
def __init__(self, tile_34, kabe_type):
146+
self.tile_34 = tile_34
147+
self.kabe_type = kabe_type

0 commit comments

Comments
 (0)