Files
Mortal-Rating/tests/test_tenhou.py
2026-07-13 17:03:41 +08:00

180 lines
5.6 KiB
Python

import pytest
from batchmortal.tenhou import (
build_tenhou_paipu_urls,
decode_tenhou_viewpoint,
format_tenhou_timestamp,
normalize_tenhou_modes,
parse_tenhou_log_url,
tenhou_mode_matches,
tenhou_record_mode,
)
PLAYER = "ププリン"
def make_record(
log_id: str,
*,
starttime: int,
playernum: int = 4,
playlength: int = 2,
url_host: str = "tenhou.net",
) -> dict:
record = {
"starttime": starttime,
"during": 32,
"playernum": playernum,
"playlength": playlength,
"player1": PLAYER,
"player2": "Tatsuno7",
"player3": "Navitas",
"tw": "198",
"url": f"http://{url_host}/0/?log={log_id}",
}
if playernum == 4:
record["player4"] = "tca00"
return record
def test_decode_nodocchi_viewpoint_from_real_example():
# Nodocchi returns 198 for this game. ププリン is player1 (first place),
# and the site's two-bit permutation decoding resolves that to Tenhou seat 2.
assert decode_tenhou_viewpoint("198", 1) == 2
def test_decode_tenhou_viewpoint_rejects_non_numeric_tw():
assert decode_tenhou_viewpoint("abc", 1) is None
assert decode_tenhou_viewpoint(None, 1) is None
@pytest.mark.parametrize("player_order", [0, 5, None])
def test_decode_tenhou_viewpoint_rejects_invalid_player_order(player_order):
assert decode_tenhou_viewpoint("198", player_order) is None
def test_decode_tenhou_viewpoint_returns_none_for_unmapped_packed_value():
assert decode_tenhou_viewpoint("999", 1) is None
def test_build_real_example_paipu_url():
record = make_record(
"2026071212gm-00a9-0000-2dab9d24",
starttime=1783828560,
)
assert build_tenhou_paipu_urls([record], PLAYER) == [
{
"source": "tenhou",
"mode": "4p-south",
"uuid": "2026071212gm-00a9-0000-2dab9d24",
"paipuUrl": "http://tenhou.net/0/?log=2026071212gm-00a9-0000-2dab9d24&tw=2",
"startTime": format_tenhou_timestamp(1783828560),
"endTime": format_tenhou_timestamp(1783828560 + 32 * 60),
}
]
def test_build_tenhou_paipu_urls_deduplicates_log_ids():
log_id = "2026071212gm-00a9-0000-11111111"
records = [
make_record(log_id, starttime=200),
make_record(log_id, starttime=100),
]
items = build_tenhou_paipu_urls(records, PLAYER)
assert len(items) == 1
assert items[0]["uuid"] == log_id
assert items[0]["startTime"] == format_tenhou_timestamp(200)
def test_build_tenhou_paipu_urls_skips_invalid_viewpoints():
valid_log_id = "2026071212gm-00a9-0000-11111111"
invalid_tw = make_record(
"2026071212gm-00a9-0000-22222222",
starttime=200,
)
invalid_tw["tw"] = "999"
missing_player = make_record(
"2026071212gm-00a9-0000-33333333",
starttime=100,
)
missing_player["player1"] = "another-player"
items = build_tenhou_paipu_urls(
[make_record(valid_log_id, starttime=300), invalid_tw, missing_player],
PLAYER,
)
assert [item["uuid"] for item in items] == [valid_log_id]
def test_mode_filter_and_limit_are_applied_per_actual_mode():
records = [
make_record("2026071213gm-00a9-0000-aaaaaaaa", starttime=300, playlength=2),
make_record("2026071212gm-00a9-0000-bbbbbbbb", starttime=200, playlength=2),
make_record("2026071211gm-00a9-0000-cccccccc", starttime=100, playlength=1),
]
items = build_tenhou_paipu_urls(records, PLAYER, modes="4p", limit=1)
assert [item["uuid"] for item in items] == [
"2026071213gm-00a9-0000-aaaaaaaa",
"2026071211gm-00a9-0000-cccccccc",
]
assert [item["mode"] for item in items] == ["4p-south", "4p-east"]
def test_non_tenhou_urls_are_not_mistaken_for_tenhou_records():
record = make_record(
"2026071212gm-00a9-0000-2dab9d24",
starttime=1783828560,
url_host="game.maj-soul.com",
)
assert build_tenhou_paipu_urls([record], PLAYER) == []
assert parse_tenhou_log_url(record["url"]) is None
def test_records_without_url_or_viewpoint_are_skipped():
no_url = make_record("2026071212gm-00a9-0000-2dab9d24", starttime=200)
no_url.pop("url")
no_viewpoint = make_record("2026071213gm-00a9-0000-aaaaaaaa", starttime=300)
no_viewpoint.pop("tw")
assert build_tenhou_paipu_urls([no_url, no_viewpoint], PLAYER) == []
def test_tenhou_mode_aliases_and_labels():
assert normalize_tenhou_modes("四南,3p-east") == ("4p-south", "3p-east")
assert normalize_tenhou_modes("*") == ("all",)
assert tenhou_record_mode({"playernum": 3, "playlength": 1}) == "3p-east"
def test_normalize_tenhou_modes_rejects_unsupported_label():
with pytest.raises(ValueError, match="Unsupported Tenhou mode 'foo'"):
normalize_tenhou_modes(("foo",))
@pytest.mark.parametrize("modes", ["", (), ("",), (" ",), (" ", "", " ")])
def test_normalize_tenhou_modes_empty_values_fall_back_to_all(modes):
assert normalize_tenhou_modes(modes) == ("all",)
def test_tenhou_mode_matches_normalized_modes():
four_player = normalize_tenhou_modes(("4p",))
assert tenhou_mode_matches("4p-east", four_player)
assert tenhou_mode_matches("4p-south", four_player)
assert not tenhou_mode_matches("3p-east", four_player)
assert not tenhou_mode_matches("3p-south", four_player)
east_only = normalize_tenhou_modes(("4p-east",))
assert tenhou_mode_matches("4p-east", east_only)
assert not tenhou_mode_matches("4p-south", east_only)
south_only = normalize_tenhou_modes(("4p-south",))
assert tenhou_mode_matches("4p-south", south_only)
assert not tenhou_mode_matches("4p-east", south_only)