Skip to content

Commit

Permalink
Merge pull request #9 from tomii9273/add_nukeari_4and1
Browse files Browse the repository at this point in the history
抜け番あり 4 + 1 戦 コード追加
  • Loading branch information
tomii9273 authored Apr 12, 2024
2 parents 7ba99ab + 4c02cba commit 5819b48
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 145 deletions.
88 changes: 88 additions & 0 deletions src/4ma_nukeari_4and1_sen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import argparse
import time
import numpy as np
from utils import select_file, check_loaded_takugumi_teamsen, takugumi_to_array, array_to_txt, check, TAKUGUMI_DIR

GAME_N = 4 # 4 人麻雀


def make_takugumi(n_player: int) -> bool:
"""
n_player 人の抜け番あり 4 + 1 戦の卓組の作成を試みる。作成できたら True を返す。
参考: https://tomii6614.web.fc2.com/nukeari_4and1_sen_method.html
"""
assert n_player == 15 or n_player >= 19
n_taku = n_player // GAME_N
k = n_player % GAME_N
assert k != 0

if k in (1, 2):

takugumi = takugumi_to_array(
TAKUGUMI_DIR + "/" + select_file(f"{n_taku}taku_{GAME_N*n_taku}nin", "4team.txt"), GAME_N
)[:4, :]
check_loaded_takugumi_teamsen(takugumi, GAME_N, taku_check=True)

n_taisen = takugumi.shape[0]
n_player_4team = takugumi.shape[1]
assert n_player_4team == n_player - k

new_takugumi = np.full((n_taisen + 1, n_player), -1, dtype=int)
new_takugumi[:n_taisen, :n_player_4team] = takugumi

new_takugumi[:n_taisen, n_player_4team] = 1
for s in range(n_taisen):
for p in range(n_taku):
if new_takugumi[s, p] == 1:
new_takugumi[s, p] = -1
new_takugumi[-1, p] = 1

if k == 2:
new_takugumi[:n_taisen, n_player_4team + 1] = 2
for s in range(n_taisen):
for p in range(n_taku, n_taku * 2):
if new_takugumi[s, p] == 2:
new_takugumi[s, p] = -1
new_takugumi[-1, p] = 2
else: # k == 3
takugumi = takugumi_to_array(
TAKUGUMI_DIR + "/" + select_file(f"{n_taku+1}taku_{GAME_N*(n_taku+1)}nin", "4team.txt"), GAME_N
)[:4, :]
check_loaded_takugumi_teamsen(takugumi, GAME_N, taku_check=False)

n_taisen = takugumi.shape[0]
n_player_4team = takugumi.shape[1]
assert n_player_4team == n_player + 1

new_takugumi = np.full((n_taisen + 1, n_player), -1, dtype=int)
new_takugumi[:n_taisen, :n_player] = takugumi[:, :n_player]

for s in range(n_taisen):
for p in range(n_player):
if new_takugumi[s, p] == takugumi[s, -1]:
new_takugumi[s, p] = -1
new_takugumi[-1, p] = p // (n_taku + 1) + 1

# 卓番号に抜けがあるので、連続にする
for s in range(n_taisen + 1):
taku_num_now = sorted(list(set(new_takugumi[s]) - {-1}))
taku_num_map = {num: i + 1 for i, num in enumerate(taku_num_now)} | {-1: -1}
for j in range(n_player):
new_takugumi[s, j] = taku_num_map[new_takugumi[s, j]]

assert check(new_takugumi)
array_to_txt(new_takugumi, f"{n_taku}taku_{n_player}nin_4and1sen.txt")
return True


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--n_player", type=int, required=True, help="人数")
args = parser.parse_args()

start = time.time()

print("found" if make_takugumi(n_player=args.n_player) else "notfound")

elapsed_time = time.time() - start
print(f"time: {elapsed_time:.2f} sec")
149 changes: 5 additions & 144 deletions src/4ma_nukeari_6and1_sen.py
Original file line number Diff line number Diff line change
@@ -1,143 +1,9 @@
import argparse
import os
import time

import numpy as np
from utils import select_file, check_loaded_takugumi_teamsen, takugumi_to_array, array_to_txt, check, TAKUGUMI_DIR

GAME_N = 4 # 4 人麻雀
TAKUGUMI_DIR = "../takugumi_txt" # 元にする卓組ファイルがあるディレクトリ


def select_file(prefix: str, suffix: str) -> str:
"""prefix で始まり suffix で終わる卓組ファイル名を TAKUGUMI_DIR から探し、一意に定まる場合は返す。"""
files = os.listdir(TAKUGUMI_DIR)
ans = []
for file in files:
if file.startswith(prefix) and file.endswith(suffix):
ans.append(file)
assert len(ans) == 1, (prefix, suffix)
return ans[0]


def check_loaded_takugumi(takugumi: np.ndarray) -> None:
"""
既存の卓組が正しいかどうかを確認する。
takugumi[対戦番号, 選手番号] = 卓番号 の形式であり、卓番号は 1-indexed である
(ただし抜け番には -1 が入っている) ことを求める。
"""
n_taisen = takugumi.shape[0]
n_player = takugumi.shape[1]

assert n_player % GAME_N == 0 or takugumi.min() == -1

for s in range(n_taisen):
taku_cnt = [0] * (takugumi[s].max() + 1)
for p in range(n_player):
assert takugumi[s][p] == -1 or 1 <= takugumi[s][p] <= n_player // GAME_N
if takugumi[s][p] != -1:
taku_cnt[takugumi[s][p]] += 1
assert taku_cnt == [0] + [GAME_N] * takugumi[s].max()

assert -1 <= takugumi.min() and takugumi.max() <= n_player // GAME_N
assert 0 not in takugumi


def takugumi_to_array(path: str) -> np.ndarray:
"""既存の卓組を読み込み ndarray に変換。軸は (対戦, 選手) にする。"""
with open(path, "r") as f:
ar = []
for item in f.readlines():
ar.append(list(map(int, item.split())))
ar = np.array(ar).T
check_loaded_takugumi(ar)
return ar


def check_loaded_takugumi_teamsen(
takugumi: np.ndarray, taku_check: bool = False
) -> None:
"""
既存の卓組が GAME_N チーム戦として正しいかどうかを確認する。
takugumi[対戦番号, 選手番号] = 卓番号 の形式であり、
第 1 チームの選手 → 第 2 チームの選手 → ... 第 GAME_N チームの選手の順に選手番号が振られており、
同チームの選手との同卓はなく、他チームの選手との同卓は最大 1 回であることを求める。
taku_check ならば、同じ卓に同じ選手が複数回つかないことも求める。
"""
n_taisen = takugumi.shape[0]
n_player = takugumi.shape[1]
assert n_player % GAME_N == 0
n_taku = n_player // GAME_N

vs_cnt = np.zeros((n_player, n_player), dtype=int)
for s in range(n_taisen):
for p in range(n_player):
for q in range(p + 1, n_player):
if takugumi[s][p] == takugumi[s][q]:
vs_cnt[p][q] += 1
vs_cnt[q][p] += 1

for p in range(n_player):
for q in range(n_player):
if p // n_taku == q // n_taku:
assert vs_cnt[p][q] == 0
else:
assert vs_cnt[p][q] in (0, 1)

if taku_check:
for p in range(n_player):
assert len(set(takugumi[:, p])) == n_taisen, (p, takugumi[:, p])


def check(takugumi: np.ndarray) -> bool:
"""(作成した) 卓組が抜け番ありの個人戦として問題ないかを確認する。"""
n_taisen = takugumi.shape[0]
n_player = takugumi.shape[1]

# 卓番号とその個数が正常であることを確認
for i in range(n_taisen):
taku_cnt = [0] * (takugumi[i].max() + 1)
for p in range(n_player):
assert takugumi[i][p] > 0 or takugumi[i][p] == -1
if takugumi[i][p] != -1:
taku_cnt[takugumi[i][p]] += 1
assert taku_cnt == [0] + [4] * takugumi[i].max(), taku_cnt

# 重複対戦がなく、各選手の抜け番が 1 回であることを確認
vs_cnt = np.zeros((n_player, n_player), dtype=int)
nuke_cnt = np.zeros(n_player, dtype=int)
for s in range(n_taisen):
for p in range(n_player):
for q in range(p + 1, n_player):
if takugumi[s][p] == takugumi[s][q] and takugumi[s][p] != -1:
vs_cnt[p][q] += 1
vs_cnt[q][p] += 1
if takugumi[s][p] == -1:
nuke_cnt[p] += 1

for p in range(n_player):
for q in range(n_player):
if vs_cnt[p][q] not in (0, 1):
print("vs_count ng")
return False

if nuke_cnt.min() == nuke_cnt.max() == 1:
return True
print("nuke_cnt ng")
return False


def array_to_txt(takugumi: np.ndarray) -> None:
"""卓組の配列を txt ファイルに変換"""
n_taisen = takugumi.shape[0]
n_player = takugumi.shape[1]
n_taku = n_player // GAME_N
with open(f"{n_taku}taku_{n_player}nin_6and1sen.txt", "w") as f:
for i in range(n_player):
for j in range(n_taisen):
w = str(takugumi[j][i]) if takugumi[j][i] != -1 else "--"
f.write(w) # ここで転置する
f.write("\t")
f.write("\n")


def make_takugumi(n_taku: int) -> bool:
Expand All @@ -148,11 +14,9 @@ def make_takugumi(n_taku: int) -> bool:
assert n_taku >= 7

takugumi = takugumi_to_array(
TAKUGUMI_DIR
+ "/"
+ select_file(f"{n_taku}taku_{GAME_N*n_taku}nin", "4team.txt")
TAKUGUMI_DIR + "/" + select_file(f"{n_taku}taku_{GAME_N*n_taku}nin", "4team.txt"), GAME_N
)[:6, :]
check_loaded_takugumi_teamsen(takugumi, taku_check=True)
check_loaded_takugumi_teamsen(takugumi, GAME_N, taku_check=True)

n_taisen = takugumi.shape[0]
n_player = takugumi.shape[1]
Expand All @@ -177,10 +41,7 @@ def make_takugumi(n_taku: int) -> bool:
for s0 in range(1, 6):
del_taku_0 = takugumi[s0, p0]
del_taku_1 = takugumi[s0, p1]
if (
del_taku_0 in takugumi[1:, p1].flatten()
or del_taku_1 in takugumi[1:, p0].flatten()
): # 操作 4 の条件
if del_taku_0 in takugumi[1:, p1].flatten() or del_taku_1 in takugumi[1:, p0].flatten(): # 操作 4 の条件
continue

new_takugumi = np.full((n_taisen + 1, n_player + 2), -1, dtype=int)
Expand All @@ -202,7 +63,7 @@ def make_takugumi(n_taku: int) -> bool:
new_takugumi[6, -1] = 3

assert check(new_takugumi)
array_to_txt(new_takugumi)
array_to_txt(new_takugumi, f"{n_taku}taku_{n_player}nin_4and1sen.txt")
return True

return False
Expand Down
19 changes: 18 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,29 @@
- Python バージョン: 3.11.5
- ライブラリのバージョン: `../requirements.txt` 参照

## 4ma_nukeari_4and1_sen.py

`python 4ma_nukeari_4and1_sen.py -t [人数P]`

$P$ 人 $[P/4]$ 卓の抜け番あり 4 + 1 戦 ($P$ が 4 の倍数でない場合) の作成を試みます。
`../takugumi_txt` 内の既存の 4 チーム戦の卓組 (同じ卓を複数回使用する人がいないよう調整済) を使用します。

### 実行結果

| 卓数 $T$ | 作成可否 | 実行時間 [s] |
| ------------------------------ | -------- | -----------: |
| 15, 19 ~ 122 (4 の倍数を除く) || < 1 |

### 参考

[抜け番あり 1 人 4 戦 合計 5 戦 卓組作成方法](https://tomii6614.web.fc2.com/nukeari_4and1_sen_method.html)

## 4ma_nukeari_6and1_sen.py

`python 4ma_nukeari_6and1_sen.py -t [卓数T]`

$T$ 卓の抜け番あり 6 + 1 戦の作成を試みます。
`../takugumi_txt` 内の既存の 4 チーム戦の卓組を使用します
`../takugumi_txt` 内の既存の 4 チーム戦の卓組 (同じ卓を複数回使用する人がいないよう調整済) を使用します

### 実行結果

Expand Down
Loading

0 comments on commit 5819b48

Please sign in to comment.