├── kanachan ├── training │ ├── cql │ │ ├── __init__.py │ │ └── Dockerfile │ ├── iql │ │ ├── __init__.py │ │ ├── Dockerfile │ │ ├── qq_model.py │ │ ├── value_model.py │ │ └── q_model.py │ ├── bert │ │ ├── phase3 │ │ │ ├── __init__.py │ │ │ ├── Dockerfile │ │ │ ├── iterator_adaptor.py │ │ │ └── train.py │ │ ├── __init__.py │ │ ├── phase1 │ │ │ ├── __init__.py │ │ │ ├── Dockerfile │ │ │ ├── iterator_adaptor.py │ │ │ ├── model.py │ │ │ ├── train.py │ │ │ ├── decoder.py │ │ │ └── config.py │ │ ├── phase2 │ │ │ ├── __init__.py │ │ │ ├── Dockerfile │ │ │ ├── iterator_adaptor.py │ │ │ └── train.py │ │ ├── README.md │ │ └── encoder.py │ ├── __init__.py │ ├── aiayn │ │ ├── __init__.py │ │ ├── model.py │ │ ├── encoder.py │ │ ├── decoder.py │ │ └── iterator_adaptor.py │ ├── ilql │ │ ├── __init__.py │ │ ├── Dockerfile │ │ ├── reward_function.py │ │ ├── q_model.py │ │ ├── policy_model.py │ │ └── qv_model.py │ ├── _core │ │ └── offline_rl │ │ │ ├── __init__.py │ │ │ └── reward_function.py │ ├── position_embedding.py │ ├── constants.py │ ├── README.md │ ├── positional_encoding.py │ ├── common.py │ └── iterator_adaptor_base.py ├── __init__.py ├── simulation │ ├── Dockerfile │ ├── __init__.py │ └── README.md ├── README.md ├── nn │ └── linear_decoder.py └── model_loader.py ├── prerequisites ├── .dockerignore ├── README.md ├── prologue.sh ├── Dockerfile ├── boost │ └── build ├── googletest │ └── install └── libbacktrace │ └── install ├── setup.py ├── pyproject.toml ├── src ├── paishan │ ├── CMakeLists.txt │ └── Dockerfile ├── CMakeLists.txt ├── annotation │ ├── CMakeLists.txt │ ├── Dockerfile │ ├── utility.cpp │ ├── round_progress.hpp │ ├── README.md │ ├── annotation.hpp │ └── player_state.hpp ├── common │ ├── CMakeLists.txt │ ├── thread.cpp │ ├── type_name.hpp │ ├── type_name.cpp │ ├── thread.hpp │ ├── assert.cpp │ └── throw.cpp ├── xiangting │ └── CMakeLists.txt └── simulation │ ├── sigang_sanle.cpp │ ├── sijia_lizhi.cpp │ ├── sifeng_lianda.cpp │ ├── jiuzhong_jiupai.cpp │ ├── utility.hpp │ ├── zimo.hpp │ ├── huangpai_pingju.cpp │ ├── sijia_lizhi.hpp │ ├── sigang_sanle.hpp │ ├── sifeng_lianda.hpp │ ├── daminggang.hpp │ ├── jiuzhong_jiupai.hpp │ ├── hule.hpp │ ├── chi.hpp │ ├── huangpai_pingju.hpp │ ├── peng.hpp │ ├── dapai.hpp │ ├── angang.hpp │ ├── jiagang.hpp │ ├── round.hpp │ ├── daminggang.cpp │ ├── hule.cpp │ ├── game.hpp │ ├── paishan.hpp │ ├── gil.cpp │ ├── CMakeLists.txt │ ├── gil.hpp │ ├── simulator.hpp │ ├── round.cpp │ ├── decision_maker.hpp │ ├── peng.cpp │ ├── round_result.hpp │ ├── xiangting_calculator.hpp │ ├── chi.cpp │ ├── simulate.hpp │ ├── jiagang.cpp │ ├── angang.cpp │ ├── round_result.cpp │ ├── game_log.hpp │ ├── game_state.hpp │ ├── utility.cpp │ ├── game.cpp │ ├── paishan.cpp │ ├── zimo.cpp │ ├── game_state.cpp │ ├── shoupai.hpp │ └── dapai.cpp ├── .gitignore ├── bin └── annotate4rl │ ├── Dockerfile │ └── README.md ├── test └── annotation_vs_simulation │ ├── CMakeLists.txt │ ├── skip-list.txt │ ├── generate.sh │ ├── Dockerfile │ └── run.py ├── mjai.app ├── bot.py ├── config.json.orig ├── xiangting_calculator │ └── __init__.py ├── test.sh ├── README.md ├── Dockerfile └── build.sh ├── .dockerignore ├── .vscode ├── c_cpp_properties.json └── settings.json ├── setup.cfg ├── .devcontainer └── devcontainer.json └── CMakeLists.txt /kanachan/training/cql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kanachan/training/iql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kanachan/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /kanachan/training/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /kanachan/training/aiayn/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /kanachan/training/bert/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /kanachan/training/ilql/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /prerequisites/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase2/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /src/paishan/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(paishan 2 | main.cpp) 3 | target_link_libraries(paishan 4 | PRIVATE common) 5 | -------------------------------------------------------------------------------- /kanachan/training/_core/offline_rl/__init__.py: -------------------------------------------------------------------------------- 1 | from .iterator_adaptor import IteratorAdaptor 2 | from .reward_function import RewardFunction 3 | -------------------------------------------------------------------------------- /kanachan/simulation/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["python3", "-m", "kanachan.simulation.run"] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/common/mahjongsoul.pb.h 2 | /src/common/mahjongsoul.pb.cc 3 | /build/ 4 | /kanachan/simulation/_simulation.so 5 | /kanachan.egg-info/ 6 | __pycache__ 7 | -------------------------------------------------------------------------------- /bin/annotate4rl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/kanachan 4 | 5 | ENTRYPOINT ["python3", "/workspace/kanachan/bin/annotate4rl/annotate4rl.py"] 6 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(xiangting) 2 | add_subdirectory(common) 3 | add_subdirectory(annotation) 4 | add_subdirectory(paishan) 5 | add_subdirectory(simulation) 6 | -------------------------------------------------------------------------------- /test/annotation_vs_simulation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(generate 2 | generate.cpp) 3 | target_link_libraries(generate 4 | PRIVATE common 5 | PRIVATE Boost::headers) 6 | -------------------------------------------------------------------------------- /kanachan/simulation/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from .tool import Tool 4 | from .test_model import TestModel 5 | from ._simulation import simulate, test # pylint: disable=import-error 6 | -------------------------------------------------------------------------------- /kanachan/training/cql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.cql.train"] 6 | -------------------------------------------------------------------------------- /kanachan/training/ilql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.ilql.train"] 6 | -------------------------------------------------------------------------------- /kanachan/training/iql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.iql.train"] 6 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.bert.phase1.train"] 6 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.bert.phase2.train"] 6 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan 2 | 3 | WORKDIR /workspace/data 4 | 5 | ENTRYPOINT ["torchrun", "--nproc_per_node", "gpu", "--standalone", "-m", "kanachan.training.bert.phase3.train"] 6 | -------------------------------------------------------------------------------- /prerequisites/README.md: -------------------------------------------------------------------------------- 1 | # Prerequisites (For Developers Only) 2 | 3 | ## How to Build 4 | 5 | ```bash 6 | kanachan$ cd prerequisites 7 | kanachan/prerequisites$ docker build --pull -t cryolite/kanachan.prerequisites . 8 | ``` 9 | -------------------------------------------------------------------------------- /mjai.app/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from _kanachan import Kanachan 4 | 5 | 6 | def main() -> None: 7 | kanachan = Kanachan() 8 | kanachan.run() 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /src/annotation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(annotate 2 | main.cpp 3 | annotation.cpp 4 | round_progress.cpp 5 | player_state.cpp 6 | utility.cpp) 7 | target_link_libraries(annotate 8 | PRIVATE common 9 | PRIVATE Boost::headers) 10 | -------------------------------------------------------------------------------- /mjai.app/config.json.orig: -------------------------------------------------------------------------------- 1 | { 2 | "model": "./model.kanachan", 3 | "device": "cpu", 4 | "dtype": "float32", 5 | "my_name": "kanachan", 6 | "room": 3, 7 | "game_style": 1, 8 | "my_grade": 14, 9 | "opponent_grade": 14 10 | } 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /src/common/mahjongsoul.pb.h 3 | /src/common/mahjongsoul.pb.cc 4 | /build 5 | /kanachan/simulation/_simulation.so 6 | /kanachan.egg-info 7 | /.dockerignore 8 | /prerequisites 9 | /annotate/Dockerfile 10 | /kanachan/Dockerfile 11 | /kanachan/**/__pycache__ 12 | /mjai.app 13 | -------------------------------------------------------------------------------- /kanachan/training/ilql/reward_function.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import (Optional, Callable,) 4 | import torch 5 | 6 | 7 | RewardFunction = Callable[ 8 | [torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, 9 | int, Optional[int], Optional[int]], 10 | float] 11 | -------------------------------------------------------------------------------- /src/common/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(common 2 | mahjongsoul.pb.cc 3 | thread.cpp 4 | assert.cpp 5 | throw.cpp 6 | type_name.cpp) 7 | target_link_libraries(common 8 | PRIVATE protobuf 9 | PRIVATE Boost::stacktrace_backtrace 10 | PRIVATE Boost::headers) 11 | set_property(TARGET common PROPERTY POSITION_INDEPENDENT_CODE ON) 12 | -------------------------------------------------------------------------------- /src/xiangting/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if (DEFINED SHANTEN_NUMBER_SOURCE_PATH) 2 | include_directories("${SHANTEN_NUMBER_SOURCE_PATH}") 3 | 4 | add_executable(make_trie 5 | "${SHANTEN_NUMBER_SOURCE_PATH}/calsht.cpp" 6 | make_trie.cpp) 7 | target_link_libraries(make_trie 8 | PRIVATE dl 9 | PRIVATE marisa 10 | PRIVATE common) 11 | endif() 12 | -------------------------------------------------------------------------------- /src/simulation/sigang_sanle.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/sigang_sanle.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | 6 | 7 | namespace Kanachan{ 8 | 9 | bool sigangSanle(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 10 | { 11 | round_state.onLiuju(game_log); 12 | return false; 13 | } 14 | 15 | } // namespace Kanachan 16 | -------------------------------------------------------------------------------- /src/simulation/sijia_lizhi.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/sijia_lizhi.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | 6 | 7 | namespace Kanachan{ 8 | 9 | bool sijiaLizhi(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 10 | { 11 | round_state.onLiuju(game_log); 12 | return false; 13 | } 14 | 15 | } // namespace Kanachan 16 | -------------------------------------------------------------------------------- /test/annotation_vs_simulation/skip-list.txt: -------------------------------------------------------------------------------- 1 | 201023-ac7ca789-c5a9-4ea6-a579-18e5616c8e4c 2 | 201130-1e76b0a5-4e0d-4bd6-ab25-af64e9be61fd 3 | 210218-e7991ab2-537b-42f8-a05e-1f915f8350fc 4 | 210304-7070f202-49c4-48c7-846f-d56f765453ba 5 | 210316-d59c6436-3a8a-4d47-ad1e-8b141925ad0c 6 | 210509-0213af76-f17e-455e-b774-d6617fc7cefd 7 | 210515-85bfdee9-c4cf-4746-b5dc-d68f3adc8239 8 | 210602-73356be8-89b4-44b2-a98d-ec50649f8d50 9 | -------------------------------------------------------------------------------- /kanachan/training/_core/offline_rl/reward_function.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Tuple, Union, List 2 | import torch 3 | 4 | 5 | RewardFunction = Callable[ 6 | [ 7 | Tuple[ 8 | torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, 9 | torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, 10 | int, int, bool]], 11 | Union[torch.Tensor, List[float]]] 12 | -------------------------------------------------------------------------------- /src/common/thread.cpp: -------------------------------------------------------------------------------- 1 | #include "common/thread.hpp" 2 | 3 | #include 4 | #include 5 | 6 | 7 | namespace Kanachan{ 8 | 9 | Kanachan::ThreadTermination::ThreadTermination(char const *message) 10 | : std::runtime_error(message) 11 | {} 12 | 13 | Kanachan::ThreadTermination::ThreadTermination(std::string const &message) 14 | : std::runtime_error(message) 15 | {} 16 | 17 | } // namespace Kanachan 18 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/iterator_adaptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | from kanachan.training.iterator_adaptor_base import IteratorAdaptorBase 5 | 6 | 7 | class IteratorAdaptor(IteratorAdaptorBase): 8 | def __init__(self, path: Path) -> None: 9 | super(IteratorAdaptor, self).__init__(path) 10 | 11 | def __next__(self): 12 | return super(IteratorAdaptor, self).__next__()[:-1] 13 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "defines": [], 6 | "compilerPath": "/home/vscode/.local/bin/g++", 7 | "cStandard": "gnu17", 8 | "cppStandard": "c++23", 9 | "intelliSenseMode": "linux-gcc-x64", 10 | "configurationProvider": "ms-vscode.cmake-tools" 11 | } 12 | ], 13 | "version": 4 14 | } -------------------------------------------------------------------------------- /src/simulation/sifeng_lianda.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/sifeng_lianda.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | bool sifengLianda(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 12 | { 13 | round_state.onLiuju(game_log); 14 | return false; 15 | } 16 | 17 | } // namespace Kanachan 18 | -------------------------------------------------------------------------------- /src/simulation/jiuzhong_jiupai.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/jiuzhong_jiupai.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | bool jiuzhongJiupai(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 12 | { 13 | round_state.onLiuju(game_log); 14 | return false; 15 | } 16 | 17 | } // namespace Kanachan 18 | -------------------------------------------------------------------------------- /src/simulation/utility.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_UTILITY_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_UTILITY_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | std::vector getRandomSeed(); 11 | 12 | [[noreturn]] void translatePythonException(); 13 | 14 | } // namespace Kanachan 15 | 16 | #endif // !defined(KANACHAN_SIMULATION_UTILITY_HPP_INCLUDE_GUARD) 17 | -------------------------------------------------------------------------------- /src/simulation/zimo.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_ZIMO_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_ZIMO_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | std::any zimo(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 12 | 13 | } // namespace Kanachan 14 | 15 | #endif // !defined(KANACHAN_SIMULATION_ZIMO_HPP_INCLUDE_GUARD) 16 | -------------------------------------------------------------------------------- /src/simulation/huangpai_pingju.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/huangpai_pingju.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any huangpaiPingju(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 13 | { 14 | return round_state.onHuangpaiPingju(game_log); 15 | } 16 | 17 | } // namespace Kanachan 18 | -------------------------------------------------------------------------------- /src/simulation/sijia_lizhi.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SIJIA_LIZHI_HPP_INCLUCE_GUARD) 2 | #define KANACHAN_SIMULATION_SIJIA_LIZHI_HPP_INCLUCE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | bool sijiaLizhi(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 11 | 12 | } // namespace Kanachan 13 | 14 | #endif // !defined(KANACHAN_SIMULATION_SIJIA_LIZHI_HPP_INCLUCE_GUARD) 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = kanachan 3 | version = 0.0.1 4 | description = Mahjong AI for Mahjong Soul (雀魂) 5 | author = Cryolite 6 | author_email = cryolite.indigo@gmail.com 7 | url = https://github.com/Cryolite/kanachan 8 | 9 | [options] 10 | include_package_data = True 11 | packages = find: 12 | install_requires = 13 | hydra-core 14 | jsonschema 15 | mahjong==1.1.11 16 | pyyaml 17 | tensorboard 18 | tqdm 19 | 20 | [options.package_data] 21 | * = *.so 22 | -------------------------------------------------------------------------------- /src/simulation/sigang_sanle.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SIGANG_SANLE_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_SIGANG_SANLE_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | bool sigangSanle(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 11 | 12 | } // namespace Kanachan 13 | 14 | #endif // !defined(KANACHAN_SIMULATION_SIGANG_SANLE_HPP_INCLUDE_GUARD) 15 | -------------------------------------------------------------------------------- /src/simulation/sifeng_lianda.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SIFENG_LIANDA_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_SIFENG_LIANDA_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | bool sifengLianda(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 11 | 12 | } // namespace Kanachan 13 | 14 | #endif // !defined(KANACHAN_SIMULATION_SIFENG_LIANDA_HPP_INCLUDE_GUARD) 15 | -------------------------------------------------------------------------------- /src/simulation/daminggang.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_DAMINGGANG_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_DAMINGGANG_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | std::any daminggang(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 12 | 13 | } // namespace Kanachan 14 | 15 | #endif // !defined(KANACHAN_SIMULATION_DAMINGGANG_HPP_INCLUDE_GUARD) 16 | -------------------------------------------------------------------------------- /src/simulation/jiuzhong_jiupai.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHNA_SIMULATION_JIUZHONG_JIUPAI_HPP_INCLUDE_GUARD) 2 | #define KANACHNA_SIMULATION_JIUZHONG_JIUPAI_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | bool jiuzhongJiupai(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 11 | 12 | } // namespace Kanachan 13 | 14 | #endif // !defined(KANACHNA_SIMULATION_JIUZHONG_JIUPAI_HPP_INCLUDE_GUARD) 15 | -------------------------------------------------------------------------------- /src/simulation/hule.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_HULE_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_HULE_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | bool hule( 12 | Kanachan::RoundState &round_state, std::uint_fast8_t zimo_tile, Kanachan::GameLog &game_log); 13 | 14 | } // namespace Kanachan 15 | 16 | #endif // !defined(KANACHAN_SIMULATION_HULE_HPP_INCLUDE_GUARD) 17 | -------------------------------------------------------------------------------- /src/common/type_name.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_COMMON_TYPE_NAME_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_COMMON_TYPE_NAME_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | std::string getTypeName(std::type_info const &ti); 11 | 12 | template 13 | std::string getTypeName(T const &x) 14 | { 15 | return Kanachan::getTypeName(typeid(x)); 16 | } 17 | 18 | } // namespace Kanachan 19 | 20 | #endif // !defined(KANACHAN_COMMON_TYPE_NAME_HPP_INCLUDE_GUARD) 21 | -------------------------------------------------------------------------------- /src/simulation/chi.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_CHI_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_CHI_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any chi( 13 | Kanachan::RoundState &round_state, std::uint_fast8_t encode, Kanachan::GameLog &game_log); 14 | 15 | } // namespace Kanachan 16 | 17 | #endif // !defined(KANACHAN_SIMULATION_CHI_HPP_INCLUDE_GUARD) 18 | -------------------------------------------------------------------------------- /src/simulation/huangpai_pingju.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_HUANGPAI_PINGJU_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_HUANGPAI_PINGJU_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | 8 | 9 | namespace Kanachan{ 10 | 11 | std::any huangpaiPingju(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log); 12 | 13 | } // namespace Kanachan 14 | 15 | #endif // !defined(KANACHAN_SIMULATION_HUANGPAI_PINGJU_HPP_INCLUDE_GUARD) 16 | -------------------------------------------------------------------------------- /src/simulation/peng.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_PENG_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_PENG_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any peng( 13 | Kanachan::RoundState &round_state, std::uint_fast8_t encode, Kanachan::GameLog &game_log); 14 | 15 | } // namespace Kanachan 16 | 17 | #endif // !defined(KANACHAN_SIMULATION_PENG_HPP_INCLUDE_GUARD) 18 | -------------------------------------------------------------------------------- /src/common/type_name.cpp: -------------------------------------------------------------------------------- 1 | #include "common/type_name.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::string getTypeName(std::type_info const &ti) 13 | { 14 | char const * const name = ti.name(); 15 | int status; 16 | std::unique_ptr p( 17 | abi::__cxa_demangle(name, NULL, NULL, &status), std::free); 18 | return { status == 0 ? p.get() : name }; 19 | } 20 | 21 | } // namespace Kanachan 22 | -------------------------------------------------------------------------------- /src/simulation/dapai.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_DAPAI_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_DAPAI_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any dapai( 13 | Kanachan::RoundState &round_state, std::uint_fast8_t tile, bool moqi, bool lizhi, 14 | Kanachan::GameLog &game_log); 15 | 16 | } // namespace Kanachan 17 | 18 | #endif // !defined(KANACHAN_SIMULATION_DAPAI_HPP_INCLUDE_GUARD) 19 | -------------------------------------------------------------------------------- /src/common/thread.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_COMMON_THREAD_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_COMMON_THREAD_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace Kanachan{ 9 | 10 | class ThreadTermination 11 | : public std::runtime_error 12 | { 13 | public: 14 | explicit ThreadTermination(char const *message); 15 | 16 | explicit ThreadTermination(std::string const &message); 17 | }; // class ThreadTermination 18 | 19 | } // namespace Kanachan 20 | 21 | #endif // !defined(KANACHAN_COMMON_THREAD_HPP_INCLUDE_GUARD) 22 | -------------------------------------------------------------------------------- /src/simulation/angang.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_ANGANG_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_ANGANG_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any angang( 13 | Kanachan::RoundState &round_state, std::uint_fast8_t zimo_tile, std::uint_fast8_t encode, 14 | Kanachan::GameLog &game_log); 15 | 16 | } // namespace Kanachan 17 | 18 | #endif // !defined(KANACHAN_SIMULATION_ANGANG_HPP_INCLUDE_GUARD) 19 | -------------------------------------------------------------------------------- /src/simulation/jiagang.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_JIAGANG_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_JIAGANG_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any jiagang( 13 | Kanachan::RoundState &round_state, std::uint_fast8_t tile, std::uint_fast8_t encode, 14 | Kanachan::GameLog &game_log); 15 | 16 | } // namespace Kanachan 17 | 18 | #endif // !defined(KANACHAN_SIMULATION_JIAGANG_HPP_INCLUDE_GUARD) 19 | -------------------------------------------------------------------------------- /kanachan/training/aiayn/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from torch import nn 4 | from kanachan.training.aiayn.encoder import Encoder 5 | from kanachan.training.aiayn.decoder import Decoder 6 | 7 | 8 | class Model(nn.Module): 9 | def __init__(self, encoder: Encoder, decoder: Decoder) -> None: 10 | super(Model, self).__init__() 11 | self.__encoder = encoder 12 | self.__decoder = decoder 13 | 14 | def forward(self, x): 15 | encode = self.__encoder(x[:-1]) 16 | prediction = self.__decoder(encode, x[-1]) 17 | return prediction 18 | -------------------------------------------------------------------------------- /src/simulation/round.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_ROUND_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_ROUND_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/paishan.hpp" 5 | #include "simulation/game_log.hpp" 6 | #include "simulation/game_state.hpp" 7 | #include 8 | #include 9 | 10 | 11 | namespace Kanachan{ 12 | 13 | bool simulateRound( 14 | std::vector const &seed, Kanachan::GameState &game_state, 15 | Kanachan::Paishan const *p_test_paishan, Kanachan::GameLog &game_log); 16 | 17 | } // namespace Kanachan 18 | 19 | #endif // !defined(KANACHAN_SIMULATION_ROUND_HPP_INCLUDE_GUARD) 20 | -------------------------------------------------------------------------------- /kanachan/training/position_embedding.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | 4 | 5 | class PositionEmbedding(nn.Module): 6 | def __init__( 7 | self, *, max_length: int, dimension: int, dropout: float, 8 | device: torch.device, dtype: torch.dtype): 9 | super(PositionEmbedding, self).__init__() 10 | 11 | self.position_embedding = nn.Embedding(max_length, dimension, device=device, dtype=dtype) 12 | self.dropout = nn.Dropout(p=dropout) 13 | 14 | def forward(self, x: torch.Tensor): 15 | x += self.position_embedding(torch.arange(x.size(1), device=x.device, dtype=torch.long)) 16 | return self.dropout(x) 17 | -------------------------------------------------------------------------------- /src/simulation/daminggang.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/daminggang.hpp" 2 | 3 | #include "simulation/zimo.hpp" 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | std::any daminggang(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 13 | { 14 | round_state.onDaminggang(game_log); 15 | 16 | // Si Gang San Le (四槓散了) の成立は打牌直後. 17 | 18 | // Zimo (自摸) 19 | auto zimo = std::bind(&Kanachan::zimo, std::ref(round_state), std::ref(game_log)); 20 | std::function next_step(std::move(zimo)); 21 | return next_step; 22 | } 23 | 24 | } // namespace Kanachan 25 | -------------------------------------------------------------------------------- /kanachan/training/constants.py: -------------------------------------------------------------------------------- 1 | NUM_TYPES_OF_SPARSE_FEATURES = 526 2 | MAX_NUM_ACTIVE_SPARSE_FEATURES = 33 3 | 4 | SEAT_INDEX = 2 5 | SEAT_OFFSET = 7 6 | 7 | NUM_NUMERIC_FEATURES = 6 8 | 9 | NUM_TYPES_OF_PROGRESSION_FEATURES = 2165 10 | # The following number is actually 106. However, it is set to 113 for 11 | # suitable alignment. 12 | MAX_LENGTH_OF_PROGRESSION_FEATURES = 113 13 | 14 | NUM_TYPES_OF_ACTIONS = 546 15 | # The following number is actually 30. However, it is set to 32 for 16 | # suitable alignment. 17 | MAX_NUM_ACTION_CANDIDATES = 32 18 | 19 | ENCODER_WIDTH = MAX_NUM_ACTIVE_SPARSE_FEATURES + NUM_NUMERIC_FEATURES \ 20 | + MAX_LENGTH_OF_PROGRESSION_FEATURES + MAX_NUM_ACTION_CANDIDATES 21 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase3/iterator_adaptor.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Tuple 3 | import torch 4 | from kanachan.training.iterator_adaptor_base import IteratorAdaptorBase 5 | 6 | 7 | class IteratorAdaptor(IteratorAdaptorBase): 8 | def __init__(self, path: Path) -> None: 9 | super(IteratorAdaptor, self).__init__(path) 10 | 11 | def __next__(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: 12 | sparse, numeric, positional, candidates, index, results = super(IteratorAdaptor, self).__next__() 13 | 14 | game_delta_of_grading_score = results[11] / 100.0 15 | 16 | return (sparse, numeric, positional, candidates, index, game_delta_of_grading_score) 17 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from kanachan.training.bert.encoder import Encoder 4 | from kanachan.training.bert.phase1.decoder import Decoder 5 | 6 | 7 | class Model(nn.Module): 8 | def __init__(self, encoder: Encoder, decoder: Decoder) -> None: 9 | super(Model, self).__init__() 10 | self.encoder = encoder 11 | self.decoder = decoder 12 | 13 | def forward( 14 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 15 | candidates: torch.Tensor) -> torch.Tensor: 16 | encode: torch.Tensor = self.encoder(sparse, numeric, progression, candidates) 17 | weights: torch.Tensor = self.decoder(candidates, encode) 18 | return weights 19 | -------------------------------------------------------------------------------- /test/annotation_vs_simulation/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | PROGRAM_NAME=generate.sh 6 | 7 | function print_usage () 8 | { 9 | cat >&2 <<'EOF' 10 | Usage: generate.sh PAISHAN_FILE ANNOTATION_FILE 11 | Output the tarball binary consisting of test case files to the standard output. 12 | EOF 13 | } 14 | 15 | if (( $# < 2 )); then 16 | echo 'Too few arguments' >&2 17 | print_usage 18 | exit 1 19 | fi 20 | if (( $# > 2 )); then 21 | echo 'Too many arguments' >&2 22 | print_usage 23 | exit 1 24 | fi 25 | 26 | temp_dir="$(mktemp -d)" 27 | trap "rm -rf '$temp_dir'" EXIT 28 | 29 | build/test/annotation_vs_simulation/generate "$1" "$2" "$temp_dir" 1>&2 30 | 31 | cd "$temp_dir" 32 | find . -name '*.json' -type f > list.txt 33 | tar -cT list.txt 34 | -------------------------------------------------------------------------------- /src/simulation/hule.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/hule.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round_state.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace{ 13 | 14 | using std::placeholders::_1; 15 | 16 | } // namespace `anonymous` 17 | 18 | namespace Kanachan{ 19 | 20 | bool hule( 21 | Kanachan::RoundState &round_state, std::uint_fast8_t const zimo_tile, 22 | Kanachan::GameLog &game_log) 23 | { 24 | if (zimo_tile >= 37u && zimo_tile != UINT_FAST8_MAX) { 25 | KANACHAN_THROW(_1) << static_cast(zimo_tile); 26 | } 27 | 28 | return round_state.onHule(zimo_tile, game_log); 29 | } 30 | 31 | } // namespace Kanachan 32 | -------------------------------------------------------------------------------- /kanachan/training/bert/README.md: -------------------------------------------------------------------------------- 1 | # `kanachan.training.bert` Python submodule 2 | 3 | ## Note 4 | 5 | The use of the term **BERT** here is a deliberate abuse. The term BERT actually refers to a combination of a model with transformer encoder layers and a learning method for that model. However, this project uses the term BERT to refer only to the model. The model should actually be called something like transformer encoder layers, but that would be too long, so this project calls it BERT. 6 | 7 | ## Submodules 8 | 9 | ### [`kanachan.training.bert.phase1`](phase1) Python submodule 10 | 11 | A training program for BERT with an objective function that imitates human choices in training data. 12 | 13 | ### [`kanachan.training.bert.phase2`](phase2) Python submodule 14 | 15 | A training program for BERT with an objective function that maximizes round deltas in training data. 16 | -------------------------------------------------------------------------------- /mjai.app/xiangting_calculator/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | from typing import (List, Union,) 5 | from ._xiangting_calculator import XiangtingCalculator as Impl 6 | 7 | 8 | _CONVERTER = [ 9 | 4, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10 | 13, 9, 10, 11, 12, 13, 14, 15, 16, 17, 11 | 22, 18, 19, 20, 21, 22, 23, 24, 25, 26, 12 | 27, 28, 29, 30, 31, 32, 33 13 | ] 14 | 15 | 16 | class XiangtingCalculator: 17 | def __init__(self, prefix: Union[str, Path]) -> None: 18 | if isinstance(prefix, Path): 19 | prefix = str(prefix) 20 | self.__impl = Impl(prefix) 21 | 22 | def calculate(self, hand: List[int], n: int) -> int: 23 | counts = [0] * 34 24 | for tile in hand: 25 | k = _CONVERTER[tile] 26 | counts[k] += 1 27 | return self.__impl.calculate(counts, n) 28 | -------------------------------------------------------------------------------- /src/simulation/game.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_GAME_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_GAME_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/paishan.hpp" 6 | #include "simulation/decision_maker.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | namespace Kanachan{ 17 | 18 | std::shared_ptr simulateGame( 19 | std::vector const &seed, std::uint_fast8_t room, bool dong_feng_zhan, 20 | std::array>, 4u> const &seats, 21 | std::vector const &test_paishan_list, std::stop_token stop_token); 22 | 23 | } // namespace Kanachan 24 | 25 | #endif // !defined(KANACHAN_SIMULATION_GAME_HPP_INCLUDE_GUARD) 26 | -------------------------------------------------------------------------------- /kanachan/training/README.md: -------------------------------------------------------------------------------- 1 | # `kanachan.training` Python submodule 2 | 3 | ## Submodules 4 | 5 | ### [`kanachan.training.bert`](bert) Python submodule 6 | 7 | Training programs for [BERT](https://arxiv.org/abs/1810.04805)[^BERT]. 8 | 9 | ### [`kanachan.training.iql`](iql) Python submodule 10 | 11 | Training programs for [implicit Q-learning (IQL)](https://arxiv.org/abs/2110.06169). 12 | 13 | ### [`kanachan.training.ilql`](ilql) Python submodule 14 | 15 | Training programs for [implicit language Q-learning (ILQL)](https://arxiv.org/abs/2206.11871). 16 | 17 | [^BERT]: The use of the term **BERT** here is a deliberate abuse. The term BERT actually refers to a combination of a model with transformer encoder layers and a learning method for that model. However, this project uses the term BERT to refer only to the model. The model should actually be called something like transformer encoder layers, but that would be too long, so this project calls it BERT. 18 | -------------------------------------------------------------------------------- /src/paishan/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan.prerequisites 2 | 3 | ARG CMAKE_BUILD_TYPE=Release 4 | 5 | COPY --chown=ubuntu . /opt/kanachan 6 | 7 | USER ubuntu 8 | 9 | WORKDIR /opt/kanachan 10 | 11 | RUN pushd src/common && \ 12 | protoc -I. --cpp_out=. mahjongsoul.proto && \ 13 | popd && \ 14 | mkdir build && \ 15 | pushd build && \ 16 | cmake -DPYTHON_VERSION="$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 17 | -DPYTHON_INCLUDE_PATH=/usr/include/python"$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 18 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ 19 | -DCMAKE_C_COMPILER=/usr/local/bin/gcc \ 20 | -DCMAKE_CXX_COMPILER=/usr/local/bin/g++ \ 21 | .. && \ 22 | VERBOSE=1 make -j paishan && \ 23 | popd 24 | 25 | ENTRYPOINT ["build/src/paishan/paishan", "/data"] 26 | -------------------------------------------------------------------------------- /kanachan/README.md: -------------------------------------------------------------------------------- 1 | # `cryolite/kanachan` Docker image 2 | 3 | This directory becomes the `kanachan` Python module. The module is intended to run on a docker container. Therefore, it is necessary to build the Docker image before using the module. The command to build the image is, with the top-level directory of the working tree of this repository as the current directory, as follows: 4 | 5 | ``` 6 | $ docker build -f kanachan/Dockerfile -t cryolite/kanachan . 7 | ``` 8 | 9 | If the image fails to build, try lowering the version of the base image as follows: 10 | 11 | ``` 12 | $ docker build -f kanachan/Dockerfile --build-arg BASE_IMAGE=nvcr.io/nvidia/pytorch:xx.yy-py3 -t cryolite/kanachan . 13 | ``` 14 | 15 | # `kanachan` Python module 16 | 17 | ## Submodules 18 | 19 | ### [`kanachan.training`](training) 20 | 21 | Training programs and prediction modules with [PyTorch](https://pytorch.org/). 22 | 23 | ### [`kanachan.simulation`](simulation) 24 | 25 | A simulation program. 26 | -------------------------------------------------------------------------------- /src/annotation/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan.prerequisites 2 | 3 | ARG CMAKE_BUILD_TYPE=Release 4 | 5 | COPY --chown=ubuntu . /opt/kanachan 6 | 7 | USER ubuntu 8 | 9 | WORKDIR /opt/kanachan 10 | 11 | RUN pushd src/common && \ 12 | protoc -I. --cpp_out=. mahjongsoul.proto && \ 13 | popd && \ 14 | mkdir build && \ 15 | pushd build && \ 16 | cmake -DPYTHON_VERSION="$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 17 | -DPYTHON_INCLUDE_PATH=/usr/include/python"$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 18 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ 19 | -DCMAKE_C_COMPILER=/usr/local/bin/gcc \ 20 | -DCMAKE_CXX_COMPILER=/usr/local/bin/g++ \ 21 | .. && \ 22 | VERBOSE=1 make -j annotate && \ 23 | popd 24 | 25 | ENTRYPOINT ["build/src/annotation/annotate", "/data"] 26 | -------------------------------------------------------------------------------- /test/annotation_vs_simulation/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cryolite/kanachan.prerequisites 2 | 3 | ARG CMAKE_BUILD_TYPE=Release 4 | 5 | COPY --chown=ubuntu . /opt/kanachan 6 | 7 | USER ubuntu 8 | 9 | WORKDIR /opt/kanachan 10 | 11 | RUN pushd src/common && \ 12 | protoc -I. --cpp_out=. mahjongsoul.proto && \ 13 | popd && \ 14 | mkdir build && \ 15 | pushd build && \ 16 | cmake -DPYTHON_VERSION="$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 17 | -DPYTHON_INCLUDE_PATH=/usr/include/python"$(python3 -V | sed -e 's@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@')" \ 18 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ 19 | -DCMAKE_C_COMPILER=/usr/local/bin/gcc \ 20 | -DCMAKE_CXX_COMPILER=/usr/local/bin/g++ \ 21 | .. && \ 22 | VERBOSE=1 make -j generate && \ 23 | popd 24 | 25 | ENTRYPOINT ["test/annotation_vs_simulation/generate.sh"] 26 | -------------------------------------------------------------------------------- /kanachan/training/positional_encoding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | import torch 5 | from torch import nn 6 | 7 | 8 | class PositionalEncoding(nn.Module): 9 | def __init__( 10 | self, *, max_length: int, dimension: int, dropout: float, 11 | device: torch.device, dtype: torch.dtype): 12 | super(PositionalEncoding, self).__init__() 13 | 14 | position = torch.arange(max_length, device=device, dtype=dtype).unsqueeze(1) 15 | div_term = torch.exp( 16 | torch.arange(0, dimension, 2, device=device, dtype=dtype) * (-math.log(10000.0) / dimension)) 17 | pe = torch.zeros(max_length, dimension, device=device, dtype=dtype) 18 | pe[:, 0::2] = torch.sin(position * div_term) 19 | pe[:, 1::2] = torch.cos(position * div_term) 20 | self.register_buffer('_pe', pe) 21 | 22 | self.dropout = nn.Dropout(p=dropout) 23 | 24 | def forward(self, x: torch.Tensor): 25 | x += self._pe 26 | return self.dropout(x) 27 | -------------------------------------------------------------------------------- /kanachan/training/iql/qq_model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.nn.parallel import DistributedDataParallel 4 | from kanachan.training.iql.q_model import QModel 5 | 6 | 7 | class QQModel(nn.Module): 8 | def __init__(self, q1_model: QModel, q2_model: QModel) -> None: 9 | super(QQModel, self).__init__() 10 | 11 | if isinstance(q1_model, DistributedDataParallel): 12 | self.q1_model = q1_model.module 13 | else: 14 | self.q1_model = q1_model 15 | if isinstance(q2_model, DistributedDataParallel): 16 | self.q2_model = q2_model.module 17 | else: 18 | self.q2_model = q2_model 19 | 20 | def forward( 21 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 22 | candidates: torch.Tensor) -> torch.Tensor: 23 | q1 = self.q1_model(sparse, numeric, progression, candidates) 24 | q2 = self.q2_model(sparse, numeric, progression, candidates) 25 | return torch.minimum(q1, q2) 26 | -------------------------------------------------------------------------------- /src/simulation/paishan.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_PAISHAN_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_PAISHAN_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace Kanachan{ 11 | 12 | class Paishan; 13 | 14 | void swap(Paishan &lhs, Paishan &rhs) noexcept; 15 | 16 | class Paishan 17 | { 18 | public: 19 | explicit Paishan(std::vector const &seed); 20 | 21 | explicit Paishan(boost::python::list paishan); 22 | 23 | Paishan(Paishan const &rhs) = default; 24 | 25 | Paishan(Paishan &&rhs) = default; 26 | 27 | void swap(Paishan &rhs) noexcept; 28 | 29 | Paishan &operator=(Paishan const &rhs); 30 | 31 | Paishan &operator=(Paishan &&rhs) noexcept; 32 | 33 | public: 34 | std::uint_fast8_t operator[](std::uint_fast8_t index) const; 35 | 36 | private: 37 | std::array tiles_; 38 | }; // class Paishan 39 | 40 | } // namespace Kanachan 41 | 42 | #endif // !defined(KANACHAN_SIMULATION_PAISHAN_HPP_INCLUDE_GUARD) 43 | -------------------------------------------------------------------------------- /kanachan/training/ilql/q_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import torch 4 | from torch import nn 5 | from torch.nn.parallel import DistributedDataParallel 6 | from kanachan.training.ilql.qv_model import QVModel 7 | 8 | 9 | class QModel(nn.Module): 10 | def __init__(self, qv1_model: QVModel, qv2_model: QVModel) -> None: 11 | super(QModel, self).__init__() 12 | 13 | if isinstance(qv1_model, DistributedDataParallel): 14 | self.qv1_model = qv1_model.module 15 | else: 16 | self.qv1_model = qv1_model 17 | if isinstance(qv2_model, DistributedDataParallel): 18 | self.qv2_model = qv2_model.module 19 | else: 20 | self.qv2_model = qv2_model 21 | 22 | def forward( 23 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 24 | candidates: torch.Tensor) -> torch.Tensor: 25 | q1, _ = self.qv1_model(sparse, numeric, progression, candidates) 26 | q2, _ = self.qv2_model(sparse, numeric, progression, candidates) 27 | return torch.minimum(q1, q2) 28 | -------------------------------------------------------------------------------- /mjai.app/test.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | . prerequisites/prologue.sh 5 | 6 | archive_path="${1:-mjai-app.zip}" 7 | 8 | tempdir="$(mktemp -d)" 9 | push_rollback_command "rm -rf '$tempdir'" 10 | 11 | pushd "$tempdir" 12 | 13 | python3 -m venv .local 14 | . .local/bin/activate 15 | push_rollback_command 'deactivate' 16 | python3 -m pip install -U pip 17 | 18 | git clone 'https://github.com/smly/mjai.app.git' 19 | pushd mjai.app 20 | python3 -m pip install -U . 21 | popd 22 | 23 | popd 24 | 25 | while true; do 26 | logs_dir="./logs.$(date +%Y-%m-%d-%H-%M-%S)" 27 | 28 | pushd "$tempdir" 29 | 30 | cat > test.py < 6 | #include 7 | 8 | 9 | namespace Kanachan::GIL{ 10 | 11 | RecursiveLock::RecursiveLock() 12 | : state_(PyGILState_Ensure()) 13 | , owns_(true) 14 | {} 15 | 16 | RecursiveLock::RecursiveLock(RecursiveLock &&rhs) noexcept 17 | : state_(rhs.state_) 18 | , owns_(rhs.owns_) 19 | { 20 | rhs.owns_ = false; 21 | } 22 | 23 | RecursiveLock::~RecursiveLock() 24 | { 25 | if (owns_) { 26 | PyGILState_Release(state_); 27 | } 28 | } 29 | 30 | RecursiveLock &RecursiveLock::operator=(RecursiveLock &&rhs) noexcept 31 | { 32 | state_ = rhs.state_; 33 | owns_ = rhs.owns_; 34 | rhs.owns_ = false; 35 | return *this; 36 | } 37 | 38 | RecursiveRelease::RecursiveRelease() 39 | : save_(nullptr) 40 | { 41 | if (PyGILState_Check() != 0) { 42 | save_ = PyEval_SaveThread(); 43 | } 44 | } 45 | 46 | RecursiveRelease::~RecursiveRelease() 47 | { 48 | if (save_ != nullptr) { 49 | PyEval_RestoreThread(save_); 50 | save_ = nullptr; 51 | } 52 | } 53 | 54 | } // namespace Kanachan::GIL 55 | -------------------------------------------------------------------------------- /src/simulation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") 2 | 3 | add_library(xiangting_calculator SHARED 4 | xiangting_calculator.cpp) 5 | target_link_libraries(xiangting_calculator 6 | PRIVATE common 7 | PRIVATE marisa 8 | PRIVATE Boost::python 9 | PRIVATE Boost::headers 10 | PRIVATE "python${PYTHON_VERSION}" 11 | PRIVATE util 12 | PRIVATE dl) 13 | 14 | add_library(simulation SHARED 15 | simulate.cpp 16 | simulator.cpp 17 | game.cpp 18 | round.cpp 19 | sijia_lizhi.cpp 20 | sigang_sanle.cpp 21 | sifeng_lianda.cpp 22 | jiuzhong_jiupai.cpp 23 | huangpai_pingju.cpp 24 | hule.cpp 25 | daminggang.cpp 26 | peng.cpp 27 | chi.cpp 28 | dapai.cpp 29 | jiagang.cpp 30 | angang.cpp 31 | zimo.cpp 32 | game_log.cpp 33 | round_state.cpp 34 | shoupai.cpp 35 | xiangting_calculator.cpp 36 | paishan.cpp 37 | game_state.cpp 38 | round_result.cpp 39 | decision_maker.cpp 40 | gil.cpp 41 | utility.cpp) 42 | target_link_libraries(simulation 43 | PRIVATE common 44 | PRIVATE marisa 45 | PRIVATE Boost::python 46 | PRIVATE Boost::headers 47 | PRIVATE "python${PYTHON_VERSION}" 48 | PRIVATE util 49 | PRIVATE dl) 50 | -------------------------------------------------------------------------------- /src/simulation/gil.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_GIL_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_GIL_HPP_INCLUDE_GUARD 3 | 4 | #define PY_SSIZE_T_CLEAN 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan::GIL{ 13 | 14 | class RecursiveLock 15 | { 16 | public: 17 | RecursiveLock(); 18 | 19 | RecursiveLock(RecursiveLock const &) = delete; 20 | 21 | RecursiveLock(RecursiveLock &&rhs) noexcept; 22 | 23 | ~RecursiveLock(); 24 | 25 | RecursiveLock &operator=(RecursiveLock const &) = delete; 26 | 27 | RecursiveLock &operator=(RecursiveLock &&rhs) noexcept; 28 | 29 | private: 30 | PyGILState_STATE state_; 31 | bool owns_; 32 | }; // class RecursiveLock 33 | 34 | class RecursiveRelease 35 | { 36 | public: 37 | RecursiveRelease(); 38 | 39 | RecursiveRelease(RecursiveRelease const &) = delete; 40 | 41 | ~RecursiveRelease(); 42 | 43 | RecursiveRelease &operator=(RecursiveRelease const &) = delete; 44 | 45 | private: 46 | PyThreadState *save_; 47 | }; // class UniqueRelease 48 | 49 | } // namespace Kanachan::GIL 50 | 51 | #endif // !defined(KANACHAN_SIMULATION_GIL_HPP_INCLUDE_GUARD) 52 | -------------------------------------------------------------------------------- /src/simulation/simulator.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SIMULATOR_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_SIMULATOR_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace Kanachan { 14 | 15 | class Simulator 16 | { 17 | private: 18 | class Impl_; 19 | 20 | public: 21 | Simulator( 22 | std::string const &device, boost::python::object dtype, std::uint_fast8_t room, 23 | std::uint_fast8_t baseline_grade, boost::python::object baseline_model, 24 | std::uint_fast8_t proposed_grade, boost::python::object proposed_model, 25 | unsigned long simulation_mode, std::size_t num_simulation_sets, 26 | std::size_t batch_size, std::size_t concurrency, boost::python::object progress); 27 | 28 | Simulator(Simulator const &) = delete; 29 | 30 | Simulator &operator=(Simulator const &) = delete; 31 | 32 | boost::python::list run(); 33 | 34 | private: 35 | std::shared_ptr p_impl_; 36 | }; // class Simulator 37 | 38 | } // namespace Kanachan 39 | 40 | #endif // !defined(KANACHAN_SIMULATION_SIMULATOR_HPP_INCLUDE_GUARD) 41 | -------------------------------------------------------------------------------- /src/simulation/round.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/round.hpp" 2 | 3 | #include "simulation/zimo.hpp" 4 | #include "simulation/round_state.hpp" 5 | #include "simulation/paishan.hpp" 6 | #include "simulation/game_state.hpp" 7 | #include "simulation/game_log.hpp" 8 | #include "common/throw.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | namespace Kanachan{ 17 | 18 | bool simulateRound( 19 | std::vector const &seed, Kanachan::GameState &game_state, 20 | Kanachan::Paishan const * const p_test_paishan, Kanachan::GameLog &game_log) 21 | { 22 | Kanachan::RoundState round_state(seed, game_state, p_test_paishan); 23 | game_log.onBeginningOfRound(); 24 | 25 | std::function next_step = std::bind( 26 | &Kanachan::zimo, std::ref(round_state), std::ref(game_log)); 27 | 28 | for (;;) { 29 | std::any next_step_ = next_step(); 30 | if (std::any_cast>(&next_step_) != nullptr) { 31 | next_step = std::any_cast>(next_step_); 32 | continue; 33 | } 34 | if (std::any_cast(&next_step_) != nullptr) { 35 | return std::any_cast(next_step_); 36 | } 37 | KANACHAN_THROW("A logic error."); 38 | } 39 | } 40 | 41 | } // namespace Kanachan 42 | -------------------------------------------------------------------------------- /src/simulation/decision_maker.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_DECISION_MAKER_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_DECISION_MAKER_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace Kanachan{ 14 | 15 | class DecisionMaker 16 | { 17 | private: 18 | class Impl_; 19 | 20 | public: 21 | DecisionMaker( 22 | std::string const &device, boost::python::object dtype, boost::python::object model, 23 | std::size_t batch_size, bool stochastic); 24 | 25 | DecisionMaker(DecisionMaker const &) = delete; 26 | 27 | DecisionMaker &operator=(DecisionMaker const &) = delete; 28 | 29 | void shrinkBatchSizeToFitNumThreads(std::size_t num_threads); 30 | 31 | std::uint_fast16_t operator()( 32 | std::vector &&sparse, std::vector &&numeric, 33 | std::vector &&progression, std::vector &&candidates, 34 | std::stop_token stop_token); 35 | 36 | void join(); 37 | 38 | private: 39 | std::shared_ptr p_impl_; 40 | }; // class DecisionMaker 41 | 42 | } // namespace Kanachan 43 | 44 | #endif // !defined(KANACHAN_SIMULATION_DECISION_MAKER_HPP_INCLUDE_GUARD) 45 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from omegaconf import DictConfig 5 | import hydra 6 | import torch 7 | from torch import nn 8 | from kanachan.training.constants import NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES 9 | import kanachan.training.bert.phase1.config # type: ignore pylint: disable=unused-import 10 | from kanachan.training.bert.phase1.decoder import Decoder 11 | from kanachan.training.bert.phase1.model import Model 12 | from kanachan.training.bert.phase1.iterator_adaptor import IteratorAdaptor 13 | from kanachan.training.bert import training 14 | 15 | 16 | @hydra.main(version_base=None, config_name='config') 17 | def _main(config: DictConfig) -> None: 18 | loss_function = nn.CrossEntropyLoss(ignore_index=NUM_TYPES_OF_ACTIONS + 1) 19 | 20 | def prediction_function(weights: torch.Tensor) -> torch.Tensor: 21 | assert weights.dim() == 2 22 | assert weights.size(1) == MAX_NUM_ACTION_CANDIDATES 23 | weights = nn.Softmax()(weights) 24 | return torch.argmax(weights, dim=1) 25 | 26 | training.main( 27 | config=config, iterator_adaptor_type=IteratorAdaptor, decoder_type=Decoder, 28 | model_type=Model, loss_function=loss_function, prediction_function=prediction_function) 29 | 30 | 31 | if __name__ == '__main__': 32 | _main() # pylint: disable=no-value-for-parameter 33 | sys.exit(0) 34 | -------------------------------------------------------------------------------- /src/annotation/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "annotation/utility.hpp" 2 | 3 | #include "common/throw.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan{ 13 | 14 | namespace{ 15 | 16 | using std::placeholders::_1; 17 | 18 | } // namepsace *unnamed* 19 | 20 | std::uint_fast8_t pai2Num(std::string_view const &pai) 21 | { 22 | if (pai.size() != 2u) { 23 | KANACHAN_THROW(_1) << "pai = " << pai; 24 | } 25 | 26 | std::uint_fast8_t base = std::numeric_limits::max(); 27 | switch (pai[1u]) { 28 | case 'm': 29 | base = 0u; 30 | break; 31 | case 'p': 32 | base = 10u; 33 | break; 34 | case 's': 35 | base = 20u; 36 | break; 37 | case 'z': 38 | base = 30u; 39 | break; 40 | default: 41 | KANACHAN_THROW(_1) << "pai = " << pai; 42 | } 43 | 44 | std::uint_fast8_t num = pai[0u] - '0'; 45 | if (pai[1u] == 'z') { 46 | if (pai[0u] == '0' || pai[0u] >= '8') { 47 | KANACHAN_THROW(_1) << "pai = " << pai; 48 | } 49 | --num; 50 | } 51 | 52 | return base + num; 53 | } 54 | 55 | std::uint_fast8_t pai2Num(std::string const &pai) 56 | { 57 | return pai2Num(std::string_view(pai.cbegin(), pai.cend())); 58 | } 59 | 60 | } // namespace Kanachan 61 | -------------------------------------------------------------------------------- /src/simulation/peng.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/peng.hpp" 2 | 3 | #include "simulation/dapai.hpp" 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include "common/assert.hpp" 7 | #include "common/throw.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | namespace{ 16 | 17 | using std::placeholders::_1; 18 | 19 | } // namespace `anonymous` 20 | 21 | namespace Kanachan{ 22 | 23 | std::any peng( 24 | Kanachan::RoundState &round_state, std::uint_fast8_t const encode, Kanachan::GameLog &game_log) 25 | { 26 | if (encode >= 40u) { 27 | KANACHAN_THROW(_1) << static_cast(encode); 28 | } 29 | 30 | std::uint_fast16_t const action = round_state.onPeng(encode, game_log); 31 | 32 | if (action >= 148u) { 33 | KANACHAN_THROW(_1) << action << ": An invalid dapai action after peng."; 34 | } 35 | 36 | std::uint_fast8_t const tile = action / 4u; 37 | bool const moqi = ((action - tile * 4u) / 2u >= 2u); 38 | KANACHAN_ASSERT((!moqi)); 39 | bool const lizhi = ((action - tile * 4u - moqi * 2u) == 1u); 40 | KANACHAN_ASSERT((!lizhi)); 41 | auto dapai = std::bind( 42 | &Kanachan::dapai, std::ref(round_state), tile, moqi, lizhi, std::ref(game_log)); 43 | std::function next_step(std::move(dapai)); 44 | return next_step; 45 | } 46 | 47 | } // namespace Kanachan 48 | -------------------------------------------------------------------------------- /src/simulation/round_result.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_ROUND_RESULT_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_ROUND_RESULT_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | 6 | 7 | namespace Kanachan{ 8 | 9 | class RoundResult 10 | { 11 | public: 12 | RoundResult(); 13 | 14 | // `type == 0`: 親の自摸和 15 | // `type == 1`: 子の自摸和 16 | // `type == 2`: 子⇒親の被自摸和 17 | // `type == 3`: 親=>子の被自摸和 18 | // `type == 4`: 子=>子の被自摸和 19 | // `type == 5`: 親の栄和 20 | // `type == 6`: 子の栄和 21 | // `type == 7`: 子⇒親の放銃 22 | // `type == 8`: 親=>子の放銃 23 | // `type == 9`: 子=>子の放銃 24 | // `type == 10`: 横移動 25 | // `type == 11`: 荒牌平局(不聴) 26 | // `type == 12`: 荒牌平局(聴牌) 27 | // `type == 13`: 荒牌平局(流し満貫) 28 | // `type == 14`: 途中流局 29 | void setType(std::uint_fast8_t type); 30 | 31 | void setInLizhi(bool in_lizhi); 32 | 33 | void setHasFulu(bool has_fulu); 34 | 35 | void setRoundDeltaScore(std::int_fast32_t round_delta_score); 36 | 37 | void setRoundScore(std::int_fast32_t round_score); 38 | 39 | std::uint_fast8_t getType() const noexcept; 40 | 41 | bool getInLizhi() const noexcept; 42 | 43 | bool getHasFulu() const noexcept; 44 | 45 | std::int_fast32_t getRoundDeltaScore() const noexcept; 46 | 47 | std::int_fast32_t getRoundScore() const noexcept; 48 | 49 | private: 50 | std::uint_fast8_t type_; 51 | bool in_lizhi_; 52 | bool has_fulu_; 53 | std::int_fast32_t round_delta_score_; 54 | std::int_fast32_t round_score_; 55 | }; 56 | 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/simulation/xiangting_calculator.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_XIANGTING_CALCULATOR_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_XIANGTING_CALCULATOR_HPP_INCLUDE_GUARD 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace Kanachan{ 14 | 15 | class XiangtingCalculator 16 | { 17 | public: 18 | explicit XiangtingCalculator( 19 | std::filesystem::path const &prefix = std::filesystem::path("/home/ubuntu/.local/share/kanachan")); 20 | 21 | explicit XiangtingCalculator(std::string const &prefix); 22 | 23 | template 24 | std::uint_fast8_t operator()( 25 | RandomAccessIterator first, RandomAccessIterator last, 26 | std::uint_fast8_t n) const; 27 | 28 | template 29 | std::uint_fast8_t operator()( 30 | RandomAccessRange const &tiles, std::uint_fast8_t n) const; 31 | 32 | boost::python::long_ calculate( 33 | boost::python::list tile_counts, boost::python::long_ n) const; 34 | 35 | private: 36 | class Impl_; 37 | std::shared_ptr p_impl_; 38 | }; // class XiangtingCalculator 39 | 40 | template 41 | std::uint_fast8_t calculateXiangting( 42 | RandomAccessRange const &tiles, std::uint_fast8_t n); 43 | 44 | } // namespace Kanachan 45 | 46 | #endif // !defined(KANACHAN_SIMULATION_XIANGTING_CALCULATOR_HPP_INCLUDE_GUARD) 47 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase2/iterator_adaptor.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Tuple 3 | import torch 4 | from kanachan.training.iterator_adaptor_base import IteratorAdaptorBase 5 | 6 | 7 | class IteratorAdaptor(IteratorAdaptorBase): 8 | def __init__(self, path: Path) -> None: 9 | super(IteratorAdaptor, self).__init__(path) 10 | 11 | def __next__(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: 12 | sparse, numeric, positional, candidates, index, results = super(IteratorAdaptor, self).__next__() 13 | 14 | round_delta_of_score = results[1] 15 | # From the game records of the 82215309 rounds crawled from July 2020 to June 2021, the mean 16 | # of the score round deltas is calculated to be -1.1618527 and the standard deviation 17 | # \sigma = 4940.5010. Since the mean is much smaller than the standard deviation, the 18 | # following code approximate it to 0 and scale the score round deltas so that the 3\sigma 19 | # range corresponds to [-1, 1]. 20 | # 21 | # Even after the scaling as described above, there are still many large outliers due to 22 | # Yiman (役満), etc. Therefore, instead of MSE, the use of Huber loss should be also 23 | # considered. 24 | round_delta_of_score /= (3.0 * 4940.0) 25 | round_delta_of_score = torch.tensor(round_delta_of_score, device='cpu', dtype=torch.float32) 26 | 27 | return (sparse, numeric, positional, candidates, index, round_delta_of_score) 28 | -------------------------------------------------------------------------------- /src/simulation/chi.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/chi.hpp" 2 | 3 | #include "simulation/dapai.hpp" 4 | #include "simulation/game_log.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include "common/assert.hpp" 7 | #include "common/throw.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | namespace { 16 | 17 | using std::placeholders::_1; 18 | 19 | } // namespace `anonymous` 20 | 21 | namespace Kanachan{ 22 | 23 | std::any chi( 24 | Kanachan::RoundState &round_state, std::uint_fast8_t const encode, Kanachan::GameLog &game_log) 25 | { 26 | if (encode >= 90u) { 27 | KANACHAN_THROW(_1) << static_cast(encode); 28 | } 29 | 30 | std::uint_fast16_t const action = round_state.onChi(encode, game_log); 31 | 32 | if (action <= 147u) { 33 | std::uint_fast8_t const tile = action / 4u; 34 | bool const moqi = ((action - tile * 4u) / 2u >= 2u); 35 | KANACHAN_ASSERT((!moqi)); 36 | bool const lizhi = ((action - tile * 4u - moqi * 2u) == 1u); 37 | KANACHAN_ASSERT((!lizhi)); 38 | auto dapai = std::bind(&Kanachan::dapai, std::ref(round_state), tile, moqi, lizhi, std::ref(game_log)); 39 | std::function next_step(std::move(dapai)); 40 | return next_step; 41 | } 42 | 43 | KANACHAN_THROW(_1) << action << ": An invalid action after chi."; 44 | #pragma GCC diagnostic push 45 | #pragma GCC diagnostic ignored "-Wreturn-type" 46 | } 47 | #pragma GCC diagnostic pop 48 | 49 | } // namespace Kanachan 50 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 7 | "runArgs": ["--gpus", "all"], 8 | "features": { 9 | "ghcr.io/devcontainers-contrib/features/wget-apt-get:1": {}, 10 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 11 | "ghcr.io/devcontainers/features/python:1": {} 12 | }, 13 | 14 | "containerEnv": { 15 | "C_INCLUDE_PATH": "/home/vscode/.local/include", 16 | "CPLUS_INCLUDE_PATH": "/home/vscode/.local/include", 17 | "LIBRARY_PATH": "/home/vscode/.local/lib64:/home/vscode/.local/lib", 18 | "LD_LIBRARY_PATH": "/home/vscode/.local/lib64:/home/vscode/.local/lib" 19 | }, 20 | 21 | // Features to add to the dev container. More info: https://containers.dev/features. 22 | // "features": {}, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | 27 | // Use 'postCreateCommand' to run commands after the container is created. 28 | "postCreateCommand": "bash /workspaces/kanachan/.devcontainer/post-create.sh" 29 | 30 | // Configure tool-specific properties. 31 | // "customizations": {}, 32 | 33 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 34 | // "remoteUser": "root" 35 | } 36 | -------------------------------------------------------------------------------- /kanachan/training/aiayn/encoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import torch 4 | from torch import nn 5 | from kanachan.training.constants import ( 6 | NUM_TYPES_OF_SPARSE_FEATURES, NUM_TYPES_OF_PROGRESSION_FEATURES, 7 | MAX_LENGTH_OF_PROGRESSION_FEATURES 8 | ) 9 | from kanachan.training.positional_embedding import PositionalEmbedding 10 | 11 | 12 | class Encoder(nn.Module): 13 | def __init__( 14 | self, num_dimensions: int, num_heads: int, num_layers:int, 15 | dropout: float=0.1, sparse: bool=False) -> None: 16 | super(Encoder, self).__init__() 17 | 18 | self.__sparse_embedding = nn.Embedding( 19 | NUM_TYPES_OF_SPARSE_FEATURES + 1, num_dimensions, 20 | padding_idx=NUM_TYPES_OF_SPARSE_FEATURES, sparse=sparse) 21 | 22 | self.__positional_embedding = PositionalEmbedding( 23 | NUM_TYPES_OF_PROGRESSION_FEATURES + 1, num_dimensions, 24 | padding_idx=NUM_TYPES_OF_PROGRESSION_FEATURES, 25 | max_length=MAX_LENGTH_OF_PROGRESSION_FEATURES, sparse=sparse) 26 | 27 | encoder_layer = nn.TransformerEncoderLayer( 28 | num_dimensions, num_heads, dropout=dropout, batch_first=True) 29 | self.__encoder = nn.TransformerEncoder(encoder_layer, num_layers) 30 | 31 | def forward(self, x): 32 | sparse, numeric, positional = x 33 | sparse = self.__sparse_embedding(sparse) 34 | positional = self.__positional_embedding(positional) 35 | embedding = torch.cat((sparse, numeric, positional), dim=1) 36 | encode = self.__encoder(embedding) 37 | return encode 38 | -------------------------------------------------------------------------------- /mjai.app/README.md: -------------------------------------------------------------------------------- 1 | # [AIJansou (mjai.app)](https://mjai.app/) Support 2 | 3 | AIJansou (mjai.app) is a Riichi Mahjong AI competition platform. This directory contains a set of tools for preparing kanachan models to participate in AIJansou. 4 | 5 | ## `build.sh` 6 | 7 | `build.sh` is a script that converts the model trained with kanachan into a `.zip` file for submission to AIJansou. First, prepare a model file (a file with the `.kanachan` extension). Next, with the top-level directory of the working tree of this repository as the current directory, execute the following command: 8 | 9 | ``` 10 | $ mjai.app/build.sh PATH_TO_MODEL_FILE 11 | ``` 12 | 13 | That's it! Now, just upload the `mjai-app.zip` file generated in the top-level directory to AIJansou and enjoy! 14 | 15 | ## `test.sh` (For Developpers Only) 16 | 17 | This directory also contains the `test.sh` script for testing the functionality of the generated `mjai-app.zip` file before actually submitting it to AIJansou. To execute this script, with the top-level directory of working tree of this repository as the current directory, run the following command: 18 | 19 | ``` 20 | $ mjai.app/test.sh 21 | ``` 22 | 23 | This script performs repeated self-matches using four replicas of the model bundled in `mjai-app.zip` to check for any errors. Once this script starts running, it will not stop unless forcibly halted, whether by repeatedly pressing the `^C` key sequence or sending a `KILL` signal. Logs for each self-match are output to a directory named `logs.YYYY-MM-DD-hh-mm-ss`, where `YYYY-MM-DD-hh-mm-ss` is the start time of each self-match. Log directories that are determined to be clearly error-free are automatically deleted. 24 | -------------------------------------------------------------------------------- /src/simulation/simulate.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SIMULATE_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_SIMULATE_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/game_log.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | namespace Kanachan{ 18 | 19 | boost::python::list simulate( 20 | std::string const &device, boost::python::object dtype, long room, 21 | long baseline_grade, boost::python::object baseline_model, 22 | long proposed_grade, boost::python::object proposed_model, 23 | long simulation_mode, long num_simulation_sets, 24 | long batch_size, long concurrency, boost::python::object progress); 25 | 26 | std::shared_ptr test( 27 | boost::python::long_ simulation_mode, boost::python::tuple grades, 28 | boost::python::object test_model, boost::python::list test_paishan_list); 29 | 30 | } // namespace Kanachan 31 | 32 | 33 | BOOST_PYTHON_MODULE(_simulation) 34 | { 35 | boost::python::class_< 36 | Kanachan::GameLog, std::shared_ptr, boost::noncopyable 37 | >("GameLog", boost::python::no_init) 38 | .def("get_result", &Kanachan::GameLog::getResult) 39 | .def("get_episode", &Kanachan::GameLog::getEpisode); 40 | boost::python::def("simulate", &Kanachan::simulate); 41 | boost::python::def("test", &Kanachan::test); 42 | } // BOOST_PYTHON_MODULE(_simulation) 43 | 44 | 45 | #endif // !defined(KANACHAN_SIMULATION_SIMULATE_HPP_INCLUDE_GUARD) 46 | -------------------------------------------------------------------------------- /src/annotation/round_progress.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_ANNOTATION_ROUND_PROGRESS_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_ANNOTATION_ROUND_PROGRESS_HPP_INCLUDE_GUARD 3 | 4 | #include "common/mahjongsoul.pb.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | namespace Kanachan{ 12 | 13 | class RoundProgress 14 | { 15 | public: 16 | RoundProgress() = default; 17 | 18 | RoundProgress(RoundProgress const &) = delete; 19 | 20 | RoundProgress &operator=(RoundProgress const &) = delete; 21 | 22 | void onNewRound(lq::RecordNewRound const &); 23 | 24 | void onZimo(lq::RecordDealTile const &record); 25 | 26 | void onDapai(lq::RecordDiscardTile const &record); 27 | 28 | void onChiPengGang(lq::RecordChiPengGang const &record); 29 | 30 | void onGang(lq::RecordAnGangAddGang const &record); 31 | 32 | std::size_t getSize() const; 33 | 34 | void print(std::size_t size, std::ostream &os) const; 35 | 36 | private: 37 | static constexpr std::uint_fast16_t beginning_of_round_offset_ = 0u; 38 | static constexpr std::uint_fast16_t zimo_offset_ = 1u; 39 | static constexpr std::uint_fast16_t dapai_offset_ = 5u; 40 | static constexpr std::uint_fast16_t chi_offset_ = 597u; 41 | static constexpr std::uint_fast16_t peng_offset_ = 957u; 42 | static constexpr std::uint_fast16_t daminggang_offset_ = 1437u; 43 | static constexpr std::uint_fast16_t angang_offset_ = 1881u; 44 | static constexpr std::uint_fast16_t jiagang_offset_ = 2017u; 45 | 46 | std::vector events_; 47 | }; // class RoundProgress 48 | 49 | } // namespace Kanachan 50 | 51 | #endif // !defined(KANACHAN_ANNOTATION_ROUND_PROGRESS_HPP_INCLUDE_GUARD) 52 | -------------------------------------------------------------------------------- /src/simulation/jiagang.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/jiagang.hpp" 2 | 3 | #include "simulation/hule.hpp" 4 | #include "simulation/zimo.hpp" 5 | #include "simulation/round_state.hpp" 6 | #include "simulation/game_log.hpp" 7 | #include "common/assert.hpp" 8 | #include "common/throw.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | namespace { 18 | 19 | using std::placeholders::_1; 20 | 21 | } // namespace `anonymous` 22 | 23 | namespace Kanachan{ 24 | 25 | std::any jiagang( 26 | Kanachan::RoundState &round_state, std::uint_fast8_t const zimo_tile, 27 | std::uint_fast8_t const encode, Kanachan::GameLog &game_log) 28 | { 29 | if (zimo_tile >= 37u) { 30 | KANACHAN_THROW(_1) << static_cast(zimo_tile); 31 | } 32 | if (encode >= 37u) { 33 | KANACHAN_THROW(_1) << static_cast(encode); 34 | } 35 | 36 | std::uint_fast16_t const action = round_state.onJiagang(zimo_tile, encode, game_log); 37 | 38 | if (action == UINT_FAST16_MAX) { 39 | // Si Gang San Le (四槓散了) の成立は打牌直後. 40 | auto zimo = std::bind(&Kanachan::zimo, std::ref(round_state), std::ref(game_log)); 41 | std::function next_step(std::move(zimo)); 42 | return next_step; 43 | } 44 | 45 | if (action == 543u) { 46 | // Qiang Gang (槍槓) 47 | std::uint_fast8_t const zimo_tile = UINT_FAST8_MAX; 48 | auto hule = std::bind(&Kanachan::hule, std::ref(round_state), zimo_tile, std::ref(game_log)); 49 | std::function next_step(std::move(hule)); 50 | return next_step; 51 | } 52 | 53 | KANACHAN_THROW(_1) << action << ": An invalid action on jia gang."; 54 | } 55 | 56 | } // namespace Kanachan 57 | -------------------------------------------------------------------------------- /kanachan/training/aiayn/decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import torch 4 | from torch import nn 5 | from kanachan.training.constants import NUM_TYPES_OF_ACTIONS 6 | 7 | 8 | class Decoder(nn.Module): 9 | def __init__( 10 | self, num_dimensions: int, num_heads: int, num_layers: int, 11 | num_final_dimensions: int=2048, dropout: float=0.1, 12 | sparse: bool=False) -> None: 13 | super(Decoder, self).__init__() 14 | 15 | self.__candidates_embedding = nn.Embedding( 16 | NUM_TYPES_OF_ACTIONS + 2, num_dimensions, 17 | padding_idx=NUM_TYPES_OF_ACTIONS + 1, sparse=sparse) 18 | self.__candidates_dropout = nn.Dropout(p=dropout) 19 | 20 | decoder_layer = nn.TransformerDecoderLayer( 21 | num_dimensions, num_heads, dropout=dropout, batch_first=True) 22 | self.__decoder = nn.TransformerDecoder(decoder_layer, num_layers) 23 | 24 | # The final layer is position-wise feed-forward network. 25 | self.__semifinal_linear = nn.Linear(num_dimensions, num_final_dimensions) 26 | self.__final_activation = nn.ReLU() 27 | self.__final_dropout = nn.Dropout(p=dropout) 28 | self.__final_linear = nn.Linear(num_final_dimensions, 1) 29 | 30 | def forward(self, encode, candidates): 31 | candidates_embedding = self.__candidates_embedding(candidates) 32 | candidates_embedding = self.__candidates_dropout(candidates_embedding) 33 | 34 | decode = self.__decoder(candidates_embedding, encode) 35 | 36 | output = self.__semifinal_linear(decode) 37 | output = self.__final_activation(output) 38 | output = self.__final_dropout(output) 39 | 40 | prediction = self.__final_linear(output) 41 | prediction = torch.squeeze(prediction, dim=2) 42 | return prediction 43 | -------------------------------------------------------------------------------- /bin/annotate4rl/README.md: -------------------------------------------------------------------------------- 1 | # `cryolite/kanachan.annotate4rl` Docker image 2 | 3 | A Docker image to create annotation data for offline reinforcement learning. 4 | 5 | ### How to Build 6 | 7 | First [build the cryolite/kanachan Docker image](../../kanachan/README.md#cryolitekanachan-docker-image). Then, execute the following command with the top directory of the working tree of this repository as the current directory: 8 | 9 | ```bash 10 | kanachan$ docker build -f bin/annotate4rl/Dockerfile -t cryolite/kanachan.annotate4rl . 11 | ``` 12 | 13 | # `annotate4rl.py` Python program 14 | 15 | ### Usage 16 | 17 | ```sh 18 | $ docker run --rm cryolite/kanachan.annotate4rl [OPTION]... [INPUT_FILE] 19 | ``` 20 | 21 | or 22 | 23 | ```sh 24 | $ another-command | docker run -i --rm cryolite/kanachan.annotate4rl [OPTION]... [-] 25 | ``` 26 | 27 | Convert the [training data format for behavioral cloning](https://github.com/Cryolite/kanachan/wiki/Notes-on-Training-Data#training-data-format-for-behavioral-cloning) in the file specified by the `INPUT_FILE` argument to the [training data format for offline reinforcement learning](https://github.com/Cryolite/kanachan/wiki/Notes-on-Training-Data#training-data-format-for-offline-reinforcement-learning). If `-` is specified for this argument or omitted, the conversion will be performed on the standard input. When using standard input, don't forget to add the `-i` option to the `docker run` command. 28 | 29 | Note that the input must contain all the annotations for each game. The easiest way to ensure this precondition is met is by piping the Mahjong Soul game record data converted with [`cryolite/kanachan.annotate`](../../src/annotation#annotation), as follows: 30 | 31 | ```sh 32 | $ docker run -v /path/to/data:/data:ro --rm cryolite/kanachan.annotate | docker run -i --rm cryolite/kanachan.annotate4rl [OPTION]... 33 | ``` 34 | -------------------------------------------------------------------------------- /src/simulation/angang.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/angang.hpp" 2 | 3 | #include "simulation/hule.hpp" 4 | #include "simulation/zimo.hpp" 5 | #include "simulation/game_log.hpp" 6 | #include "simulation/round_state.hpp" 7 | #include "common/throw.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | namespace { 17 | 18 | using std::placeholders::_1; 19 | 20 | } // namespace `anonymous` 21 | 22 | namespace Kanachan{ 23 | 24 | std::any angang( 25 | Kanachan::RoundState &round_state, std::uint_fast8_t const zimo_tile, 26 | std::uint_fast8_t const encode, Kanachan::GameLog &game_log) 27 | { 28 | if (zimo_tile >= 37u) { 29 | KANACHAN_THROW(_1) << static_cast(zimo_tile); 30 | } 31 | if (encode >= 34u) { 32 | KANACHAN_THROW(_1) << static_cast(encode); 33 | } 34 | 35 | std::uint_fast16_t const action = round_state.onAngang(zimo_tile, encode, game_log); 36 | 37 | if (action == UINT_FAST16_MAX) { 38 | // Si Gang San Le (四槓散了) の成立は打牌直後. 39 | auto zimo = std::bind(&Kanachan::zimo, std::ref(round_state), std::ref(game_log)); 40 | std::function next_step(std::move(zimo)); 41 | return next_step; 42 | } 43 | 44 | if (action == 543u) { 45 | // Qiang Gang (槍槓) 46 | std::uint_fast8_t const zimo_tile_ = UINT_FAST8_MAX; 47 | auto hule = std::bind(&Kanachan::hule, std::ref(round_state), zimo_tile_, std::ref(game_log)); 48 | std::function next_step(std::move(hule)); 49 | return next_step; 50 | } 51 | 52 | KANACHAN_THROW(_1) << action << ": An invalid action on an gang."; 53 | #pragma GCC diagnostic push 54 | #pragma GCC diagnostic ignored "-Wreturn-type" 55 | } 56 | #pragma GCC diagnostic pop 57 | 58 | } // namespace Kanachan 59 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase2/train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from omegaconf import DictConfig 5 | import hydra 6 | import torch 7 | from torch import nn 8 | from kanachan.training.constants import MAX_NUM_ACTION_CANDIDATES 9 | import kanachan.training.bert.phase1.config # type: ignore pylint: disable=unused-import 10 | from kanachan.training.bert.phase1.decoder import Decoder 11 | from kanachan.training.bert.phase1.model import Model 12 | from kanachan.training.bert.phase2.iterator_adaptor import IteratorAdaptor 13 | from kanachan.training.bert import training 14 | 15 | 16 | @hydra.main(version_base=None, config_name='config') 17 | def _main(config: DictConfig) -> None: 18 | _loss_function = nn.MSELoss() 19 | 20 | def loss_function( 21 | prediction: torch.Tensor, index: torch.Tensor, target: torch.Tensor) -> torch.Tensor: 22 | assert prediction.dim() == 2 23 | batch_size = prediction.size(0) 24 | assert prediction.size(1) == MAX_NUM_ACTION_CANDIDATES 25 | assert index.dim() == 1 26 | assert index.size(0) == batch_size 27 | assert target.dim() == 1 28 | assert target.size(0) == batch_size 29 | prediction = prediction[torch.arange(batch_size), index] 30 | return _loss_function(prediction.to(dtype=torch.float32), target.to(dtype=torch.float32)) 31 | 32 | def prediction_function(weights: torch.Tensor) -> torch.Tensor: 33 | assert weights.dim() == 2 34 | assert weights.size(1) == MAX_NUM_ACTION_CANDIDATES 35 | return torch.argmax(weights, dim=1) 36 | 37 | training.main( 38 | config=config, iterator_adaptor_type=IteratorAdaptor, decoder_type=Decoder, 39 | model_type=Model, loss_function=loss_function, prediction_function=prediction_function) 40 | 41 | 42 | if __name__ == '__main__': 43 | _main() # pylint: disable=no-value-for-parameter 44 | sys.exit(0) 45 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase3/train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from omegaconf import DictConfig 5 | import hydra 6 | import torch 7 | from torch import nn 8 | from kanachan.training.constants import MAX_NUM_ACTION_CANDIDATES 9 | import kanachan.training.bert.phase1.config # type: ignore pylint: disable=unused-import 10 | from kanachan.training.bert.phase1.decoder import Decoder 11 | from kanachan.training.bert.phase1.model import Model 12 | from kanachan.training.bert.phase3.iterator_adaptor import IteratorAdaptor 13 | from kanachan.training.bert import training 14 | 15 | 16 | @hydra.main(version_base=None, config_name='config') 17 | def _main(config: DictConfig) -> None: 18 | _loss_function = nn.MSELoss() 19 | 20 | def loss_function( 21 | prediction: torch.Tensor, index: torch.Tensor, target: torch.Tensor) -> torch.Tensor: 22 | assert prediction.dim() == 2 23 | batch_size = prediction.size(0) 24 | assert prediction.size(1) == MAX_NUM_ACTION_CANDIDATES 25 | assert index.dim() == 1 26 | assert index.size(0) == batch_size 27 | assert target.dim() == 1 28 | assert target.size(0) == batch_size 29 | prediction = prediction[torch.arange(batch_size), index] 30 | return _loss_function(prediction.to(dtype=torch.float32), target.to(dtype=torch.float32)) 31 | 32 | def prediction_function(weights: torch.Tensor) -> torch.Tensor: 33 | assert weights.dim() == 2 34 | assert weights.size(1) == MAX_NUM_ACTION_CANDIDATES 35 | return torch.argmax(weights, dim=1) 36 | 37 | training.main( 38 | config=config, iterator_adaptor_type=IteratorAdaptor, decoder_type=Decoder, 39 | model_type=Model, loss_function=loss_function, prediction_function=prediction_function) 40 | 41 | 42 | if __name__ == '__main__': 43 | _main() # pylint: disable=no-value-for-parameter 44 | sys.exit(0) 45 | -------------------------------------------------------------------------------- /prerequisites/prologue.sh: -------------------------------------------------------------------------------- 1 | # This Bash file is not designed to be called directly, but rather is read by 2 | # `source` Bash builtin command in the very beginning of another Bash script. 3 | 4 | PS4='+${BASH_SOURCE[0]}:$LINENO: ' 5 | if [[ -t 1 ]] && type -t tput >/dev/null; then 6 | if (( "$(tput colors)" == 256 )); then 7 | PS4='$(tput setaf 10)'$PS4'$(tput sgr0)' 8 | else 9 | PS4='$(tput setaf 2)'$PS4'$(tput sgr0)' 10 | fi 11 | fi 12 | 13 | new_args=() 14 | while (( $# > 0 )); do 15 | arg="$1" 16 | shift 17 | case "$arg" in 18 | --debug) 19 | debug=yes 20 | new_args+=("$@") 21 | break 22 | ;; 23 | --) 24 | new_args+=(-- "$@") 25 | break 26 | ;; 27 | *) 28 | new_args+=("$arg") 29 | ;; 30 | esac 31 | done 32 | set -- ${new_args[@]+"${new_args[@]}"} 33 | unset new_args 34 | if [[ ${debug-no} == yes || ${VERBOSE+DEFINED} == DEFINED ]]; then 35 | set -x 36 | fi 37 | unset debug 38 | 39 | function print_error_message () 40 | { 41 | if [[ -t 2 ]] && type -t tput >/dev/null; then 42 | if (( "$(tput colors)" == 256 )); then 43 | echo "$(tput setaf 9)$1$(tput sgr0)" >&2 44 | else 45 | echo "$(tput setaf 1)$1$(tput sgr0)" >&2 46 | fi 47 | else 48 | echo "$1" >&2 49 | fi 50 | } 51 | 52 | function die_with_logic_error () 53 | { 54 | set +x 55 | print_error_message "$1: error: A logic error." 56 | exit 1 57 | } 58 | 59 | function die_with_user_error () 60 | { 61 | set +x 62 | print_error_message "$1: error: $2" 63 | print_error_message "Try \`$1 --help' for more information." 64 | exit 1 65 | } 66 | 67 | function die_with_runtime_error () 68 | { 69 | set +x 70 | print_error_message "$1: error: $2" 71 | exit 1 72 | } 73 | 74 | rollback_stack=() 75 | 76 | function push_rollback_command () 77 | { 78 | rollback_stack+=("$1") 79 | } 80 | 81 | function rollback () 82 | { 83 | for (( i = ${#rollback_stack[@]} - 1; i >= 0; --i )); do 84 | eval "${rollback_stack[$i]}" 85 | done 86 | rollback_stack=() 87 | } 88 | 89 | trap rollback EXIT 90 | -------------------------------------------------------------------------------- /src/simulation/round_result.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/round_result.hpp" 2 | 3 | #include "common/assert.hpp" 4 | #include "common/throw.hpp" 5 | #include 6 | #include 7 | 8 | 9 | namespace{ 10 | 11 | using std::placeholders::_1; 12 | 13 | } 14 | 15 | namespace Kanachan{ 16 | 17 | RoundResult::RoundResult() 18 | : type_(UINT_FAST8_MAX) 19 | , in_lizhi_(false) 20 | , has_fulu_(false) 21 | , round_delta_score_(INT_FAST32_MAX) 22 | , round_score_(INT_FAST32_MAX) 23 | {} 24 | 25 | void RoundResult::setType(std::uint_fast8_t const type) 26 | { 27 | if (type > 14u) { 28 | KANACHAN_THROW(_1) << static_cast(type); 29 | } 30 | type_ = type; 31 | } 32 | 33 | void RoundResult::setInLizhi(bool const in_lizhi) 34 | { 35 | in_lizhi_ = in_lizhi; 36 | } 37 | 38 | void RoundResult::setHasFulu(bool const has_fulu) 39 | { 40 | has_fulu_ = has_fulu; 41 | } 42 | 43 | void RoundResult::setRoundDeltaScore(std::int_fast32_t const round_delta_score) 44 | { 45 | round_delta_score_ = round_delta_score; 46 | } 47 | 48 | void RoundResult::setRoundScore(std::int_fast32_t const round_score) 49 | { 50 | round_score_ = round_score; 51 | } 52 | 53 | std::uint_fast8_t RoundResult::getType() const noexcept 54 | { 55 | KANACHAN_ASSERT((type_ <= 14u || type_ == UINT_FAST8_MAX)); 56 | return type_; 57 | } 58 | 59 | bool RoundResult::getInLizhi() const noexcept 60 | { 61 | return in_lizhi_; 62 | } 63 | 64 | bool RoundResult::getHasFulu() const noexcept 65 | { 66 | return has_fulu_; 67 | } 68 | 69 | std::int_fast32_t RoundResult::getRoundDeltaScore() const noexcept 70 | { 71 | if (round_delta_score_ == INT_FAST32_MAX) { 72 | KANACHAN_THROW("An uninitialized object."); 73 | } 74 | return round_delta_score_; 75 | } 76 | 77 | std::int_fast32_t RoundResult::getRoundScore() const noexcept 78 | { 79 | if (round_score_ == INT_FAST32_MAX) { 80 | KANACHAN_THROW("An uninitialized object."); 81 | } 82 | return round_score_; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /kanachan/training/common.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | import logging 4 | from typing import NoReturn, Optional, Union 5 | import torch 6 | from torch import nn 7 | from torch.utils.data import IterableDataset 8 | 9 | 10 | def initialize_logging( 11 | experiment_path: Path, local_rank: Optional[int]) -> None: 12 | fmt = '%(asctime)s %(filename)s:%(lineno)d:%(levelname)s: %(message)s' 13 | if local_rank is None: 14 | path = experiment_path / 'training.log' 15 | else: 16 | path = experiment_path / f'training.{local_rank}.log' 17 | file_handler = logging.FileHandler(path, encoding='UTF-8') 18 | if local_rank is None or local_rank == 0: 19 | console_handler = logging.StreamHandler() 20 | handlers = (console_handler, file_handler) 21 | else: 22 | handlers = (file_handler,) 23 | logging.basicConfig(format=fmt, level=logging.INFO, handlers=handlers) 24 | 25 | 26 | def load_state_dict(module: nn.Module, state_dict: dict) -> None: 27 | fixed_state_dict = {} 28 | for k, v in state_dict.items(): 29 | k = re.sub('^.*?__', '', k) 30 | k = re.sub('\\..*?__', '.', k) 31 | k = re.sub('^module\\.', '', k) 32 | fixed_state_dict[k] = v 33 | module.load_state_dict(fixed_state_dict) 34 | 35 | 36 | class Dataset(IterableDataset): 37 | def __init__(self, path: Union[str, Path], iterator_adaptor) -> None: 38 | super(Dataset, self).__init__() 39 | if isinstance(path, str): 40 | path = Path(path) 41 | if not path.exists(): 42 | raise RuntimeError(f'{path}: does not exist.') 43 | self.__path = path 44 | self.__iterator_adaptor = iterator_adaptor 45 | 46 | def __iter__(self): 47 | return self.__iterator_adaptor(self.__path) 48 | 49 | def __getitem__(self, index) -> NoReturn: 50 | raise NotImplementedError('Not implemented.') 51 | 52 | 53 | def get_gradient(model: nn.Module) -> torch.Tensor: 54 | gradient = [param.grad.view(-1) for param in model.parameters() if param.grad is not None] 55 | return torch.cat(gradient) 56 | 57 | 58 | def is_gradient_nan(model: nn.Module) -> torch.Tensor: 59 | gradient = get_gradient(model) 60 | return torch.any(torch.isnan(gradient)) 61 | -------------------------------------------------------------------------------- /kanachan/training/ilql/policy_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | import torch 5 | from torch import nn 6 | from kanachan.training.constants import ( 7 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES,) 8 | from kanachan.training.bert.encoder import Encoder 9 | 10 | 11 | class PolicyDecoder(nn.Module): 12 | def __init__( 13 | self, *, dimension: int, dim_final_feedforward: int, 14 | activation_function: str, dropout: float, **_) -> None: 15 | super(PolicyDecoder, self).__init__() 16 | 17 | # The final layer is position-wise feed-forward network. 18 | self.semifinal_linear = nn.Linear(dimension, dim_final_feedforward) 19 | if activation_function == 'relu': 20 | self.semifinal_activation = nn.ReLU() 21 | elif activation_function == 'gelu': 22 | self.semifinal_activation = nn.GELU() 23 | else: 24 | raise ValueError( 25 | f'{activation_function}: invalid activation function') 26 | self.semifinal_dropout = nn.Dropout(p=dropout) 27 | self.final_linear = nn.Linear(dim_final_feedforward, 1) 28 | 29 | def forward(self, x): 30 | candidates, encode = x 31 | 32 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 33 | decode = self.semifinal_linear(encode) 34 | decode = self.semifinal_activation(decode) 35 | decode = self.semifinal_dropout(decode) 36 | prediction = self.final_linear(decode) 37 | prediction = torch.squeeze(prediction, dim=2) 38 | prediction = torch.where(candidates < NUM_TYPES_OF_ACTIONS, prediction, -math.inf) 39 | assert prediction.dim() == 2 40 | assert prediction.size(0) == candidates.size(0) 41 | assert prediction.size(1) == MAX_NUM_ACTION_CANDIDATES 42 | 43 | return prediction 44 | 45 | 46 | class PolicyModel(nn.Module): 47 | def __init__(self, encoder: Encoder, decoder: PolicyDecoder) -> None: 48 | super(PolicyModel, self).__init__() 49 | self.encoder = encoder 50 | self.decoder = decoder 51 | 52 | def mode(self, mode: str) -> None: 53 | if mode not in ('training', 'validation', 'prediction'): 54 | raise ValueError(mode) 55 | 56 | def forward(self, x) -> torch.Tensor: 57 | encode = self.encoder(x) 58 | decode = self.decoder((x[3], encode)) 59 | return decode 60 | -------------------------------------------------------------------------------- /src/common/assert.cpp: -------------------------------------------------------------------------------- 1 | #include "assert.hpp" 2 | 3 | #if defined(KANACHAN_ENABLE_ASSERT) 4 | 5 | #include "throw.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | #if defined(KANACHAN_WITH_COVERAGE) 18 | 19 | extern "C" void __gcov_flush(); 20 | 21 | #endif // defined(KANACHAN_WITH_COVERAGE) 22 | 23 | namespace Kanachan{ 24 | 25 | AssertionFailure::AssertionFailure(std::string const &error_message) 26 | : std::logic_error(error_message) 27 | {} 28 | 29 | AssertionFailure::AssertionFailure(std::string_view error_message) 30 | : AssertionFailure(std::string(error_message)) 31 | {} 32 | 33 | namespace Detail_{ 34 | 35 | AssertMessenger::AssertMessenger(char const *file_name, int line_number, char const *function_name, 36 | char const *expression, boost::stacktrace::stacktrace &&stacktrace) 37 | : oss_(), 38 | file_name_(file_name), 39 | line_number_(line_number), 40 | function_name_(function_name), 41 | stacktrace_(std::move(stacktrace)) 42 | { 43 | oss_ << file_name_ << ':' << line_number_ << ": " << function_name_ << ": " 44 | << "Assertion `" << expression << "' failed.\n"; 45 | } 46 | 47 | AssertMessenger &AssertMessenger::operator<<(std::ostream &(*pf)(std::ostream &)) 48 | { 49 | oss_ << pf; 50 | return *this; 51 | } 52 | 53 | AssertMessenger &AssertMessenger::operator<<(std::ios &(*pf)(std::ios &)) 54 | { 55 | oss_ << pf; 56 | return *this; 57 | } 58 | 59 | AssertMessenger &AssertMessenger::operator<<(std::ios_base &(*pf)(std::ios_base &)) 60 | { 61 | oss_ << pf; 62 | return *this; 63 | } 64 | 65 | AssertMessenger::operator int() const noexcept 66 | { 67 | return 0; 68 | } 69 | 70 | [[noreturn]] AssertMessenger::~AssertMessenger() noexcept(false) 71 | { 72 | throw boost::enable_error_info(Kanachan::AssertionFailure(oss_.str())) 73 | << boost::throw_file(file_name_) 74 | << boost::throw_line(line_number_) 75 | << boost::throw_function(function_name_) 76 | << Kanachan::StackTraceErrorInfo(std::move(stacktrace_)); 77 | } 78 | 79 | } // namespace Detail_ 80 | 81 | } // namespace Kanachan 82 | 83 | #endif // defined(KANACHAN_ENABLE_ASSERT) 84 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/decoder.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import OrderedDict 3 | import torch 4 | from torch import nn 5 | from kanachan.training.constants import ( 6 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES, ENCODER_WIDTH 7 | ) 8 | 9 | 10 | class Decoder(nn.Module): 11 | def __init__( 12 | self, *, dimension: int, dim_feedforward: int, activation_function: str, dropout: float, 13 | num_layers: int, device: str, dtype: torch.dtype) -> None: 14 | if dimension < 1: 15 | raise ValueError(dimension) 16 | if dim_feedforward < 1: 17 | raise ValueError(dim_feedforward) 18 | if activation_function not in ('relu', 'gelu'): 19 | raise ValueError(activation_function) 20 | if dropout < 0.0 or 1.0 <= dropout: 21 | raise ValueError(dropout) 22 | if num_layers < 1: 23 | raise ValueError(num_layers) 24 | 25 | super(Decoder, self).__init__() 26 | 27 | layers = OrderedDict() 28 | for i in range(num_layers - 1): 29 | layers[f'layer{i}'] = nn.Linear( 30 | dimension if i == 0 else dim_feedforward, dim_feedforward, 31 | device=device, dtype=dtype) 32 | if activation_function == 'relu': 33 | layers[f'activation{i}'] = nn.ReLU() 34 | elif activation_function == 'gelu': 35 | layers[f'activation{i}'] = nn.GELU() 36 | else: 37 | raise ValueError(activation_function) 38 | layers[f'dropout{i}'] = nn.Dropout(p=dropout) 39 | suffix = '' if num_layers == 1 else str(num_layers - 1) 40 | layers['layer' + suffix] = nn.Linear( 41 | dimension if num_layers == 1 else dim_feedforward, 1, device=device, dtype=dtype) 42 | self.layers = nn.Sequential(layers) 43 | 44 | def forward(self, candidates: torch.Tensor, encode: torch.Tensor) -> torch.Tensor: 45 | assert candidates.dim() == 2 46 | assert candidates.size(1) == MAX_NUM_ACTION_CANDIDATES 47 | assert encode.dim() == 3 48 | assert encode.size(1) == ENCODER_WIDTH 49 | assert candidates.size(0) == encode.size(0) 50 | 51 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 52 | weights = self.layers(encode) 53 | weights = torch.squeeze(weights, dim=2) 54 | 55 | weights = torch.where(candidates < NUM_TYPES_OF_ACTIONS, weights, -math.inf) 56 | 57 | return weights 58 | -------------------------------------------------------------------------------- /prerequisites/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | SHELL ["/bin/bash", "-c"] 4 | 5 | RUN apt-get update && \ 6 | apt-get -y dist-upgrade && \ 7 | apt-get -y install \ 8 | autoconf \ 9 | bzip2 \ 10 | curl \ 11 | g++ \ 12 | git \ 13 | gpg \ 14 | libgmp-dev \ 15 | libtool \ 16 | libssl-dev \ 17 | make \ 18 | protobuf-compiler \ 19 | python3-dev \ 20 | xz-utils && \ 21 | apt-get clean && rm -rf /var/lib/apt/lists/* && \ 22 | useradd -ms /bin/bash ubuntu && \ 23 | mkdir /opt/kanachan-prerequisites && \ 24 | chown ubuntu:ubuntu /opt/kanachan-prerequisites 25 | 26 | COPY --chown=ubuntu . /opt/kanachan-prerequisites 27 | 28 | RUN /opt/kanachan-prerequisites/gcc/install --debug 29 | 30 | ENV LD_LIBRARY_PATH=/usr/local/lib64:/usr/local/lib 31 | 32 | RUN /opt/kanachan-prerequisites/libbacktrace/install --debug 33 | 34 | RUN /opt/kanachan-prerequisites/cmake/install --debug 35 | 36 | RUN /opt/kanachan-prerequisites/googletest/install --debug 37 | 38 | RUN echo 'import toolset : using ; using python : : /usr/bin/python3 ;' >/root/user-config.jam 39 | RUN /opt/kanachan-prerequisites/boost/download --debug --source-dir /usr/local/src/boost 40 | RUN /opt/kanachan-prerequisites/boost/build --debug --source-dir /usr/local/src/boost -- \ 41 | -d+2 --with-headers --with-stacktrace --with-python --build-type=complete --layout=tagged \ 42 | toolset=gcc variant=debug threading=multi link=shared runtime-link=shared \ 43 | cxxflags=-D_GLIBCXX_DEBUG cxxflags=-D_GLIBCXX_DEBUG_PEDANTIC \ 44 | cflags=-fsanitize=address cxxflags=-fsanitize=address linkflags=-fsanitize=address \ 45 | cflags=-fsanitize=undefined cxxflags=-fsanitize=undefined linkflags=-fsanitize=undefined 46 | RUN /opt/kanachan-prerequisites/boost/build --debug --source-dir /usr/local/src/boost -- \ 47 | -d+2 --with-headers --with-stacktrace --with-python --build-type=complete --layout=tagged \ 48 | toolset=gcc variant=release threading=multi link=shared runtime-link=shared 49 | 50 | USER ubuntu 51 | 52 | RUN mkdir -p /home/ubuntu/.local/src && \ 53 | pushd /home/ubuntu/.local/src && \ 54 | git clone 'https://github.com/s-yata/marisa-trie.git' && \ 55 | popd 56 | 57 | RUN mkdir -p /home/ubuntu/.local/src && \ 58 | pushd /home/ubuntu/.local/src && \ 59 | git clone 'https://github.com/tomohxx/shanten-number' && \ 60 | pushd shanten-number && \ 61 | tar xzvf index.tar.gz && \ 62 | popd && \ 63 | popd 64 | 65 | USER root 66 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "/home/vscode/.local/lib/python3.10/site-packages" 4 | ], 5 | "python.linting.pylintArgs": [ 6 | "--generated-members=numpy.* ,torch.*" 7 | ], 8 | "explorer.excludeGitIgnore": true, 9 | "files.associations": { 10 | "cctype": "cpp", 11 | "clocale": "cpp", 12 | "cmath": "cpp", 13 | "cstdarg": "cpp", 14 | "cstddef": "cpp", 15 | "cstdio": "cpp", 16 | "cstdlib": "cpp", 17 | "cstring": "cpp", 18 | "ctime": "cpp", 19 | "cwchar": "cpp", 20 | "cwctype": "cpp", 21 | "any": "cpp", 22 | "array": "cpp", 23 | "atomic": "cpp", 24 | "strstream": "cpp", 25 | "bit": "cpp", 26 | "*.tcc": "cpp", 27 | "bitset": "cpp", 28 | "chrono": "cpp", 29 | "codecvt": "cpp", 30 | "compare": "cpp", 31 | "complex": "cpp", 32 | "concepts": "cpp", 33 | "condition_variable": "cpp", 34 | "cstdint": "cpp", 35 | "deque": "cpp", 36 | "map": "cpp", 37 | "set": "cpp", 38 | "string": "cpp", 39 | "unordered_map": "cpp", 40 | "unordered_set": "cpp", 41 | "vector": "cpp", 42 | "exception": "cpp", 43 | "algorithm": "cpp", 44 | "functional": "cpp", 45 | "iterator": "cpp", 46 | "memory": "cpp", 47 | "memory_resource": "cpp", 48 | "numeric": "cpp", 49 | "optional": "cpp", 50 | "random": "cpp", 51 | "ratio": "cpp", 52 | "regex": "cpp", 53 | "source_location": "cpp", 54 | "string_view": "cpp", 55 | "system_error": "cpp", 56 | "tuple": "cpp", 57 | "type_traits": "cpp", 58 | "utility": "cpp", 59 | "fstream": "cpp", 60 | "initializer_list": "cpp", 61 | "iomanip": "cpp", 62 | "iosfwd": "cpp", 63 | "iostream": "cpp", 64 | "istream": "cpp", 65 | "limits": "cpp", 66 | "mutex": "cpp", 67 | "new": "cpp", 68 | "numbers": "cpp", 69 | "ostream": "cpp", 70 | "ranges": "cpp", 71 | "semaphore": "cpp", 72 | "span": "cpp", 73 | "sstream": "cpp", 74 | "stdexcept": "cpp", 75 | "stop_token": "cpp", 76 | "streambuf": "cpp", 77 | "thread": "cpp", 78 | "cinttypes": "cpp", 79 | "typeindex": "cpp", 80 | "typeinfo": "cpp", 81 | "variant": "cpp" 82 | } 83 | } -------------------------------------------------------------------------------- /kanachan/nn/linear_decoder.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import OrderedDict 3 | import torch 4 | import torch.nn as nn 5 | from kanachan.training.constants import ( 6 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES, ENCODER_WIDTH) 7 | 8 | 9 | class LinearDecoder(nn.Module): 10 | def __init__( 11 | self, *, dimension: int, dim_feedforward: int, activation_function: str, dropout: float, 12 | num_layers: int, device: torch.device, dtype: torch.dtype) -> None: 13 | if dimension < 1: 14 | raise ValueError(dimension) 15 | if dim_feedforward < 1: 16 | raise ValueError(dim_feedforward) 17 | if activation_function not in ('relu', 'gelu',): 18 | raise ValueError(activation_function) 19 | if dropout < 0.0 or 1.0 <= dropout: 20 | raise ValueError(dropout) 21 | if num_layers < 1: 22 | raise ValueError(num_layers) 23 | 24 | super().__init__() 25 | 26 | layers = OrderedDict() 27 | for i in range(num_layers - 1): 28 | layers[f'layer{i}'] = nn.Linear( 29 | dimension if i == 0 else dim_feedforward, dim_feedforward, 30 | device=device, dtype=dtype) 31 | if activation_function == 'relu': 32 | layers[f'activation{i}'] = nn.ReLU() 33 | elif activation_function == 'gelu': 34 | layers[f'activation{i}'] = nn.GELU() 35 | else: 36 | raise ValueError(activation_function) 37 | layers[f'dropout{i}'] = nn.Dropout(p=dropout) 38 | suffix = '' if num_layers == 1 else str(num_layers - 1) 39 | layers['layer' + suffix] = nn.Linear( 40 | dimension if num_layers == 1 else dim_feedforward, 1, device=device, dtype=dtype) 41 | self.layers = nn.Sequential(layers) 42 | 43 | def forward(self, candidates: torch.Tensor, encode: torch.Tensor) -> torch.Tensor: 44 | assert candidates.dim() == 2 45 | assert encode.dim() == 3 46 | assert encode.size(1) == ENCODER_WIDTH 47 | assert candidates.size(0) == encode.size(0) 48 | 49 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 50 | output: torch.Tensor = self.layers(encode) 51 | output = torch.squeeze(output, dim=2) 52 | assert output.dim() == 2 53 | assert output.size(0) == candidates.size(0) 54 | assert output.size(1) == MAX_NUM_ACTION_CANDIDATES 55 | output = torch.where(candidates < NUM_TYPES_OF_ACTIONS, output, -math.inf) 56 | 57 | return output 58 | -------------------------------------------------------------------------------- /src/simulation/game_log.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_GAME_LOG_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_GAME_LOG_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/round_result.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan{ 13 | 14 | class GameLog 15 | { 16 | private: 17 | class Decision_ 18 | { 19 | public: 20 | Decision_( 21 | std::vector &&sparse, std::vector &&numeric, 22 | std::vector &&progression, std::vector &&candidates, 23 | std::uint_fast16_t action); 24 | 25 | std::vector const &getSparse() const noexcept; 26 | 27 | std::vector const &getNumeric() const noexcept; 28 | 29 | std::vector const &getProgression() const noexcept; 30 | 31 | std::vector const &getCandidates() const noexcept; 32 | 33 | std::uint_fast8_t getActionIndex() const noexcept; 34 | 35 | private: 36 | std::vector sparse_; 37 | std::vector numeric_; 38 | std::vector progression_; 39 | std::vector candidates_; 40 | std::uint_fast8_t action_index_; 41 | }; 42 | 43 | using Decisions_ = std::vector; 44 | 45 | using RoundResults_ = std::vector; 46 | 47 | public: 48 | GameLog(); 49 | 50 | GameLog(GameLog const &) = delete; 51 | 52 | GameLog &operator=(GameLog const &) = delete; 53 | 54 | void onBeginningOfRound(); 55 | 56 | void onDecision( 57 | std::uint_fast8_t seat, std::vector &&sparse, 58 | std::vector &&numeric, std::vector &&progression, 59 | std::vector &&candidates, std::uint_fast16_t action); 60 | 61 | void onEndOfRound(std::array const &round_results); 62 | 63 | void onEndOfGame(std::array const &scores); 64 | 65 | void setWithProposedModel(std::array const &with_proposed_model); 66 | 67 | boost::python::list getResult() const; 68 | 69 | boost::python::dict getEpisode(std::uint_fast8_t seat) const; 70 | 71 | private: 72 | std::array decisions_; 73 | std::array round_results_; 74 | std::array game_scores_; 75 | std::array with_proposed_model_; 76 | }; 77 | 78 | } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /src/simulation/game_state.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_GAME_STATE_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_GAME_STATE_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/decision_maker.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan{ 13 | 14 | class GameState 15 | { 16 | public: 17 | using Seat = std::pair>; 18 | 19 | public: 20 | GameState( 21 | std::uint_fast8_t room, bool dong_feng_zhan, std::array const &seats, 22 | std::stop_token stop_token); 23 | 24 | GameState(GameState const &) = delete; 25 | 26 | GameState(GameState &&) = delete; 27 | 28 | GameState &operator=(GameState const &) = delete; 29 | 30 | GameState &operator=(GameState &&) = delete; 31 | 32 | public: 33 | std::uint_fast8_t getRoom() const; 34 | 35 | bool isDongfengZhan() const; 36 | 37 | std::uint_fast8_t getPlayerGrade(std::uint_fast8_t seat) const; 38 | 39 | std::uint_fast8_t getChang() const; 40 | 41 | std::uint_fast8_t getJu() const; 42 | 43 | std::uint_fast8_t getBenChang() const; 44 | 45 | std::uint_fast8_t getNumLizhiDeposits() const; 46 | 47 | std::int_fast32_t getPlayerScore(std::uint_fast8_t seat) const; 48 | 49 | std::uint_fast8_t getPlayerRanking(std::uint_fast8_t seat) const; 50 | 51 | public: 52 | std::uint_fast16_t selectAction( 53 | std::uint_fast8_t seat, std::vector &&sparse, 54 | std::vector &&numeric, std::vector &&progression, 55 | std::vector &&candidates) const; 56 | 57 | public: 58 | void onSuccessfulLizhi(std::uint_fast8_t seat); 59 | 60 | void addPlayerScore(std::uint_fast8_t seat, std::int_fast32_t score); 61 | 62 | enum struct RoundEndStatus : std::uint_fast8_t 63 | { 64 | hule = 0u, 65 | huangpai_pingju = 1u, 66 | liuju = 2u 67 | }; // enum struct RoundEndStatus 68 | 69 | void onLianzhuang(RoundEndStatus round_end_status); 70 | 71 | void onLunzhuang(RoundEndStatus round_end_status); 72 | 73 | private: 74 | std::uint_fast8_t room_; 75 | bool dong_feng_zhan_; 76 | std::array seats_; 77 | std::uint_fast8_t chang_ = 0u; 78 | std::uint_fast8_t ju_ = 0u; 79 | std::uint_fast8_t ben_chang_ = 0u; 80 | std::uint_fast8_t lizhi_deposits_ = 0u; 81 | std::array scores_{ 25000, 25000, 25000, 25000 }; 82 | std::stop_token stop_token_; 83 | }; // class GameState 84 | 85 | } // namespace Kanachan 86 | 87 | #endif // !defined(KANACHAN_SIMULATION_GAME_STATE_HPP_INCLUDE_GUARD) 88 | -------------------------------------------------------------------------------- /kanachan/training/iql/value_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from collections import OrderedDict 4 | import torch 5 | from torch import nn 6 | from kanachan.training.constants import ( 7 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES,) 8 | from kanachan.training.bert.encoder import Encoder 9 | 10 | 11 | class ValueDecoder(nn.Module): 12 | def __init__( 13 | self, *, dimension: int, dim_feedforward: int, activation_function: str, dropout: float, 14 | num_layers: int, device: torch.device, dtype: torch.dtype) -> None: 15 | if num_layers <= 0: 16 | raise ValueError(num_layers) 17 | 18 | super(ValueDecoder, self).__init__() 19 | 20 | layers = OrderedDict() 21 | for i in range(num_layers - 1): 22 | layers[f'layer{i}'] = nn.Linear( 23 | dimension if i == 0 else dim_feedforward, dim_feedforward, 24 | device=device, dtype=dtype) 25 | if activation_function == 'relu': 26 | layers[f'activation{i}'] = nn.ReLU() 27 | elif activation_function == 'gelu': 28 | layers[f'activation{i}'] = nn.GELU() 29 | else: 30 | raise ValueError(activation_function) 31 | layers[f'dropout{i}'] = nn.Dropout(p=dropout) 32 | layers[f'layer{num_layers - 1}'] = nn.Linear( 33 | dimension if num_layers == 1 else dim_feedforward, 1, device=device, dtype=dtype) 34 | self.layers = nn.Sequential(layers) 35 | 36 | def forward(self, candidates: torch.Tensor, encode: torch.Tensor) -> torch.Tensor: 37 | assert candidates.dim() == 2 38 | assert encode.dim() == 3 39 | assert candidates.size(0) == encode.size(0) 40 | 41 | mask = (candidates == NUM_TYPES_OF_ACTIONS) 42 | 43 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 44 | value: torch.Tensor = self.layers(encode) 45 | value = torch.squeeze(value, dim=2) 46 | value = value[mask] 47 | assert value.dim() == 1 48 | assert value.size(0) == candidates.size(0) 49 | 50 | # Set `V` of the terminal state to `0.0`. 51 | value = torch.where(candidates[:, 0] != NUM_TYPES_OF_ACTIONS, value, 0.0) 52 | 53 | return value 54 | 55 | 56 | class ValueModel(nn.Module): 57 | def __init__(self, encoder: Encoder, decoder: ValueDecoder) -> None: 58 | super(ValueModel, self).__init__() 59 | self.encoder = encoder 60 | self.decoder = decoder 61 | 62 | def forward( 63 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 64 | candidates: torch.Tensor) -> torch.Tensor: 65 | encode = self.encoder(sparse, numeric, progression, candidates) 66 | prediction = self.decoder(candidates, encode) 67 | return prediction 68 | -------------------------------------------------------------------------------- /mjai.app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV LD_LIBRARY_PATH=/usr/local/lib64:/usr/local/lib 4 | ENV PATH=/usr/local/bin${PATH:+:$PATH} 5 | 6 | RUN apt-get update && \ 7 | apt-get -y dist-upgrade && \ 8 | DEBIAN_FRONTEND=noninteractive apt-get -y install \ 9 | autoconf \ 10 | automake \ 11 | curl \ 12 | g++ \ 13 | git \ 14 | gnupg \ 15 | libssl-dev \ 16 | libtool \ 17 | locales \ 18 | locales-all \ 19 | m4 \ 20 | make \ 21 | xz-utils \ 22 | zip \ 23 | zlib1g-dev && \ 24 | apt-get clean && rm -rf /var/lib/apt/lists/* && \ 25 | locale-gen en_US.UTF-8 ja_JP.UTF-8 && \ 26 | mkdir -p /opt/src && \ 27 | (cd /opt && git clone 'https://github.com/Cryolite/prerequisites.git') && \ 28 | /opt/prerequisites/cmake/install --debug && \ 29 | (cd /opt/src && curl -fLsSo Python-3.10.7.tar.xz 'https://www.python.org/ftp/python/3.10.7/Python-3.10.7.tar.xz') && \ 30 | (cd /opt/src && tar xf Python-3.10.7.tar.xz) && \ 31 | rm /opt/src/Python-3.10.7.tar.xz && \ 32 | (cd /opt/src/Python-3.10.7 && ./configure --enable-optimizations --with-lto) && \ 33 | (cd /opt/src/Python-3.10.7 && make -j) && \ 34 | (cd /opt/src/Python-3.10.7 && make install) && \ 35 | rm -rf /opt/src/Python-3.10.7 && \ 36 | echo 'using python : : /usr/local/bin/python3 ;' >/root/user-config.jam && \ 37 | /opt/prerequisites/boost/download --debug --source-dir /opt/src/boost && \ 38 | /opt/prerequisites/boost/build --debug \ 39 | --source-dir /opt/src/boost -- \ 40 | -d+2 --with-headers --with-stacktrace --with-python --build-type=complete --layout=tagged \ 41 | toolset=gcc variant=release threading=multi link=shared runtime-link=shared && \ 42 | (mkdir -p /opt/src && cd /opt/src && curl -fLsSo protobuf-cpp-3.21.7.tar.gz 'https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protobuf-cpp-3.21.7.tar.gz') && \ 43 | (cd /opt/src && tar xf protobuf-cpp-3.21.7.tar.gz) && \ 44 | rm /opt/src/protobuf-cpp-3.21.7.tar.gz && \ 45 | (cd /opt/src/protobuf-3.21.7 && ./configure) && \ 46 | (cd /opt/src/protobuf-3.21.7 && make -j) && \ 47 | (cd /opt/src/protobuf-3.21.7 && make install) && \ 48 | rm -rf /opt/src/protobuf-3.21.7 && \ 49 | (mkdir -p /opt/src && cd /opt/src && git clone 'https://github.com/s-yata/marisa-trie.git') && \ 50 | (cd /opt/src/marisa-trie && autoreconf -i) && \ 51 | (cd /opt/src/marisa-trie && CFLAGS='-DNDEBUG -O3 -flto' CXXFLAGS='-DNDEBUG -O3 -flto' ./configure --enable-native-code --disable-static) && \ 52 | (cd /opt/src/marisa-trie && make -j) && \ 53 | (cd /opt/src/marisa-trie && make install) && \ 54 | rm -rf /opt/src/marisa-trie && \ 55 | (mkdir -p /opt/src && cd /opt/src && git clone 'https://github.com/tomohxx/shanten-number') && \ 56 | (cd /opt/src/shanten-number && tar xf index.tar.gz) && \ 57 | (mkdir -p /opt/src && cd /opt/src && git clone 'https://github.com/MahjongRepository/mahjong' -b v1.1.11) 58 | -------------------------------------------------------------------------------- /mjai.app/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | . prerequisites/prologue.sh 5 | 6 | docker build --pull -f mjai.app/Dockerfile -t cryolite/kanachan.mjai-app . 7 | 8 | container_id=$(docker run -d --rm cryolite/kanachan.mjai-app sleep infinity) 9 | push_rollback_command "docker stop $container_id" 10 | 11 | docker exec -it $container_id mkdir -p /opt/kanachan 12 | docker cp . ${container_id}:/opt/kanachan 13 | docker exec -it $container_id rm -rf /opt/kanachan/build 14 | 15 | docker exec -it $container_id bash -c 'cd /opt/kanachan/src/common && protoc -I. --cpp_out=. mahjongsoul.proto' 16 | 17 | docker exec -it $container_id mkdir -p /opt/kanachan/build 18 | docker exec -it $container_id bash -c \ 19 | 'cd /opt/kanachan/build && \ 20 | cmake \ 21 | -DPYTHON_VERSION="$(python3 -V | sed -e '\''s@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@'\'')" \ 22 | -DPYTHON_INCLUDE_PATH=/usr/local/include/python"$(python3 -V | sed -e '\''s@^Python[[:space:]]\{1,\}\([[:digit:]]\{1,\}\.[[:digit:]]\{1,\}\)\.[[:digit:]]\{1,\}@\1@'\'')" \ 23 | -DMARISA_TRIE_ROOT=/usr/local \ 24 | -DSHANTEN_NUMBER_SOURCE_PATH=/opt/src/shanten-number \ 25 | -DCMAKE_BUILD_TYPE=Release \ 26 | -DCMAKE_BUILD_RPATH=/workspace/.local/lib \ 27 | ..' 28 | docker exec -it $container_id bash -c 'cd /opt/kanachan/build && VERBOSE=1 make -j make_trie xiangting_calculator' 29 | docker exec -it $container_id /opt/kanachan/build/src/xiangting/make_trie /opt/src/shanten-number /opt/kanachan/mjai.app 30 | 31 | docker exec -it $container_id mkdir -p /opt/kanachan/mjai.app/.local/lib 32 | docker exec -it $container_id bash -c 'cp -f /usr/local/lib/libboost_stacktrace_backtrace* /opt/kanachan/mjai.app/.local/lib' 33 | docker exec -it $container_id bash -c 'cp -f /usr/local/lib/libboost_python* /opt/kanachan/mjai.app/.local/lib' 34 | docker exec -it $container_id bash -c 'cp -f /usr/local/lib/libmarisa.so* /opt/kanachan/mjai.app/.local/lib' 35 | docker exec -it $container_id cp -rf /opt/src/mahjong/mahjong /opt/kanachan/mjai.app 36 | docker exec -it $container_id bash -c 'cd /opt/kanachan && cp -f build/src/simulation/libxiangting_calculator.so mjai.app/xiangting_calculator/_xiangting_calculator.so' 37 | docker exec -it $container_id cp -rf /opt/kanachan/kanachan /opt/kanachan/mjai.app 38 | 39 | docker cp "$1" ${container_id}:/opt/kanachan/mjai.app/model.kanachan 40 | 41 | docker exec -it $container_id bash -c 'cd /opt/kanachan/mjai.app && cp -f config.json.orig config.json' 42 | 43 | docker exec -it $container_id bash -c 'cd /opt/kanachan/mjai.app && \ 44 | zip -r mjai-app.zip \ 45 | .local \ 46 | _kanachan.py \ 47 | bot.py \ 48 | config.json \ 49 | hand_calculator.py \ 50 | kanachan \ 51 | mahjong \ 52 | model.kanachan \ 53 | shupai.trie \ 54 | shupai.xiangting \ 55 | xiangting_calculator \ 56 | zipai.trie \ 57 | zipai.xiangting' 58 | 59 | docker cp ${container_id}:/opt/kanachan/mjai.app/mjai-app.zip . 60 | -------------------------------------------------------------------------------- /src/simulation/utility.cpp: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include "simulation/utility.hpp" 3 | 4 | #include "simulation/gil.hpp" 5 | #include "common/assert.hpp" 6 | #include "common/throw.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | namespace{ 22 | 23 | namespace python = boost::python; 24 | 25 | } // namespace `anonymous` 26 | 27 | namespace Kanachan{ 28 | 29 | std::vector getRandomSeed() 30 | { 31 | constexpr std::size_t state_size = std::mt19937::state_size; 32 | 33 | std::random_device rand; 34 | std::vector seed; 35 | seed.reserve(state_size); 36 | for (std::size_t i = 0; i < state_size; ++i) { 37 | std::uint_least32_t const seed_ = rand(); 38 | seed.push_back(seed_); 39 | } 40 | return seed; 41 | } 42 | 43 | void translatePythonException() 44 | { 45 | std::exception_ptr const p = std::current_exception(); 46 | if (!p) { 47 | KANACHAN_THROW("No exception."); 48 | } 49 | 50 | try { 51 | std::rethrow_exception(p); 52 | } 53 | catch (python::error_already_set const &) { 54 | Kanachan::GIL::RecursiveLock gil_lock; 55 | 56 | auto const [type, value, traceback] = [](){ 57 | PyObject *p_type = nullptr; 58 | PyObject *p_value = nullptr; 59 | PyObject *p_traceback = nullptr; 60 | PyErr_Fetch(&p_type, &p_value, &p_traceback); 61 | 62 | python::object type{ python::handle<>(p_type) }; 63 | 64 | python::object value; 65 | if (p_value != nullptr) { 66 | value = python::object{ python::handle<>(p_value) }; 67 | } 68 | else { 69 | value = python::object(); 70 | } 71 | 72 | python::object traceback; 73 | if (p_traceback != nullptr) { 74 | traceback = python::object{python::handle<>(p_traceback)}; 75 | } 76 | 77 | return std::tuple(type, value, traceback); 78 | }(); 79 | 80 | python::object m = python::import("traceback"); 81 | 82 | if (python::extract(value).check()) { 83 | python::object o = m.attr("format_tb")(traceback); 84 | o = python::str("").attr("join")(o); 85 | std::string message = python::extract(o); 86 | message += python::extract(value); 87 | KANACHAN_THROW(message); 88 | } 89 | 90 | python::object o = m.attr("format_exception")(type, value, traceback); 91 | o = python::str("").attr("join")(o); 92 | python::extract str(o); 93 | KANACHAN_ASSERT((str.check())); 94 | KANACHAN_THROW(str()); 95 | } 96 | } 97 | 98 | } // namespace Kanachan 99 | -------------------------------------------------------------------------------- /src/simulation/game.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/game.hpp" 2 | 3 | #include "simulation/game_log.hpp" 4 | #include "simulation/round.hpp" 5 | #include "simulation/paishan.hpp" 6 | #include "simulation/game_state.hpp" 7 | #include "simulation/decision_maker.hpp" 8 | #include "common/assert.hpp" 9 | #include "common/throw.hpp" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | 20 | namespace { 21 | 22 | using std::placeholders::_1; 23 | using Seat_ = std::pair>; 24 | using Seats_ = std::array; 25 | 26 | } // namespace `anonymous` 27 | 28 | namespace Kanachan{ 29 | 30 | std::shared_ptr simulateGame( 31 | std::vector const &seed, std::uint_fast8_t room, bool dong_feng_zhan, 32 | Seats_ const &seats, std::vector const &test_paishan_list, 33 | std::stop_token stop_token) 34 | { 35 | if (seed.empty() && test_paishan_list.empty()) { 36 | KANACHAN_THROW( 37 | "Either `seed` or `test_paishan_list` must not be empty."); 38 | } 39 | if (!seed.empty() && !test_paishan_list.empty()) { 40 | KANACHAN_THROW("Either `seed` or `test_paishan_list` must be empty."); 41 | } 42 | if (room >= 5u) { 43 | KANACHAN_THROW(_1) << room; 44 | } 45 | for (auto [grade, model] : seats) { 46 | if (grade >= 16u) { 47 | KANACHAN_THROW(_1) << grade; 48 | } 49 | if (!model) { 50 | KANACHAN_THROW("The model of a seat is empty."); 51 | } 52 | } 53 | 54 | bool const test = !test_paishan_list.empty(); 55 | std::size_t i = 0u; 56 | 57 | Kanachan::GameState game_state(room, dong_feng_zhan, seats, stop_token); 58 | std::shared_ptr p_game_log = std::make_shared(); 59 | 60 | bool end_of_game = false; 61 | while (!end_of_game) { 62 | if (test) { 63 | if (i >= test_paishan_list.size()) { 64 | KANACHAN_THROW(_1) 65 | << "The number of test PaiShan is too small: i == " << i 66 | << ", test_paishan_list.size() == " << test_paishan_list.size(); 67 | } 68 | Kanachan::Paishan const &test_paishan = test_paishan_list[i++]; 69 | end_of_game = Kanachan::simulateRound(seed, game_state, &test_paishan, *p_game_log); 70 | if (end_of_game && i != test_paishan_list.size()) { 71 | KANACHAN_THROW(_1) 72 | << "The number of test pai shan is too large: i == " << i 73 | << ", test_paishan_list.size() == " << test_paishan_list.size(); 74 | } 75 | } 76 | else { 77 | end_of_game = Kanachan::simulateRound(seed, game_state, nullptr, *p_game_log); 78 | } 79 | } 80 | 81 | p_game_log->onEndOfGame({ 82 | game_state.getPlayerScore(0u), game_state.getPlayerScore(1u), 83 | game_state.getPlayerScore(2u), game_state.getPlayerScore(3u) 84 | }); 85 | 86 | return p_game_log; 87 | } 88 | 89 | } // namespace Kanachan 90 | -------------------------------------------------------------------------------- /kanachan/model_loader.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import importlib 3 | from typing import Any, Union, Dict, Iterable 4 | import torch 5 | from torch import nn 6 | 7 | 8 | def dump_object(obj: Any, args: Iterable[Any], kwargs: Dict[str, Any]) -> Dict[str, Any]: 9 | module_name = obj.__class__.__module__ 10 | class_name = obj.__class__.__qualname__ 11 | 12 | state = { 13 | '__kanachan__': '11fc2bfe-c4c7-402e-b11e-7cb3ff6f9945', 14 | 'module': module_name, 15 | 'class': class_name, 16 | } 17 | if len(args) != 0: 18 | state['args'] = args 19 | if len(kwargs) != 0: 20 | state['kwargs'] = kwargs 21 | 22 | return state 23 | 24 | 25 | def dump_model(model: nn.Module, args: Iterable[Any], kwargs: Dict[str, Any]) -> Dict[str, Any]: 26 | state = dump_object(model, args, kwargs) 27 | state['state_dict'] = model.state_dict() 28 | 29 | return state 30 | 31 | 32 | def _load_model(state: Any, map_location: torch.device) -> Any: 33 | if not isinstance(state, dict): 34 | return state 35 | if '__kanachan__' not in state: 36 | return state 37 | if state['__kanachan__'] != '11fc2bfe-c4c7-402e-b11e-7cb3ff6f9945': 38 | return state 39 | 40 | if 'module' not in state: 41 | raise RuntimeError('A broken Kanachan\'s model file.') 42 | module_name = state['module'] 43 | module = importlib.import_module(module_name) 44 | 45 | if 'class' not in state: 46 | raise RuntimeError('A broken Kanachan\'s model file.') 47 | class_name = state['class'] 48 | if not hasattr(module, class_name): 49 | raise RuntimeError(f'The `{module_name}` mudule does not have the `{class_name}` class.') 50 | _class = getattr(module, class_name) 51 | 52 | args = [] 53 | if 'args' in state: 54 | for arg in state['args']: 55 | args.append(_load_model(arg, map_location)) 56 | 57 | kwargs = {} 58 | if 'kwargs' in state: 59 | for name, value in state['kwargs'].items(): 60 | kwargs[name] = _load_model(value, map_location) 61 | if map_location is not None and 'device' in kwargs: 62 | kwargs['device'] = map_location 63 | 64 | model: nn.Module = _class(*args, **kwargs) 65 | 66 | if 'state_dict' in state: 67 | model.load_state_dict(state['state_dict']) 68 | 69 | return model 70 | 71 | 72 | def load_model(model_path: Union[str, Path], map_location: torch.device=None) -> nn.Module: 73 | if isinstance(model_path, str): 74 | model_path = Path(model_path) 75 | if not model_path.exists(): 76 | raise RuntimeError(f'{model_path}: Does not exist.') 77 | if not model_path.is_file(): 78 | raise RuntimeError(f'{model_path}: Not a file.') 79 | 80 | state = torch.load(model_path, map_location=map_location) 81 | if not isinstance(state, dict): 82 | raise RuntimeError(f'{model_path}: Not a Kanachan\'s model file.') 83 | if '__kanachan__' not in state: 84 | raise RuntimeError(f'{model_path}: Not a Kanachan\'s model file.') 85 | if state['__kanachan__'] != '11fc2bfe-c4c7-402e-b11e-7cb3ff6f9945': 86 | raise RuntimeError(f'{model_path}: Not a Kanachan\'s model file.') 87 | 88 | return _load_model(state, map_location) 89 | -------------------------------------------------------------------------------- /src/simulation/paishan.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/paishan.hpp" 2 | 3 | #include "simulation/gil.hpp" 4 | #include "common/assert.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | namespace { 19 | 20 | using std::placeholders::_1; 21 | namespace python = boost::python; 22 | 23 | } // namespace `anonymous` 24 | 25 | namespace Kanachan{ 26 | 27 | void swap(Paishan &lhs, Paishan &rhs) noexcept 28 | { 29 | lhs.swap(rhs); 30 | } 31 | 32 | Paishan::Paishan(std::vector const &seed) 33 | : tiles_({ 34 | 0u, 1u, 1u, 1u, 1u, 2u, 2u, 2u, 2u, 3u, 3u, 3u, 3u, 4u, 4u, 4u, 4u, 35 | 5u, 5u, 5u, 6u, 6u, 6u, 6u, 7u, 7u, 7u, 7u, 8u, 8u, 8u, 8u, 9u, 9u, 9u, 9u, 36 | 10u, 11u, 11u, 11u, 11u, 12u, 12u, 12u, 12u, 13u, 13u, 13u, 13u, 14u, 14u, 14u, 14u, 37 | 15u, 15u, 15u, 16u, 16u, 16u, 16u, 17u, 17u, 17u, 17u, 18u, 18u, 18u, 18u, 19u, 19u, 19u, 19u, 38 | 20u, 21u, 21u, 21u, 21u, 22u, 22u, 22u, 22u, 23u, 23u, 23u, 23u, 24u, 24u, 24u, 24u, 39 | 25u, 25u, 25u, 26u, 26u, 26u, 26u, 27u, 27u, 27u, 27u, 28u, 28u, 28u, 28u, 29u, 29u, 29u, 29u, 40 | 30u, 30u, 30u, 30u, 31u, 31u, 31u, 31u, 32u, 32u, 32u, 32u, 33u, 33u, 33u, 33u, 41 | 34u, 34u, 34u, 34u, 35u, 35u, 35u, 35u, 36u, 36u, 36u, 36u, 42 | }) 43 | { 44 | std::seed_seq sseq(seed.cbegin(), seed.cend()); 45 | std::mt19937 urng(sseq); 46 | std::shuffle(tiles_.begin(), tiles_.end(), urng); 47 | } 48 | 49 | Paishan::Paishan(python::list paishan) 50 | : tiles_() 51 | { 52 | if (paishan.is_none()) { 53 | KANACHAN_THROW(_1) << "`paishan` must not be a `None`."; 54 | } 55 | 56 | Kanachan::GIL::RecursiveLock gil_lock; 57 | 58 | python::ssize_t const length = python::len(paishan); 59 | if (length != 136u) { 60 | KANACHAN_THROW(_1) << "paishan: An wrong length (" << length << ")."; 61 | } 62 | for (python::ssize_t i = 0; i < length; ++i) { 63 | long const tile = [&](){ 64 | python::extract tile(paishan[i]); 65 | if (!tile.check()) { 66 | KANACHAN_THROW(_1) << "paishan: A type error."; 67 | } 68 | return tile(); 69 | }(); 70 | if (tile < 0 || 37 <= tile) { 71 | KANACHAN_THROW(_1) << "paishan: An wrong tile (" << tile << ")."; 72 | } 73 | tiles_[i] = tile; 74 | } 75 | } 76 | 77 | void Paishan::swap(Paishan &rhs) noexcept 78 | { 79 | using std::swap; 80 | swap(tiles_, rhs.tiles_); 81 | } 82 | 83 | Paishan &Paishan::operator=(Paishan const &rhs) 84 | { 85 | Paishan(rhs).swap(*this); 86 | return *this; 87 | } 88 | 89 | Paishan &Paishan::operator=(Paishan &&rhs) noexcept 90 | { 91 | Paishan(std::move(rhs)).swap(*this); 92 | return *this; 93 | } 94 | 95 | std::uint_fast8_t Paishan::operator[](std::uint_fast8_t const index) const 96 | { 97 | KANACHAN_ASSERT((index < 136u)); 98 | return tiles_[index]; 99 | } 100 | 101 | } // namespace Kanachan 102 | -------------------------------------------------------------------------------- /src/simulation/zimo.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/zimo.hpp" 2 | 3 | #include "simulation/jiuzhong_jiupai.hpp" 4 | #include "simulation/hule.hpp" 5 | #include "simulation/jiagang.hpp" 6 | #include "simulation/angang.hpp" 7 | #include "simulation/dapai.hpp" 8 | #include "simulation/round_state.hpp" 9 | #include "simulation/game_log.hpp" 10 | #include "common/assert.hpp" 11 | #include "common/throw.hpp" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | 20 | namespace { 21 | 22 | using std::placeholders::_1; 23 | 24 | } // namespace `anonymous` 25 | 26 | namespace Kanachan{ 27 | 28 | std::any zimo(Kanachan::RoundState &round_state, Kanachan::GameLog &game_log) 29 | { 30 | auto const [action, zimo_tile] = round_state.onZimo(game_log); 31 | 32 | if (action <= 147u) { 33 | // Da Pai (打牌) 34 | // この時点ではロンされる可能性があるため立直は成立しない. 35 | // また,立直が確定しない限り四家立直も確定しない. 36 | KANACHAN_ASSERT((zimo_tile < 37u)); 37 | std::uint_fast8_t const tile = action / 4u; 38 | // 親の配牌14枚のうちどれが第一自摸であるかの区別が存在しないため, 39 | // 親の第一打牌は常に手出しになる. 40 | bool const moqi = round_state.getNumLeftTiles() == 69u ? false : ((action - tile * 4u) / 2u == 1u); 41 | bool const lizhi = ((action - tile * 4u - moqi * 2u) == 1u); 42 | auto dapai = std::bind( 43 | &Kanachan::dapai, std::ref(round_state), tile, moqi, lizhi, std::ref(game_log)); 44 | std::function next_step(std::move(dapai)); 45 | return next_step; 46 | } 47 | 48 | if (action == UINT_FAST16_MAX) { 49 | // Da Pai after Li Zhi (立直後の打牌) 50 | KANACHAN_ASSERT((zimo_tile < 37u)); 51 | auto dapai = std::bind( 52 | &Kanachan::dapai, std::ref(round_state), zimo_tile, /*moqi = */true, 53 | /*lizhi = */false, std::ref(game_log)); 54 | std::function next_step(std::move(dapai)); 55 | return next_step; 56 | } 57 | 58 | if (/*148u <= action && */action <= 181u) { 59 | // An Gang (暗槓) 60 | // 暗槓は槍槓が成立する可能性があるため,この時点では四槓散了は確定しない. 61 | KANACHAN_ASSERT((zimo_tile < 37u)); 62 | std::uint_fast8_t const encode = action - 148u; 63 | auto angang = std::bind( 64 | &Kanachan::angang, std::ref(round_state), zimo_tile, encode, std::ref(game_log)); 65 | std::function next_step(std::move(angang)); 66 | return next_step; 67 | } 68 | 69 | if (/*182u <= action && */action <= 218u) { 70 | // Jia Gang (加槓) 71 | // 加槓は槍槓が成立する可能性があるため,この時点では四槓散了は確定しない. 72 | KANACHAN_ASSERT((zimo_tile < 37u)); 73 | std::uint_fast8_t const encode = action - 182u; 74 | auto jiagang = std::bind( 75 | &Kanachan::jiagang, std::ref(round_state), zimo_tile, encode, std::ref(game_log)); 76 | std::function next_step(std::move(jiagang)); 77 | return next_step; 78 | } 79 | 80 | if (action == 219u) { 81 | // Zi Mo Hu (自摸和) 82 | KANACHAN_ASSERT((zimo_tile < 37u)); 83 | auto hule = std::bind(&Kanachan::hule, std::ref(round_state), zimo_tile, std::ref(game_log)); 84 | std::function next_step(std::move(hule)); 85 | return next_step; 86 | } 87 | 88 | if (action == 220u) { 89 | // Jiu Zhong Jiu Pai (九種九牌) 90 | KANACHAN_ASSERT((zimo_tile == UINT_FAST8_MAX)); 91 | auto jiuzhong_jiupai = std::bind( 92 | &Kanachan::jiuzhongJiupai, std::ref(round_state), std::ref(game_log)); 93 | std::function next_step(std::move(jiuzhong_jiupai)); 94 | return next_step; 95 | } 96 | 97 | KANACHAN_THROW(_1) << action << ": An invalid action on zimo."; 98 | } 99 | 100 | } // namespace Kanachan 101 | -------------------------------------------------------------------------------- /kanachan/training/iql/q_model.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import OrderedDict 3 | import torch 4 | from torch import nn 5 | from kanachan.training.constants import ( 6 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES, ENCODER_WIDTH 7 | ) 8 | from kanachan.training.bert.encoder import Encoder 9 | from kanachan.training.iql.value_model import ValueDecoder 10 | 11 | 12 | class QDecoder(nn.Module): 13 | def __init__( 14 | self, *, dimension: int, dim_feedforward: int, activation_function: str, dropout: float, 15 | num_layers: int, device: torch.device, dtype: torch.dtype) -> None: 16 | if dimension < 1: 17 | raise ValueError(dimension) 18 | if dim_feedforward < 1: 19 | raise ValueError(dim_feedforward) 20 | if activation_function not in ('relu', 'gelu'): 21 | raise ValueError(activation_function) 22 | if dropout < 0.0 or 1.0 <= dropout: 23 | raise ValueError(dropout) 24 | if num_layers < 1: 25 | raise ValueError(num_layers) 26 | 27 | super(QDecoder, self).__init__() 28 | 29 | self.value_decoder = ValueDecoder( 30 | dimension=dimension, dim_feedforward=dim_feedforward, 31 | activation_function=activation_function, dropout=dropout, num_layers=num_layers, 32 | device=device, dtype=dtype) 33 | 34 | layers = OrderedDict() 35 | for i in range(num_layers - 1): 36 | layers[f'layer{i}'] = nn.Linear( 37 | dimension if i == 0 else dim_feedforward, dim_feedforward, 38 | device=device, dtype=dtype) 39 | if activation_function == 'relu': 40 | layers[f'activation{i}'] = nn.ReLU() 41 | elif activation_function == 'gelu': 42 | layers[f'activation{i}'] = nn.GELU() 43 | else: 44 | raise ValueError(activation_function) 45 | layers[f'dropout{i}'] = nn.Dropout(p=dropout) 46 | suffix = '' if num_layers == 1 else str(num_layers - 1) 47 | layers['layer' + suffix] = nn.Linear( 48 | dimension if num_layers == 1 else dim_feedforward, 1, device=device, dtype=dtype) 49 | self.layers = nn.Sequential(layers) 50 | 51 | def forward( 52 | self, candidates: torch.Tensor, encode: torch.Tensor) -> torch.Tensor: 53 | assert candidates.dim() == 2 54 | assert encode.dim() == 3 55 | assert encode.size(1) == ENCODER_WIDTH 56 | assert candidates.size(0) == encode.size(0) 57 | 58 | value: torch.Tensor = self.value_decoder(candidates, encode) 59 | assert value.dim() == 1 60 | assert value.size(0) == candidates.size(0) 61 | value = torch.unsqueeze(value, dim=1) 62 | value = value.expand(-1, MAX_NUM_ACTION_CANDIDATES) 63 | 64 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 65 | advantage: torch.Tensor = self.layers(encode) 66 | advantage = torch.squeeze(advantage, dim=2) 67 | assert advantage.dim() == 2 68 | assert advantage.size(0) == candidates.size(0) 69 | assert advantage.size(1) == MAX_NUM_ACTION_CANDIDATES 70 | 71 | q = value + advantage 72 | q = torch.where(candidates < NUM_TYPES_OF_ACTIONS, q, -math.inf) 73 | 74 | return q 75 | 76 | 77 | class QModel(nn.Module): 78 | def __init__(self, encoder: Encoder, decoder: QDecoder) -> None: 79 | super(QModel, self).__init__() 80 | self.encoder = encoder 81 | self.decoder = decoder 82 | 83 | def forward( 84 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 85 | candidates: torch.Tensor) -> torch.Tensor: 86 | encode = self.encoder(sparse, numeric, progression, candidates) 87 | return self.decoder(candidates, encode) 88 | -------------------------------------------------------------------------------- /test/annotation_vs_simulation/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | import os 5 | import json 6 | import sys 7 | from kanachan.simulation import (TestModel, test,) 8 | 9 | 10 | def _on_leaf(test_file_path: Path): 11 | print(f'{test_file_path}: ', end='', flush=True) 12 | 13 | with open(test_file_path) as f: 14 | test_data = json.load(f) 15 | 16 | uuid = test_data['uuid'] 17 | room = test_data['room'] 18 | style = test_data['style'] 19 | players = test_data['players'] 20 | rounds = test_data['rounds'] 21 | 22 | player_grades = tuple(p['grade'] for p in players) 23 | 24 | paishan_list = [] 25 | for r in rounds: 26 | paishan = r['paishan'] 27 | paishan_list.append(paishan) 28 | 29 | simulation_mode = 1 # Non-duplicated simulation mode 30 | if style == 0: 31 | simulation_mode += 2 # Dong Feng Zhan (東風戦) 32 | simulation_mode += (1 << (3 + room)) 33 | 34 | test_model = TestModel(rounds) 35 | 36 | result = test(simulation_mode, player_grades, test_model, paishan_list) 37 | 38 | if len(result['rounds']) != len(test_data['rounds']): 39 | raise RuntimeError(f'{result["rounds"]} != {test_data["rounds"]}') 40 | 41 | for i in range(len(test_data['rounds'])): 42 | round_data = test_data['rounds'][i] 43 | round_result = result['rounds'][i] 44 | for j in range(4): 45 | delta_score_to_be = round_data['delta_scores'][j] 46 | delta_score_as_is = round_result[j]['delta_score'] 47 | if delta_score_as_is != delta_score_to_be: 48 | delta_scores_as_is = [round_result[k]['delta_score'] for k in range(4)] 49 | raise RuntimeError( 50 | f'''{i}-th round: 51 | {delta_scores_as_is} 52 | {round_data["delta_scores"]}''') 53 | 54 | final_ranking = [test_data['players'][i]['final_ranking'] for i in range(4)] 55 | if result['ranking'] != final_ranking: 56 | raise RuntimeError(f'{result["ranking"]} != {final_ranking}') 57 | 58 | final_scores = [test_data['players'][i]['final_score'] for i in range(4)] 59 | if result['scores'] != final_scores: 60 | raise RuntimeError(f'{result["scores"]} != {final_scores}') 61 | 62 | print('PASS') 63 | 64 | 65 | def main(): 66 | if len(sys.argv) < 2: 67 | raise RuntimeError('Too few arguments.') 68 | if len(sys.argv) > 3: 69 | raise RuntimeError('Too many arguments.') 70 | 71 | path = Path(sys.argv[1]) 72 | if not path.exists(): 73 | raise RuntimeError(f'{path}: Does not exist.') 74 | 75 | if path.is_file(): 76 | _on_leaf(path) 77 | return 78 | 79 | skip_list_path = None 80 | if len(sys.argv) == 3: 81 | skip_list_path = Path(sys.argv[2]) 82 | if not skip_list_path.is_file(): 83 | raise RuntimeError(f'{skip_list_path}: Not a file.') 84 | 85 | skip_list = set() 86 | if skip_list_path is not None: 87 | with open(skip_list_path) as f: 88 | for line in f: 89 | line = line.rstrip('\n') 90 | skip_list.add(line) 91 | 92 | if not path.is_dir(): 93 | raise RuntimeError(f'{path}: Neither a file nor a directory.') 94 | for dirpath, dirnames, filenames in os.walk(path): 95 | for filename in filenames: 96 | if not filename.endswith('.json'): 97 | continue 98 | test_file_path = Path(dirpath) / filename 99 | if test_file_path.stem in skip_list: 100 | print(f'{test_file_path}: SKIP') 101 | continue 102 | _on_leaf(test_file_path) 103 | 104 | 105 | if __name__ == '__main__': 106 | main() 107 | sys.exit(0) 108 | -------------------------------------------------------------------------------- /kanachan/training/ilql/qv_model.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import OrderedDict 3 | from typing import Tuple 4 | import torch 5 | from torch import nn 6 | from kanachan.training.constants import ( 7 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES, ENCODER_WIDTH 8 | ) 9 | from kanachan.training.bert.encoder import Encoder 10 | from kanachan.training.iql.value_model import ValueDecoder 11 | 12 | 13 | class QVDecoder(nn.Module): 14 | def __init__( 15 | self, *, dimension: int, dim_feedforward: int, activation_function: str, dropout: float, 16 | num_layers: int, device: torch.device, dtype: torch.dtype) -> None: 17 | if dimension < 1: 18 | raise ValueError(dimension) 19 | if dim_feedforward < 1: 20 | raise ValueError(dim_feedforward) 21 | if activation_function not in ('relu', 'gelu'): 22 | raise ValueError(activation_function) 23 | if dropout < 0.0 or 1.0 <= dropout: 24 | raise ValueError(dropout) 25 | if num_layers < 1: 26 | raise ValueError(num_layers) 27 | 28 | super(QVDecoder, self).__init__() 29 | 30 | self.value_decoder = ValueDecoder( 31 | dimension=dimension, dim_feedforward=dim_feedforward, 32 | activation_function=activation_function, dropout=dropout, num_layers=num_layers, 33 | device=device, dtype=dtype) 34 | 35 | layers = OrderedDict() 36 | for i in range(num_layers - 1): 37 | layers[f'layer{i}'] = nn.Linear( 38 | dimension if i == 0 else dim_feedforward, dim_feedforward, 39 | device=device, dtype=dtype) 40 | if activation_function == 'relu': 41 | layers[f'activation{i}'] = nn.ReLU() 42 | elif activation_function == 'gelu': 43 | layers[f'activation{i}'] = nn.GELU() 44 | else: 45 | raise ValueError(activation_function) 46 | layers[f'dropout{i}'] = nn.Dropout(p=dropout) 47 | suffix = '' if num_layers == 1 else str(num_layers - 1) 48 | layers['layer' + suffix] = nn.Linear( 49 | dimension if num_layers == 1 else dim_feedforward, 1, device=device, dtype=dtype) 50 | self.layers = nn.Sequential(layers) 51 | 52 | def forward( 53 | self, candidates: torch.Tensor, 54 | encode: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: 55 | assert candidates.dim() == 2 56 | assert encode.dim() == 3 57 | assert encode.size(1) == ENCODER_WIDTH 58 | assert candidates.size(0) == encode.size(0) 59 | 60 | value: torch.Tensor = self.value_decoder(candidates, encode) 61 | assert value.dim() == 1 62 | assert value.size(0) == candidates.size(0) 63 | value_expanded = torch.unsqueeze(value, dim=1) 64 | value_expanded = value_expanded.expand(-1, MAX_NUM_ACTION_CANDIDATES) 65 | 66 | encode = encode[:, -MAX_NUM_ACTION_CANDIDATES:] 67 | advantage: torch.Tensor = self.layers(encode) 68 | advantage = torch.squeeze(advantage, dim=2) 69 | assert advantage.dim() == 2 70 | assert advantage.size(0) == candidates.size(0) 71 | assert advantage.size(1) == MAX_NUM_ACTION_CANDIDATES 72 | 73 | q = value_expanded + advantage 74 | q = torch.where(candidates < NUM_TYPES_OF_ACTIONS, q, -math.inf) 75 | 76 | return q, value 77 | 78 | 79 | class QVModel(nn.Module): 80 | def __init__(self, encoder: Encoder, decoder: QVDecoder) -> None: 81 | super(QVModel, self).__init__() 82 | self.encoder = encoder 83 | self.decoder = decoder 84 | 85 | def forward( 86 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 87 | candidates: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: 88 | encode = self.encoder(sparse, numeric, progression, candidates) 89 | return self.decoder(candidates, encode) 90 | -------------------------------------------------------------------------------- /src/annotation/README.md: -------------------------------------------------------------------------------- 1 | # Annotate 2 | 3 | **annotate** is a C++ program that extracts almost all *decision-making points* from game records of Mahjong Soul, and converts the game situation at each decision-making point together with the player's choice and round's/game's final results into annotations suitable to learning. The output of the program is text, where each line represents the annotation of a decision-making point. The format of the annotation (each line of the output) is specified in [Training Data Format for Behavioral Cloning](https://github.com/Cryolite/kanachan/wiki/Notes-on-Training-Data#training-data-format-for-behavioral-cloning). 4 | 5 | A *decision-making point* is defined by a point in a game at which a player is forced to choose an action among multiple options. Possible points and actions are enumerated as follows: 6 | 7 | * Immediately after a self-draw (zimo, 自摸): 8 | * which tile in their hand to discard if not in riichi, 9 | * whether or not to declare riichi if possible, 10 | * whether or not to declare to win (zimo hu, 自摸和) if possible, 11 | * whether or not to declare a closed kong (an gang, 暗槓) if possible, 12 | * whether or not to declare a open kong (jia gang, 加槓) if possible, and 13 | * whether or not to declare no game (jiu zhong jiu pai, 九種九牌) if possible. 14 | * Immediately after other player's discard: 15 | * whether or not to declare chi if possible, 16 | * whether or not to declare pon (peng, ポン, 碰) if possible, 17 | * whether or not to declare kong (da ming gang, 大明槓) if possible, and 18 | * whether or not to declare to win (rong, 栄和) if possible. 19 | * Immediately after other player's kong: 20 | * whether or not declare to win (qiang gang, 槍槓) if possible. 21 | 22 | However, decision-making points in the following relatively rare situations cannot be extracted and are assumed to be skipped, because they are not recorded in game records even if the player makes a choice, or the GUI disappears before the player makes a choice: 23 | 24 | * A player is forced to choose whether or not to chow, but pon, kong, or rong by another player takes precedence, or 25 | * a player is forced to choose whether or not to pon or kong, but rong by another player takes precedence. 26 | 27 | ## How to Build (For Developer Only) 28 | 29 | Non-developers do not need to run the following command to build the Docker image because it built and available as a [public Docker image](https://hub.docker.com/r/cryolite/kanachan.annotate). 30 | 31 | ```bash 32 | kanachan$ docker build -f src/annotation/Dockerfile -t cryolite/kanachan.annotate . 33 | ``` 34 | 35 | ## How to Use 36 | 37 | ### Data Preparation 38 | 39 | **annotate** assumes that game records of Mahjong Soul reside in one directory. All the files in that directory must be game record files. Each file in the directory must consists of the record of exactly one game. The content of each file, the format of the game record in other words, must be the same as the WebSocket response message that is returned from the Mahjong Soul API server when you hit a URL of the format `https://game.mahjongsoul.com/?paipu=YYMMDD-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`, where `YYMMDD-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` is the UUID of a game. 40 | 41 | ### Annotation 42 | 43 | Let `/path/to/data` be the path to the directory you have prepared according to the description in the [Data Preparation](#data-preparation) section. The following command line 44 | 45 | ```bash 46 | $ docker run -v /path/to/data:/data:ro --rm cryolite/kanachan.annotate 47 | ``` 48 | 49 | prints the annotations converted from the game records in `/path/to/data` to the standard output. 50 | 51 | Be careful not to specify the `-t` option to the `docker run` command, because the `-t` option will cause the newline code in the standard output of the `docker run` command to be `\r\n` instead of `\n` (cf. https://github.com/moby/moby/issues/8513). However, the training programs will work correctly even if the newline code in the annotation data is `\r\n`. 52 | -------------------------------------------------------------------------------- /kanachan/training/aiayn/iterator_adaptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pathlib 4 | import json 5 | import torch 6 | import torch.nn.functional 7 | from torch.utils.data import get_worker_info 8 | from kanachan.training.constants import ( 9 | NUM_TYPES_OF_SPARSE_FEATURES, MAX_NUM_ACTIVE_SPARSE_FEATURES, 10 | NUM_NUMERIC_FEATURES, NUM_TYPES_OF_PROGRESSION_FEATURES, 11 | MAX_LENGTH_OF_PROGRESSION_FEATURES, NUM_TYPES_OF_ACTIONS, 12 | MAX_NUM_ACTION_CANDIDATES 13 | ) 14 | 15 | 16 | class IteratorAdaptor(object): 17 | def __init__(self, path: pathlib.Path, num_dimensions: int, dtype) -> None: 18 | self.__fp = open(path, encoding='UTF-8') 19 | self.__num_dimensions = num_dimensions 20 | self.__dtype = dtype 21 | 22 | if get_worker_info() is not None: 23 | try: 24 | for _ in range(get_worker_info().id): 25 | next(self.__fp) 26 | except StopIteration as _: 27 | pass 28 | 29 | def __del__(self) -> None: 30 | self.__fp.close() 31 | 32 | def __parse_line(self, line: str): 33 | line = line.rstrip('\n') 34 | uuid, sparse, numeric, positional, candidates, index, _ = line.split('\t') 35 | 36 | sparse = json.loads('[' + sparse + ']') 37 | if len(sparse) > MAX_NUM_ACTIVE_SPARSE_FEATURES: 38 | raise RuntimeError(f'{uuid}: {len(sparse)}') 39 | for i in sparse: 40 | if i >= NUM_TYPES_OF_SPARSE_FEATURES: 41 | raise RuntimeError(f'{uuid}: {i}') 42 | for i in range(len(sparse), MAX_NUM_ACTIVE_SPARSE_FEATURES): 43 | # padding 44 | sparse.append(NUM_TYPES_OF_SPARSE_FEATURES) 45 | sparse = torch.tensor(sparse, device='cpu', dtype=torch.int32) 46 | 47 | numeric = json.loads('[' + numeric + ']') 48 | if len(numeric) != NUM_NUMERIC_FEATURES: 49 | raise RuntimeError(uuid) 50 | numeric[2:] = [s / 10000.0 for s in numeric[2:]] 51 | numeric = torch.tensor(numeric, device='cpu', dtype=self.__dtype) 52 | numeric = torch.unsqueeze(numeric, 1) 53 | numeric = torch.nn.functional.pad( 54 | numeric, (0, self.__num_dimensions - 1)) 55 | 56 | positional = json.loads('[' + positional + ']') 57 | if len(positional) > MAX_LENGTH_OF_PROGRESSION_FEATURES: 58 | raise RuntimeError(f'{uuid}: {len(positional)}') 59 | for p in positional: 60 | if p >= NUM_TYPES_OF_PROGRESSION_FEATURES: 61 | raise RuntimeError(f'{uuid}: {p}') 62 | for i in range(len(positional), MAX_LENGTH_OF_PROGRESSION_FEATURES): 63 | positional.append(NUM_TYPES_OF_PROGRESSION_FEATURES) 64 | positional = torch.tensor(positional, device='cpu', dtype=torch.int32) 65 | 66 | candidates = json.loads('[' + candidates + ']') 67 | if len(candidates) > MAX_NUM_ACTION_CANDIDATES: 68 | raise RuntimeError(f'{uuid}: {len(candidates)}') 69 | for a in candidates: 70 | if a >= NUM_TYPES_OF_ACTIONS: 71 | raise RuntimeError(f'{uuid}: {a}') 72 | for i in range(len(candidates), MAX_NUM_ACTION_CANDIDATES): 73 | candidates.append(NUM_TYPES_OF_ACTIONS + 1) 74 | candidates = torch.tensor(candidates, device='cpu', dtype=torch.int32) 75 | 76 | index = torch.tensor(int(index), device='cpu', dtype=torch.int64) 77 | 78 | return sparse, numeric, positional, candidates, index 79 | 80 | def __next__(self): 81 | if get_worker_info() is None: 82 | line = next(self.__fp) 83 | return self.__parse_line(line) 84 | else: 85 | line = next(self.__fp) 86 | try: 87 | assert(get_worker_info().num_workers >= 1) 88 | for _ in range(get_worker_info().num_workers - 1): 89 | next(self.__fp) 90 | except StopIteration as _: 91 | pass 92 | return self.__parse_line(line) 93 | -------------------------------------------------------------------------------- /kanachan/training/iterator_adaptor_base.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import gzip 3 | import bz2 4 | import torch 5 | import torch.nn.functional 6 | from torch.utils.data import get_worker_info 7 | from kanachan.training.constants import ( 8 | NUM_TYPES_OF_SPARSE_FEATURES, MAX_NUM_ACTIVE_SPARSE_FEATURES, 9 | NUM_NUMERIC_FEATURES, NUM_TYPES_OF_PROGRESSION_FEATURES, 10 | MAX_LENGTH_OF_PROGRESSION_FEATURES, NUM_TYPES_OF_ACTIONS, 11 | MAX_NUM_ACTION_CANDIDATES) 12 | 13 | 14 | class IteratorAdaptorBase(object): 15 | def __init__(self, path: Path) -> None: 16 | if path.suffix == '.gz': 17 | self.__fp = gzip.open(path, mode='rt', encoding='UTF-8') 18 | elif path.suffix == '.bz2': 19 | self.__fp = bz2.open(path, mode='rt', encoding='UTF-8') 20 | else: 21 | self.__fp = open(path, encoding='UTF-8') 22 | 23 | if get_worker_info() is not None: 24 | try: 25 | for _ in range(get_worker_info().id): 26 | next(self.__fp) 27 | except StopIteration as _: 28 | pass 29 | 30 | def __del__(self) -> None: 31 | self.__fp.close() 32 | 33 | def __parse_line(self, line: str): 34 | line = line.rstrip('\n') 35 | uuid, sparse, numeric, progression, candidates, index, results = line.split('\t') 36 | 37 | sparse = [int(x) for x in sparse.split(',')] 38 | if len(sparse) > MAX_NUM_ACTIVE_SPARSE_FEATURES: 39 | raise RuntimeError(f'{uuid}: {len(sparse)}') 40 | for i in sparse: 41 | if i >= NUM_TYPES_OF_SPARSE_FEATURES: 42 | raise RuntimeError(f'{uuid}: {i}') 43 | for i in range(len(sparse), MAX_NUM_ACTIVE_SPARSE_FEATURES): 44 | # padding 45 | sparse.append(NUM_TYPES_OF_SPARSE_FEATURES) 46 | sparse = torch.tensor(sparse, device='cpu', dtype=torch.int32) 47 | 48 | numeric = [int(x) for x in numeric.split(',')] 49 | if len(numeric) != NUM_NUMERIC_FEATURES: 50 | raise RuntimeError(uuid) 51 | numeric[2:] = [x / 10000.0 for x in numeric[2:]] 52 | numeric = torch.tensor(numeric, device='cpu', dtype=torch.float32) 53 | 54 | progression = [int(x) for x in progression.split(',')] 55 | if len(progression) > MAX_LENGTH_OF_PROGRESSION_FEATURES: 56 | raise RuntimeError(f'{uuid}: {len(progression)}') 57 | for p in progression: 58 | if p >= NUM_TYPES_OF_PROGRESSION_FEATURES: 59 | raise RuntimeError(f'{uuid}: {p}') 60 | for i in range(len(progression), MAX_LENGTH_OF_PROGRESSION_FEATURES): 61 | # padding 62 | progression.append(NUM_TYPES_OF_PROGRESSION_FEATURES) 63 | progression = torch.tensor(progression, device='cpu', dtype=torch.int32) 64 | 65 | candidates = [int(x) for x in candidates.split(',')] 66 | if len(candidates) > MAX_NUM_ACTION_CANDIDATES: 67 | raise RuntimeError(f'{uuid}: {len(candidates)}') 68 | for a in candidates: 69 | if a >= NUM_TYPES_OF_ACTIONS: 70 | raise RuntimeError(f'{uuid}: {a}') 71 | for i in range(len(candidates), MAX_NUM_ACTION_CANDIDATES): 72 | # padding 73 | candidates.append(NUM_TYPES_OF_ACTIONS + 1) 74 | candidates = torch.tensor(candidates, device='cpu', dtype=torch.int32) 75 | 76 | index = int(index) 77 | index = torch.tensor(index, device='cpu', dtype=torch.int64) 78 | 79 | results = [int(x) for x in results.split(',')] 80 | 81 | return (sparse, numeric, progression, candidates, index, results) 82 | 83 | def __next__(self): 84 | if get_worker_info() is None: 85 | line = next(self.__fp) 86 | return self.__parse_line(line) 87 | else: 88 | line = next(self.__fp) 89 | try: 90 | assert(get_worker_info().num_workers >= 1) 91 | for _ in range(get_worker_info().num_workers - 1): 92 | next(self.__fp) 93 | except StopIteration as _: 94 | pass 95 | return self.__parse_line(line) 96 | -------------------------------------------------------------------------------- /prerequisites/boost/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | script_dir="$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")" 6 | prologue_sh_path="$(readlink -e "$script_dir/../prologue.sh")" 7 | . "$prologue_sh_path" 8 | unset prologue_sh_path 9 | unset script_dir 10 | 11 | PROGRAM_NAME=build 12 | 13 | function print_usage () 14 | { 15 | cat <<'EOF' 16 | Usage: build [OPTION]... -- [b2arg]... 17 | Build Boost. 18 | 19 | --source-dir= The source directory (mandatory). 20 | --prefix= The prefix of installation locations (default: 21 | `/usr/local'). 22 | -h, --help Display this help and exit. 23 | EOF 24 | } 25 | 26 | if getopt -T; (( $? != 4 )); then 27 | die_with_runtime_error "$PROGRAM_NAME" "\`getopt' is not an enhanced version." 28 | fi 29 | opts="$(getopt -n "$PROGRAM_NAME" -l source-dir:,prefix:,help -- h "$@")" 30 | eval set -- "$opts" 31 | 32 | libraries=() 33 | 34 | while (( $# > 0 )); do 35 | arg="$1" 36 | shift 37 | case "$arg" in 38 | --source-dir) 39 | if (( $# == 0 )); then 40 | die_with_logic_error "$PROGRAM_NAME" 41 | fi 42 | source_dir="$1" 43 | shift 44 | ;; 45 | --prefix) 46 | if (( $# == 0 )); then 47 | die_with_logic_error "$PROGRAM_NAME" 48 | fi 49 | prefix="$1" 50 | shift 51 | ;; 52 | -h|--help) 53 | set +x 54 | print_usage 55 | exit 0 56 | ;; 57 | --) 58 | rest_args=("$@") 59 | break 60 | ;; 61 | *) 62 | die_with_user_error "$PROGRAM_NAME" "An invalid argument \`$arg'." 63 | ;; 64 | esac 65 | done 66 | 67 | b2_args=() 68 | 69 | b2_args+=("-j$(nproc)") 70 | 71 | if [[ $SHELLOPTS =~ (^|:)xtrace($|:) ]]; then 72 | b2_args+=('-d+2') 73 | fi 74 | 75 | temp_dir="$(mktemp -d)" \ 76 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to create a temporary directory." 77 | push_rollback_command "rm -rf \"$temp_dir\"" 78 | 79 | if [[ ${source_dir-NOT-DEFINED} == NOT-DEFINED ]]; then 80 | die_with_user_error "$PROGRAM_NAME" "\`--source-dir' option is mandatory." 81 | fi 82 | if [[ -z $source_dir ]]; then 83 | die_with_user_error "$PROGRAM_NAME" "An invalid value \`$source_dir' for \`--source-dir' option." 84 | fi 85 | if [[ $(readlink -m "$source_dir") != $(cd "$temp_dir" >/dev/null && readlink -m "$source_dir") ]]; then 86 | die_with_user_error "$PROGRAM_NAME" "A relative path \`$source_dir' is specified for \`--source-dir' option, but is expected to be an absolute one." 87 | fi 88 | 89 | if [[ ${prefix-NOT-DEFINED} == NOT-DEFINED ]]; then 90 | prefix=/usr/local 91 | fi 92 | if [[ -z $prefix ]]; then 93 | die_with_user_error "$PROGRAM_NAME" "An invalid value \`$prefix' for \`--prefix' option." 94 | fi 95 | if [[ $(readlink -m "$prefix") != $(cd "$temp_dir" >/dev/null && readlink -m "$prefix") ]]; then 96 | die_with_user_error "$PROGRAM_NAME" "A relative path \`$prefix' is specified for \`--prefix' option, but is expected to be an absolute one." 97 | fi 98 | b2_args+=("--prefix=$prefix") 99 | 100 | b2_args+=(${rest_args[@]+"${rest_args[@]}"}) 101 | 102 | b2_args+=(install) 103 | 104 | mkdir -p "$prefix/bin" \ 105 | || die_with_logic_error "$PROGRAM_NAME" "Failed to create the directory \`$prefix/bin'." 106 | touch "$temp_dir/timestamp" \ 107 | || die_with_logic_error "$PROGRAM_NAME" "Failed to \`touch' \`$temp_dir/timestamp'." 108 | (cd "$source_dir" && ./bootstrap.sh) \ 109 | || die_with_logic_error "$PROGRAM_NAME" "Failed to execute \`bootstrap.sh'." 110 | (cd "$source_dir" && mv b2 "$prefix/bin") \ 111 | || die_with_logic_error "$PROGRAM_NAME" "Failed to \`mv' \`bjam' or \`b2'." 112 | find "$source_dir" -newer "$temp_dir/timestamp" -type f -delete \ 113 | || die_with_logic_error "$PROGRAM_NAME" "Failed to delete intermediate files." 114 | find "$source_dir" -newer "$temp_dir/timestamp" -type d -empty -delete \ 115 | || die_with_logic_error "$PROGRAM_NAME" "Failed to delete intermediate files." 116 | rm "$temp_dir/timestamp" 117 | 118 | (cd "$source_dir" && "$prefix/bin/b2" ${b2_args[@]+"${b2_args[@]}"}) \ 119 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to build Boost libraries." 120 | -------------------------------------------------------------------------------- /src/simulation/game_state.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/game_state.hpp" 2 | 3 | #include "simulation/decision_maker.hpp" 4 | #include "common/assert.hpp" 5 | #include "common/throw.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace { 14 | 15 | using std::placeholders::_1; 16 | 17 | } // namespace `anonymous 18 | 19 | namespace Kanachan{ 20 | 21 | GameState::GameState( 22 | std::uint_fast8_t const room, bool const dong_feng_zhan, std::array const &seats, 23 | std::stop_token stop_token) 24 | : room_(room) 25 | , dong_feng_zhan_(dong_feng_zhan) 26 | , seats_(seats) 27 | , stop_token_(stop_token) 28 | { 29 | KANACHAN_ASSERT((room_ < 5u)); 30 | for (auto [grade, model] : seats_) { 31 | KANACHAN_ASSERT((grade < 16u)); 32 | } 33 | } 34 | 35 | std::uint_fast8_t GameState::getRoom() const 36 | { 37 | return room_; 38 | } 39 | 40 | bool GameState::isDongfengZhan() const 41 | { 42 | return dong_feng_zhan_; 43 | } 44 | 45 | std::uint_fast8_t GameState::getPlayerGrade(std::uint_fast8_t const seat) const 46 | { 47 | KANACHAN_ASSERT((seat < 4u)); 48 | return seats_[seat].first; 49 | } 50 | 51 | std::uint_fast8_t GameState::getChang() const 52 | { 53 | KANACHAN_ASSERT((dong_feng_zhan_ && chang_ < 2u || !dong_feng_zhan_ && chang_ < 3u)); 54 | return chang_; 55 | } 56 | 57 | std::uint_fast8_t GameState::getJu() const 58 | { 59 | KANACHAN_ASSERT((ju_ < 4u)); 60 | return ju_; 61 | } 62 | 63 | std::uint_fast8_t GameState::getBenChang() const 64 | { 65 | return ben_chang_; 66 | } 67 | 68 | std::uint_fast8_t GameState::getNumLizhiDeposits() const 69 | { 70 | return lizhi_deposits_; 71 | } 72 | 73 | std::int_fast32_t GameState::getPlayerScore(std::uint_fast8_t const seat) const 74 | { 75 | KANACHAN_ASSERT((seat < 4u)); 76 | return scores_[seat]; 77 | } 78 | 79 | std::uint_fast8_t GameState::getPlayerRanking(std::uint_fast8_t const seat) const 80 | { 81 | KANACHAN_ASSERT((seat < 4u)); 82 | std::int_fast32_t const score = scores_[seat]; 83 | std::uint_fast8_t ranking = 0u; 84 | for (std::uint_fast8_t i = 0u; i < seat; ++i) { 85 | if (scores_[i] >= score) { 86 | ++ranking; 87 | } 88 | } 89 | for (std::uint_fast8_t i = seat + 1u; i < 4u; ++i) { 90 | if (scores_[i] > score) { 91 | ++ranking; 92 | } 93 | } 94 | return ranking; 95 | } 96 | 97 | std::uint_fast16_t GameState::selectAction( 98 | std::uint_fast8_t const seat, std::vector &&sparse, 99 | std::vector &&numeric, std::vector &&progression, 100 | std::vector &&candidates) const 101 | { 102 | KANACHAN_ASSERT((seat < 4u)); 103 | Kanachan::DecisionMaker &decision_maker = *(seats_[seat].second); 104 | return decision_maker( 105 | std::move(sparse), std::move(numeric), std::move(progression), std::move(candidates), 106 | stop_token_); 107 | } 108 | 109 | void GameState::onSuccessfulLizhi(std::uint_fast8_t const seat) 110 | { 111 | KANACHAN_ASSERT((seat < 4u)); 112 | KANACHAN_ASSERT((scores_[seat] >= 1000)); 113 | scores_[seat] -= 1000; 114 | ++lizhi_deposits_; 115 | } 116 | 117 | void GameState::addPlayerScore(std::uint_fast8_t const seat, std::int_fast32_t const score) 118 | { 119 | scores_[seat] += score; 120 | } 121 | 122 | void GameState::onLianzhuang(RoundEndStatus const round_end_status) 123 | { 124 | ++ben_chang_; 125 | if (round_end_status == RoundEndStatus::hule) { 126 | lizhi_deposits_ = 0u; 127 | } 128 | } 129 | 130 | void GameState::onLunzhuang(RoundEndStatus const round_end_status) 131 | { 132 | if (round_end_status == RoundEndStatus::liuju) { 133 | KANACHAN_THROW("An invalid argument."); 134 | } 135 | 136 | if (++ju_ == 4u) { 137 | ++chang_; 138 | ju_ = 0u; 139 | } 140 | 141 | switch (round_end_status) { 142 | case RoundEndStatus::hule: 143 | ben_chang_ = 0u; 144 | lizhi_deposits_ = 0u; 145 | break; 146 | case RoundEndStatus::huangpai_pingju: 147 | ++ben_chang_; 148 | break; 149 | default: 150 | KANACHAN_THROW("A logic error."); 151 | } 152 | } 153 | 154 | } // namespace Kanachan 155 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25.0) 2 | project(kanachan) 3 | 4 | enable_testing() 5 | 6 | if (NOT DEFINED PYTHON_VERSION) 7 | execute_process( 8 | COMMAND bash "-c" "python --version | grep -Eo '^Python [[:digit:]]+\\.[[:digit:]]+' | grep -Eo '[[:digit:]]+\\.[[:digit:]]+'" 9 | OUTPUT_VARIABLE PYTHON_VERSION 10 | OUTPUT_STRIP_TRAILING_WHITESPACE) 11 | endif() 12 | 13 | if (NOT DEFINED PYTHON_INCLUDE_PATH) 14 | set(PYTHON_INCLUDE_PATH /usr/include/python${PYTHON_VERSION}) 15 | endif() 16 | 17 | message("PYTHON_VERSION=${PYTHON_VERSION}") 18 | message("PYTHON_INCLUDE_PATH=${PYTHON_INCLUDE_PATH}") 19 | 20 | message("CMAKE_C_COMPILER=${CMAKE_C_COMPILER}") 21 | message("CMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}") 22 | 23 | set(CMAKE_CXX_STANDARD 20) 24 | 25 | include_directories( 26 | "${PYTHON_INCLUDE_PATH}" 27 | src) 28 | 29 | if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) 30 | add_compile_options(-Werror) 31 | endif() 32 | 33 | message("CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}") 34 | 35 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 36 | set(KANACHAN_ENABLE_ASSERT ON) 37 | endif() 38 | if (KANACHAN_ENABLE_ASSERT) 39 | add_compile_definitions(KANACHAN_ENABLE_ASSERT) 40 | endif() 41 | message("KANACHAN_ENABLE_ASSERT=${KANACHAN_ENABLE_ASSERT}") 42 | 43 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 44 | if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) 45 | add_compile_definitions(_GLIBCXX_DEBUG _GLIBCXX_DEBUG_PEDANTIC) 46 | endif() 47 | endif() 48 | 49 | if (KANACHAN_WITH_COVERAGE) 50 | add_compile_definitions(KANACHAN_WITH_COVERAGE) 51 | add_compile_options("-coverage") 52 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -coverage") 53 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -coverage") 54 | endif() 55 | message("KANACHAN_WITH_COVERAGE=${KANACHAN_WITH_COVERAGE}") 56 | 57 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 58 | if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) 59 | set(KANACHAN_WITH_ASAN ON) 60 | endif() 61 | endif() 62 | if (KANACHAN_WITH_ASAN) 63 | add_compile_options(-fsanitize=address) 64 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address") 65 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") 66 | endif() 67 | message("KANACHAN_WITH_ASAN=${KANACHAN_WITH_ASAN}") 68 | 69 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 70 | if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) 71 | set(KANACHAN_WITH_UBSAN ON) 72 | endif() 73 | endif() 74 | if (KANACHAN_WITH_UBSAN) 75 | add_compile_options(-fsanitize=undefined) 76 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=undefined") 77 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") 78 | endif() 79 | message("KANACHAN_WITH_UBSAN=${KANACHAN_WITH_UBSAN}") 80 | 81 | if (KANACHAN_WITH_TSAN) 82 | add_compile_options(-fsanitize=thread) 83 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=thread") 84 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=thread") 85 | endif() 86 | message("KANACHAN_WITH_TSAN=${KANACHAN_WITH_TSAN}") 87 | 88 | if (CMAKE_BUILD_TYPE STREQUAL "Release") 89 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) 90 | endif() 91 | 92 | add_compile_options(-pthread) 93 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pthread") 94 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") 95 | 96 | message("CMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS}") 97 | message("CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS}") 98 | 99 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 100 | set(Boost_USE_DEBUG_LIBS ON) 101 | set(Boost_USE_RELEASE_LIBS OFF) 102 | else() 103 | set(Boost_USE_DEBUG_LIBS OFF) 104 | set(Boost_USE_RELEASE_LIBS ON) 105 | endif() 106 | 107 | set(BOOST_ROOT "$ENV{HOME}/.local") 108 | find_package(Boost 109 | REQUIRED 110 | COMPONENTS stacktrace_backtrace python) 111 | 112 | add_compile_definitions(BOOST_STACKTRACE_USE_BACKTRACE) 113 | 114 | add_subdirectory(src) 115 | add_subdirectory(test/annotation_vs_simulation) 116 | -------------------------------------------------------------------------------- /src/simulation/shoupai.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_SIMULATION_SHOUPAI_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_SIMULATION_SHOUPAI_HPP_INCLUDE_GUARD 3 | 4 | #include "simulation/paishan.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | namespace Kanachan{ 15 | 16 | class Shoupai; 17 | 18 | void swap(Shoupai &lhs, Shoupai &rhs) noexcept; 19 | 20 | class Shoupai 21 | { 22 | public: 23 | Shoupai(std::uint_fast8_t index, Kanachan::Paishan const &paishan); 24 | 25 | Shoupai(Shoupai const &rhs) = default; 26 | 27 | Shoupai(Shoupai &&rhs) = default; 28 | 29 | ~Shoupai(); 30 | 31 | void swap(Shoupai &rhs) noexcept; 32 | 33 | Shoupai &operator=(Shoupai const &rhs); 34 | 35 | Shoupai &operator=(Shoupai &&rhs) noexcept; 36 | 37 | public: 38 | bool isMenqian() const; 39 | 40 | bool isTingpai() const; 41 | 42 | std::uint_fast8_t getNumGangzi() const; 43 | 44 | private: 45 | std::vector getShoupai34_() const; 46 | 47 | std::uint_fast8_t getNumFulu_() const; 48 | 49 | boost::python::list getShoupai136_(boost::python::list fulu_list, std::uint_fast8_t hupai) const; 50 | 51 | boost::python::list getFuluList_() const; 52 | 53 | void updateHupaiList_(); 54 | 55 | public: 56 | void appendToFeatures(std::vector &sparse_features) const; 57 | 58 | std::vector getCandidatesOnZimo( 59 | std::uint_fast8_t zimo_tile, bool first_zimo, bool lizhi_prohibited, 60 | bool gang_prohibited, long tool_config) const; 61 | 62 | std::vector getCandidatesOnDapai( 63 | std::uint_fast8_t relseat, std::uint_fast8_t dapai, bool gang_prohibited, 64 | long tool_config) const; 65 | 66 | std::vector getCandidatesOnChiPeng() const; 67 | 68 | std::vector getCandidatesOnAngang( 69 | std::uint_fast8_t relseat, std::uint_fast8_t encode) const; 70 | 71 | std::vector getCandidatesOnJiagang( 72 | std::uint_fast8_t relseat, std::uint_fast8_t encode, long tool_config) const; 73 | 74 | std::pair calculateHand( 75 | std::uint_fast8_t hupai, std::vector const &dora_indicators, 76 | long tool_config) const; 77 | 78 | void onPostZimo(std::uint_fast8_t zimo_tile, std::uint_fast8_t dapai, bool in_lizhi); 79 | 80 | void onChi(std::uint_fast8_t encode); 81 | 82 | void onPeng(std::uint_fast8_t relseat, std::uint_fast8_t encode); 83 | 84 | void onPostChiPeng(std::uint_fast8_t dapai); 85 | 86 | void onDaminggang(std::uint_fast8_t relseat, std::uint_fast8_t dapai); 87 | 88 | void onAngang(std::uint_fast8_t zimo_tile, std::uint_fast8_t encode); 89 | 90 | void onJiagang(std::uint_fast8_t zimo_tile, std::uint_fast8_t encode); 91 | 92 | void onPostGang(bool in_lizhi); 93 | 94 | private: 95 | std::array shoupai_ = { 96 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 97 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 98 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 99 | 0u, 0u, 0u, 0u, 0u, 0u, 0u 100 | }; 101 | 102 | // チー: [0, 90) 103 | // ポン: [90, 210) = 90 + relseat * 40 + peng 104 | // 大明槓: [210, 321) = 210 + relseat * 37 + tile 105 | // 暗槓: [321, 355) = 321 + tile' 106 | // 加槓: [355, 392) = 355 + tile 107 | std::array fulu_list_ = { 108 | std::numeric_limits::max(), 109 | std::numeric_limits::max(), 110 | std::numeric_limits::max(), 111 | std::numeric_limits::max() 112 | }; 113 | 114 | std::uint_fast16_t kuikae_delayed_ = std::numeric_limits::max(); 115 | 116 | boost::python::object external_tool_; 117 | 118 | std::vector he_; 119 | mutable bool tingpai_cache_ = false; 120 | mutable std::uint_fast8_t xiangting_lower_bound_ = std::numeric_limits::max(); 121 | std::vector hupai_list_{}; 122 | mutable bool zhenting_ = false; 123 | }; // class Shoupai 124 | 125 | } // namespace Kanachan 126 | 127 | #endif // !defined(KANACHAN_SIMULATION_SHOUPAI_HPP_INCLUDE_GUARD) 128 | -------------------------------------------------------------------------------- /prerequisites/googletest/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | script_dir="$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")" 6 | prologue_sh_path="$(readlink -e "$script_dir/../prologue.sh")" 7 | . "$prologue_sh_path" 8 | unset prologue_sh_path 9 | unset script_dir 10 | 11 | PROGRAM_NAME=install 12 | 13 | function print_usage () 14 | { 15 | cat <<'EOF' 16 | Usage: install [OPTION]... 17 | Install GoogleTest. 18 | 19 | --prefix= Pass `--prefix=' on to googletest 20 | `configure' script (default: `/usr/local'). 21 | --source-dir= The source directory (default: 22 | `/src/googletest'). 23 | --clobber-source-dir Delete the source directory before the source 24 | archive is expanded there. 25 | -h, --help Display this help and exit. 26 | EOF 27 | } 28 | 29 | if getopt -T; (( $? != 4 )); then 30 | die_with_runtime_error "$PROGRAM_NAME" "\`getopt' is not an enhanced version." 31 | fi 32 | opts="$(getopt -n "$PROGRAM_NAME" -l prefix:,source-dir:,clobber-source-dir,help -- h "$@")" 33 | eval set -- "$opts" 34 | 35 | while (( $# > 0 )); do 36 | arg="$1" 37 | shift 38 | case "$arg" in 39 | --prefix) 40 | if (( $# == 0 )); then 41 | die_with_logic_error "$PROGRAM_NAME" 42 | fi 43 | prefix="$1" 44 | shift 45 | ;; 46 | --source-dir) 47 | if (( $# == 0 )); then 48 | die_with_logic_error "$PROGRAM_NAME" 49 | fi 50 | source_dir="$1" 51 | shift 52 | ;; 53 | --clobber-source-dir) 54 | clobber_source_dir=yes 55 | ;; 56 | -h|--help) 57 | set +x 58 | print_usage 59 | exit 0 60 | ;; 61 | --) 62 | if (( $# > 0 )); then 63 | die_with_user_error "$PROGRAM_NAME" "An invalid argument \`$1'." 64 | fi 65 | break 66 | ;; 67 | *) 68 | die_with_user_error "$PROGRAM_NAME" "An invalid argument \`$arg'." 69 | ;; 70 | esac 71 | done 72 | 73 | configure_options=() 74 | 75 | : ${prefix=/usr/local} 76 | configure_options+=("--prefix=$prefix") 77 | 78 | temp_dir="$(mktemp -d)" \ 79 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to create a temporary directory." 80 | push_rollback_command "rm -rf \"$temp_dir\"" 81 | 82 | : ${source_dir="$prefix/src/googletest"} 83 | if [[ -z $source_dir ]]; then 84 | die_with_user_error "$PROGRAM_NAME" "An invalid value \`$source_dir' for \`--source-dir' option." 85 | fi 86 | if [[ $(readlink -m "$source_dir") != $(cd "$temp_dir" >/dev/null && readlink -m "$source_dir") ]]; then 87 | die_with_user_error "$PROGRAM_NAME" "A relative path \`$source_dir' is specified for \`--source-dir' option, but is expected to be an absolute one." 88 | fi 89 | 90 | if [[ -e $source_dir ]]; then 91 | case "${clobber_source_dir-no}" in 92 | yes) 93 | rm -rf "$source_dir" 94 | ;; 95 | no) 96 | die_with_user_error "$PROGRAM_NAME" "Could not overwrite \`$source_dir'. Use \`--clobber-source-dir' to overwrite it." 97 | ;; 98 | *) 99 | die_with_logic_error "$PROGRAM_NAME" 100 | ;; 101 | esac 102 | fi 103 | 104 | source_dir_prefix="$(dirname "$source_dir")" 105 | source_dir_basename="$(basename "$source_dir")" 106 | mkdir -p "$source_dir_prefix" \ 107 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to create \`$source_dir_prefix' directory, which is necessary to create the source directory \`$source_dir'." 108 | (cd "$source_dir_prefix" && git clone 'https://github.com/google/googletest.git' "$source_dir_basename") \ 109 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`git clone' googletest repository." 110 | 111 | build_dir="$temp_dir/build" 112 | mkdir "$build_dir" 113 | 114 | (cd "$build_dir" && cmake "$source_dir") \ 115 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`cmake' googletest." 116 | 117 | make_options=() 118 | 119 | # Check whether this script is (directly or indirectly) called from `make'. 120 | if ! declare -p MAKEFLAGS 2>/dev/null | grep -Eq '^declare -x MAKEFLAGS='; then 121 | make_options+=(-j -l "$(nproc)") 122 | fi 123 | 124 | (cd "$build_dir" && make ${make_options[@]+"${make_options[@]}"}) \ 125 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`make' googletest." 126 | 127 | (cd "$build_dir" && make install) \ 128 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`make install' googletest." 129 | -------------------------------------------------------------------------------- /prerequisites/libbacktrace/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | script_dir="$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")" 6 | prologue_sh_path="$(readlink -e "$script_dir/../prologue.sh")" 7 | . "$prologue_sh_path" 8 | unset prologue_sh_path 9 | unset script_dir 10 | 11 | PROGRAM_NAME=install 12 | 13 | function print_usage () 14 | { 15 | cat <<'EOF' 16 | Usage: install [OPTION]... 17 | Install libbacktrace. 18 | 19 | --prefix= Pass `--prefix=' on to libbacktrace 20 | `configure' script (default: `/usr/local'). 21 | --source-dir= The source directory (default: 22 | `/src/libbacktrace'). 23 | --clobber-source-dir Delete the source directory before the source 24 | archive is expanded there. 25 | -h, --help Display this help and exit. 26 | EOF 27 | } 28 | 29 | if getopt -T; (( $? != 4 )); then 30 | die_with_runtime_error "$PROGRAM_NAME" "\`getopt' is not an enhanced version." 31 | fi 32 | opts="$(getopt -n "$PROGRAM_NAME" -l prefix:,source-dir:,clobber-source-dir,help -- h "$@")" 33 | eval set -- "$opts" 34 | 35 | while (( $# > 0 )); do 36 | arg="$1" 37 | shift 38 | case "$arg" in 39 | --prefix) 40 | if (( $# == 0 )); then 41 | die_with_logic_error "$PROGRAM_NAME" 42 | fi 43 | prefix="$1" 44 | shift 45 | ;; 46 | --source-dir) 47 | if (( $# == 0 )); then 48 | die_with_logic_error "$PROGRAM_NAME" 49 | fi 50 | source_dir="$1" 51 | shift 52 | ;; 53 | --clobber-source-dir) 54 | clobber_source_dir=yes 55 | ;; 56 | -h|--help) 57 | set +x 58 | print_usage 59 | exit 0 60 | ;; 61 | --) 62 | if (( $# > 0 )); then 63 | die_with_user_error "$PROGRAM_NAME" "An invalid argument \`$1'." 64 | fi 65 | break 66 | ;; 67 | *) 68 | die_with_user_error "$PROGRAM_NAME" "An invalid argument \`$arg'." 69 | ;; 70 | esac 71 | done 72 | 73 | configure_options=() 74 | 75 | : ${prefix=/usr/local} 76 | configure_options+=("--prefix=$prefix") 77 | 78 | temp_dir="$(mktemp -d)" \ 79 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to create a temporary directory." 80 | push_rollback_command "rm -rf \"$temp_dir\"" 81 | 82 | : ${source_dir="$prefix/src/libbacktrace"} 83 | if [[ -z $source_dir ]]; then 84 | die_with_user_error "$PROGRAM_NAME" "An invalid value \`$source_dir' for \`--source-dir' option." 85 | fi 86 | if [[ $(readlink -m "$source_dir") != $(cd "$temp_dir" >/dev/null && readlink -m "$source_dir") ]]; then 87 | die_with_user_error "$PROGRAM_NAME" "A relative path \`$source_dir' is specified for \`--source-dir' option, but is expected to be an absolute one." 88 | fi 89 | 90 | if [[ -e $source_dir ]]; then 91 | case "${clobber_source_dir-no}" in 92 | yes) 93 | rm -rf "$source_dir" 94 | ;; 95 | no) 96 | die_with_user_error "$PROGRAM_NAME" "Could not overwrite \`$source_dir'. Use \`--clobber-source-dir' to overwrite it." 97 | ;; 98 | *) 99 | die_with_logic_error "$PROGRAM_NAME" 100 | ;; 101 | esac 102 | fi 103 | 104 | source_dir_prefix="$(dirname "$source_dir")" 105 | source_dir_basename="$(basename "$source_dir")" 106 | mkdir -p "$source_dir_prefix" \ 107 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to create \`$source_dir_prefix' directory, which is necessary to create the source directory \`$source_dir'." 108 | (cd "$source_dir_prefix" && git clone 'https://github.com/ianlancetaylor/libbacktrace.git' "$source_dir_basename") \ 109 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`git clone' libbacktrace repository." 110 | 111 | build_dir="$temp_dir/build" 112 | mkdir "$build_dir" 113 | 114 | (cd "$build_dir" || exit $? 115 | "$source_dir/configure" --with-pic ${configure_options[@]+"${configure_options[@]}"}) \ 116 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`configure' libbacktrace." 117 | 118 | make_options=() 119 | 120 | # Check whether this script is (directly or indirectly) called from `make'. 121 | if ! declare -p MAKEFLAGS 2>/dev/null | grep -Eq '^declare -x MAKEFLAGS='; then 122 | make_options+=(-j -l "$(nproc)") 123 | fi 124 | 125 | (cd "$build_dir" && make ${make_options[@]+"${make_options[@]}"}) \ 126 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`make' libbacktrace." 127 | 128 | (cd "$build_dir" && make check) \ 129 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`make check' libbacktrace." 130 | 131 | (cd "$build_dir" && make install) \ 132 | || die_with_runtime_error "$PROGRAM_NAME" "Failed to \`make install' libbacktrace." 133 | -------------------------------------------------------------------------------- /kanachan/simulation/README.md: -------------------------------------------------------------------------------- 1 | # `cryolite/kanachan.simulation` Docker image 2 | 3 | ### How to Build 4 | 5 | Make the top (`kanachan`) directory of the working tree of this repository the current directory, and execute the following commands: 6 | 7 | ```bash 8 | kanachan$ docker build --pull -f kanachan/Dockerfile -t cryolite/kanachan . 9 | kanachan$ docker build --pull -f kanachan/simulation -t cryolite/kanachan.simulation . 10 | ``` 11 | 12 | # `kanachan.simulation.run` Python program 13 | 14 | The `kanachan.simulation.run` Python program simulates Mahjong Soul ranking matches to compare the performance of two given models. 15 | 16 | ### Prerequisits 17 | 18 | The following items are required to run this program: 19 | 20 | - [Docker](https://www.docker.com/), 21 | - NVIDIA driver, and 22 | - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/overview.html). 23 | - ([CUDA](https://developer.nvidia.com/cuda-toolkit) is not required.) 24 | 25 | For detailed installation instructions for the above prerequisite items, refer to those for each OS and distribution. 26 | 27 | After the installation of the prerequisite items, build the `cryolite/kanachan.simulation` Docker image (see above). 28 | 29 | ### Usage 30 | 31 | ``` 32 | $ docker run --gpus all -v /path/to/host-data:/workspace/data -it --rm cryolite/kanachan.simulation OPTIONS... 33 | ``` 34 | 35 | If you want to run this program on a specific GPU, use the `--gpus device=n` option, where `n` is the GPU number displayed by the [`nvidia-smi`](https://developer.nvidia.com/nvidia-system-management-interface) command, or `--device` option (see below). 36 | 37 | #### Options 38 | 39 | `--device DEVICE`: Specify the device on which to run simulation. The allowed values are `cpu`, `cuda`, and `cuda:n`, where `n` is the GPU number displayed by the `nvidia-smi` command. If no value is specified for this option, a suitable value will be inferred from the PyTorch build information. 40 | 41 | `--dtype DTYPE`: Specify the [PyTorch `dtype`](https://pytorch.org/docs/stable/tensor_attributes.html#torch.dtype) for simulation. The allowed values are `float16`, `half`, `float32`, `float`, `float64`, or `double`. Default to `float32` for CPU, and `float16` for CUDA. 42 | 43 | `--baseline-model BASELINE_MODEL`: Specify the path to the model file of the [baseline model](https://github.com/Cryolite/kanachan/wiki/Methods-and-Metrics-in-Performance-Comparison-and-Evaluation). The path must be one that can be interpreted within the Docker guest. 44 | 45 | `--baseline-grade BASELINE_GRADE`: Specify which ranking grade the baseline model should behave as in simulation. `0` means Novice 1 (初心1), `1` Novice 2 (初心2), ..., `14` Saint 3 (雀聖3), and `15` Celestial (魂天). The value must be in the range `0` (Novice 1, 初心1) to `15` (Celestial, 魂天). 46 | 47 | `--proposed-model PROPOSED_MODEL`: Specify the path to the model file of the [proposed model](https://github.com/Cryolite/kanachan/wiki/Methods-and-Metrics-in-Performance-Comparison-and-Evaluation). The path must be one that can be interpreted within the Docker guest. 48 | 49 | `--proposed-grade PROPOSED_GRADE`: Specify which ranking grade the proposed model should behave as in simulation. The meaning of the value is the same as one for the `--baseline-grade` option. 50 | 51 | `--room ROOM`: Specify which room simulation emulates. `0` means Bronze Room (銅の間), `1` Silver Room (銀の間), `2` Gold Room (金の間), `3` Jade Room (玉の間), and `4` Throne Room (王座の間). 52 | 53 | `--dongfengzhan`: Specify simulation emulates quater-length games (Dong Feng Zhan, 東風戦) instead of half-length games (Ban Zhuang Zhan, 半荘戦). 54 | 55 | `--mode MODE`: Specify the [comparison style](https://github.com/Cryolite/kanachan/wiki/Methods-and-Metrics-in-Performance-Comparison-and-Evaluation) of simulation. The allowed values are `2vs2` or `1vs3`. Default to `2vs2`. Ignored if the `--non-duplicated` option is specified. 56 | 57 | `--non-duplicated`: Disable [duplicated mahjong](https://github.com/Cryolite/kanachan/wiki/Methods-and-Metrics-in-Performance-Comparison-and-Evaluation). 58 | 59 | `-n N`: Specify the number of _sets_ of simulation. A set consists of 6 games in the 2vs2 mode, 4 games in the 1vs3 mode, or 1 game in the non-duplicated mode. See [Methods and Metrics in Performance Comparison and Evaluation](https://github.com/Cryolite/kanachan/wiki/Methods-and-Metrics-in-Performance-Comparison-and-Evaluation) 60 | for detail. 61 | 62 | `--batch-size BATCH_SIZE`: Specify the batch size of simulation. The larger the batch size is, the higher it will result in simulation throughput but the more main/GPU memory consumption. Default to `1`. 63 | 64 | `--concurrency CONCURRENCY`: Specify the number of threads used for simulation. The `CONCURRENCY` must be greater or equal to `BATCH_SIZE * 2 - 1`. Default to `BATCH_SIZE * 2`. 65 | -------------------------------------------------------------------------------- /kanachan/training/bert/phase1/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from dataclasses import dataclass, field 3 | from typing import Any, Optional, List 4 | from omegaconf import MISSING 5 | from hydra.core.config_store import ConfigStore 6 | 7 | 8 | @dataclass 9 | class CpuConfig: 10 | type: Optional[str] = 'cpu' 11 | dtype: str = 'float64' 12 | amp_dtype: Optional[str] = None 13 | 14 | @dataclass 15 | class CudaConfig: 16 | type: Optional[str] = 'cuda' 17 | dtype: str = 'float32' 18 | amp_dtype: str = 'float16' 19 | 20 | 21 | @dataclass 22 | class BertBaseEncoderConfig: 23 | position_encoder: str = 'position_embedding' 24 | dimension: int = 768 25 | num_heads: int = 12 26 | dim_feedforward: Optional[int] = None 27 | activation_function: str = 'gelu' 28 | dropout: float = 0.1 29 | num_layers: int = 12 30 | load_from: Optional[Path] = None 31 | 32 | @dataclass 33 | class BertLargeEncoderConfig: 34 | position_encoder: str = 'position_embedding' 35 | dimension: int = 1024 36 | num_heads: int = 16 37 | dim_feedforward: Optional[int] = None 38 | activation_function: str = 'gelu' 39 | dropout: float = 0.1 40 | num_layers: int = 24 41 | load_from: Optional[Path] = None 42 | 43 | 44 | @dataclass 45 | class SingleDecoderConfig: 46 | dim_feedforward: Optional[int] = None 47 | activation_function: str = 'gelu' 48 | dropout: float = 0.1 49 | num_layers: int = 1 50 | load_from: Optional[Path] = None 51 | 52 | @dataclass 53 | class DoubleDecoderConfig: 54 | dim_feedforward: Optional[int] = None 55 | activation_function: str = 'gelu' 56 | dropout: float = 0.1 57 | num_layers: int = 2 58 | load_from: Optional[Path] = None 59 | 60 | @dataclass 61 | class TripleDecoderConfig: 62 | dim_feedforward: Optional[int] = None 63 | activation_function: str = 'gelu' 64 | dropout: float = 0.1 65 | num_layers: int = 3 66 | load_from: Optional[Path] = None 67 | 68 | 69 | @dataclass 70 | class SgdOptimizerConfig: 71 | type: str = 'sgd' 72 | momentum: Optional[float] = 0.0 73 | epsilon: Optional[float] = None 74 | learning_rate: float = MISSING 75 | initialize: bool = False 76 | 77 | @dataclass 78 | class AdamOptimizerConfig: 79 | type: str = 'adam' 80 | momentum: Optional[float] = None 81 | epsilon: Optional[float] = 1.0e-8 82 | learning_rate: float = 0.001 83 | initialize: bool = False 84 | 85 | @dataclass 86 | class RAdamOptimizerConfig: 87 | type: str = 'radam' 88 | momentum: Optional[float] = None 89 | epsilon: Optional[float] = 1.0e-8 90 | learning_rate: float = 0.001 91 | initialize: bool = False 92 | 93 | @dataclass 94 | class LambOptimizerConfig: 95 | type: str = 'lamb' 96 | momentum: Optional[float] = None 97 | epsilon: Optional[float] = 1.0e-6 98 | learning_rate: float = 0.001 99 | initialize: bool = False 100 | 101 | 102 | _defaults = [ 103 | { 'device': 'cuda' }, 104 | { 'encoder': 'bert_base' }, 105 | { 'decoder': 'double' }, 106 | { 'optimizer': 'lamb' }, 107 | '_self_' 108 | ] 109 | 110 | 111 | @dataclass 112 | class Config: 113 | defaults: List[Any] = field(default_factory=lambda: _defaults) 114 | training_data: Path = MISSING 115 | validation_data: Optional[Path] = None 116 | num_workers: Optional[int] = None 117 | initial_model: Optional[Path] = None 118 | initial_model_prefix: Optional[Path] = None 119 | initial_model_index: Optional[int] = None 120 | checkpointing: bool = False 121 | training_batch_size: int = MISSING 122 | validation_batch_size: Optional[int] = None 123 | gradient_accumulation_steps: int = 1 124 | max_gradient_norm: float = 1.0 125 | snapshot_interval: int = 0 126 | 127 | 128 | config_store = ConfigStore.instance() 129 | config_store.store(name='cpu', node=CpuConfig, group='device') 130 | config_store.store(name='cuda', node=CudaConfig, group='device') 131 | config_store.store(name='bert_base', node=BertBaseEncoderConfig, group='encoder') 132 | config_store.store(name='bert_large', node=BertLargeEncoderConfig, group='encoder') 133 | config_store.store(name='single', node=SingleDecoderConfig, group='decoder') 134 | config_store.store(name='double', node=DoubleDecoderConfig, group='decoder') 135 | config_store.store(name='triple', node=TripleDecoderConfig, group='decoder') 136 | config_store.store(name='sgd', node=SgdOptimizerConfig, group='optimizer') 137 | config_store.store(name='adam', node=AdamOptimizerConfig, group='optimizer') 138 | config_store.store(name='radam', node=RAdamOptimizerConfig, group='optimizer') 139 | config_store.store(name='lamb', node=LambOptimizerConfig, group='optimizer') 140 | config_store.store(name='config', node=Config) 141 | -------------------------------------------------------------------------------- /src/annotation/annotation.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_ANNOTATION_ANNOTATION_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_ANNOTATION_ANNOTATION_HPP_INCLUDE_GUARD 3 | 4 | #include "annotation/round_progress.hpp" 5 | #include "annotation/player_state.hpp" 6 | #include "common/mahjongsoul.pb.h" 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan{ 13 | 14 | class Annotation 15 | { 16 | private: 17 | Annotation( 18 | std::uint_fast8_t seat, 19 | std::array const &player_states, 20 | Kanachan::RoundProgress const &round_progress, 21 | std::uint_fast8_t prev_dapai_seat, 22 | std::uint_fast8_t prev_dapai, 23 | std::vector const &action_candidates); 24 | 25 | public: 26 | Annotation( 27 | std::uint_fast8_t seat, 28 | std::array const &player_states, 29 | Kanachan::RoundProgress const &round_progress, 30 | std::uint_fast8_t prev_dapai_seat, 31 | std::uint_fast8_t prev_dapai, 32 | std::vector const &action_candidates, 33 | lq::RecordDealTile const &); 34 | 35 | Annotation( 36 | std::uint_fast8_t seat, 37 | std::array const &player_states, 38 | Kanachan::RoundProgress const &round_progress, 39 | std::vector const &action_candidates, 40 | lq::RecordDiscardTile const &record); 41 | 42 | Annotation( 43 | std::uint_fast8_t seat, 44 | std::array const &player_state, 45 | Kanachan::RoundProgress const &round_progress, 46 | std::uint_fast8_t prev_dapai_seat, 47 | std::uint_fast8_t prev_dapai, 48 | std::vector const &action_candidates, 49 | lq::RecordChiPengGang const &record, 50 | bool skipped); 51 | 52 | Annotation( 53 | std::uint_fast8_t seat, 54 | std::array const &player_states, 55 | Kanachan::RoundProgress const &round_progress, 56 | std::vector const &action_candidates, 57 | lq::RecordAnGangAddGang const &record); 58 | 59 | Annotation( 60 | std::uint_fast8_t seat, 61 | std::array const &player_states, 62 | Kanachan::RoundProgress const &round_progress, 63 | std::uint_fast8_t prev_dapai_seat, 64 | std::uint_fast8_t prev_dapai, 65 | std::vector const &action_candidates, 66 | lq::HuleInfo const *p_record); 67 | 68 | Annotation( 69 | std::uint_fast8_t seat, 70 | std::array const &player_states, 71 | Kanachan::RoundProgress const &round_progress, 72 | std::vector const &action_candidates, 73 | lq::RecordLiuJu const &); 74 | 75 | Annotation( 76 | std::uint_fast8_t seat, 77 | std::array const &player_states, 78 | Kanachan::RoundProgress const &round_progress, 79 | std::uint_fast8_t prev_dapai_seat, 80 | std::uint_fast8_t prev_dapai, 81 | std::vector const &action_candidates, 82 | lq::RecordNoTile const &); 83 | 84 | Annotation(Annotation const &) = default; 85 | 86 | Annotation &operator=(Annotation const &) = delete; 87 | 88 | void printWithRoundResult( 89 | std::string const &uuid, std::uint_fast8_t i, 90 | Kanachan::RoundProgress const &round_progress, 91 | std::uint_fast8_t round_result, 92 | std::array const &round_delta_scores, 93 | std::array const &round_ranks, 94 | std::ostream &os) const; 95 | 96 | private: 97 | static constexpr std::uint_fast16_t dapai_offset_ = 0u; 98 | static constexpr std::uint_fast16_t angang_offset_ = 148u; 99 | static constexpr std::uint_fast16_t jiagang_offset_ = 182u; 100 | static constexpr std::uint_fast16_t zimohu_offset_ = 219u; 101 | static constexpr std::uint_fast16_t liuju_offset_ = 220u; 102 | static constexpr std::uint_fast16_t skip_offset_ = 221u; 103 | static constexpr std::uint_fast16_t chi_offset_ = 222u; 104 | static constexpr std::uint_fast16_t peng_offset_ = 312u; 105 | static constexpr std::uint_fast16_t daminggang_offset_ = 432u; 106 | static constexpr std::uint_fast16_t rong_offset_ = 543u; 107 | 108 | std::uint_fast8_t seat_; 109 | std::array player_states_; 110 | std::uint_fast8_t round_progress_size_; 111 | 112 | // Action 113 | std::vector action_candidates_; 114 | std::uint_fast8_t action_index_ = std::numeric_limits::max(); 115 | }; // class Annotation 116 | 117 | } // namespace Kanachan 118 | 119 | #endif // !defined(KANACHAN_ANNOTATION_ANNOTATION_HPP_INCLUDE_GUARD) 120 | -------------------------------------------------------------------------------- /src/simulation/dapai.cpp: -------------------------------------------------------------------------------- 1 | #include "simulation/dapai.hpp" 2 | 3 | #include "simulation/sijia_lizhi.hpp" 4 | #include "simulation/sigang_sanle.hpp" 5 | #include "simulation/sifeng_lianda.hpp" 6 | #include "simulation/huangpai_pingju.hpp" 7 | #include "simulation/hule.hpp" 8 | #include "simulation/daminggang.hpp" 9 | #include "simulation/peng.hpp" 10 | #include "simulation/chi.hpp" 11 | #include "simulation/zimo.hpp" 12 | #include "simulation/game_log.hpp" 13 | #include "simulation/round_state.hpp" 14 | #include "common/assert.hpp" 15 | #include "common/throw.hpp" 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | namespace{ 25 | 26 | using std::placeholders::_1; 27 | namespace python = boost::python; 28 | 29 | } // namespace `anonymous` 30 | 31 | namespace Kanachan{ 32 | 33 | std::any dapai( 34 | Kanachan::RoundState &round_state, std::uint_fast8_t const tile, bool const moqi, 35 | bool const lizhi, Kanachan::GameLog &game_log) 36 | { 37 | if (tile >= 37u) { 38 | KANACHAN_THROW(_1) << static_cast(tile); 39 | } 40 | 41 | auto const [dapai_seat, action] = round_state.onDapai(tile, moqi, lizhi, game_log); 42 | 43 | if (action == 221u) { 44 | // Skip 45 | KANACHAN_ASSERT((dapai_seat == UINT_FAST8_MAX)); 46 | if (round_state.getNumLeftTiles() == 0u) { 47 | auto huangpai_pingju = std::bind( 48 | &Kanachan::huangpaiPingju, std::ref(round_state), std::ref(game_log)); 49 | std::function next_step(std::move(huangpai_pingju)); 50 | return next_step; 51 | } 52 | auto zimo = std::bind(&Kanachan::zimo, std::ref(round_state), std::ref(game_log)); 53 | std::function next_step(std::move(zimo)); 54 | return next_step; 55 | } 56 | 57 | if (dapai_seat != UINT_FAST8_MAX && action != UINT_FAST16_MAX) { 58 | // Chi, Peng, or Da Ming Gang (チー・ポン・大明槓) 59 | KANACHAN_ASSERT((dapai_seat < 4u)); 60 | if (222u <= action && action <= 311u) { 61 | // Chi (チー) 62 | std::uint_fast8_t const encode = action - 222u; 63 | auto chi = std::bind(&Kanachan::chi, std::ref(round_state), encode, std::ref(game_log)); 64 | std::function next_step(std::move(chi)); 65 | return next_step; 66 | } 67 | if (312u <= action && action <= 431u) { 68 | // Peng (ポン) 69 | std::uint_fast8_t const relseat = (action - 312u) / 40u; 70 | std::uint_fast8_t const encode = action - 312u - relseat * 40u; 71 | auto peng = std::bind(&Kanachan::peng, std::ref(round_state), encode, std::ref(game_log)); 72 | std::function next_step(std::move(peng)); 73 | return next_step; 74 | } 75 | if (432u <= action && action <= 542u) { 76 | // Da Ming Gang (大明槓) 77 | auto daminggang = std::bind(&Kanachan::daminggang, std::ref(round_state), std::ref(game_log)); 78 | std::function next_step(std::move(daminggang)); 79 | return next_step; 80 | } 81 | KANACHAN_THROW(_1) << action << ": An invalid action on dapai."; 82 | } 83 | 84 | if (action == 543u) { 85 | // Rong (栄和) 86 | KANACHAN_ASSERT((dapai_seat == UINT_FAST8_MAX)); 87 | std::uint_fast8_t const zimo_tile = UINT_FAST8_MAX; 88 | auto hule = std::bind(&Kanachan::hule, std::ref(round_state), zimo_tile, std::ref(game_log)); 89 | std::function next_step(std::move(hule)); 90 | return next_step; 91 | } 92 | 93 | if (action == UINT_FAST16_MAX) { 94 | // Liu Ju (流局) in the middle. 95 | if (round_state.checkSifengLianda()) { 96 | // Si Feng Lian Da (四風連打) 97 | auto sifeng_lianda = std::bind( 98 | &Kanachan::sifengLianda, std::ref(round_state), std::ref(game_log)); 99 | std::function next_step(std::move(sifeng_lianda)); 100 | return next_step; 101 | } 102 | if (round_state.checkSigangSanle()) { 103 | // Si Gang San Le (四槓散了) 104 | auto sigang_sanle = std::bind( 105 | &Kanachan::sigangSanle, std::ref(round_state), std::ref(game_log)); 106 | std::function next_step(std::move(sigang_sanle)); 107 | return next_step; 108 | } 109 | if (round_state.checkSijiaLizhi()) { 110 | // Si Jia Li Zhi (四家立直) 111 | auto sijia_lizhi = std::bind( 112 | &Kanachan::sijiaLizhi, std::ref(round_state), std::ref(game_log)); 113 | std::function next_step(std::move(sijia_lizhi)); 114 | return next_step; 115 | } 116 | } 117 | 118 | KANACHAN_THROW(_1) << action << ": An invalid action on dapai."; 119 | #pragma GCC diagnostic push 120 | #pragma GCC diagnostic ignored "-Wreturn-type" 121 | } 122 | #pragma GCC diagnostic pop 123 | 124 | } // namespace Kanachan 125 | -------------------------------------------------------------------------------- /src/common/throw.cpp: -------------------------------------------------------------------------------- 1 | #include "common/throw.hpp" 2 | #include "common/type_name.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | #if defined(KANACHAN_WITH_COVERAGE) 13 | 14 | extern "C" void __gcov_flush(); 15 | 16 | #endif // defined(KANACHAN_WITH_COVERAGE) 17 | 18 | namespace Kanachan::Detail_{ 19 | 20 | namespace{ 21 | 22 | void printErrorMessage(boost::exception const &e) 23 | { 24 | if (char const * const * const p = boost::get_error_info(e)) { 25 | std::cerr << *p << ':'; 26 | } 27 | if (int const * const p = boost::get_error_info(e)) { 28 | std::cerr << *p << ": "; 29 | } 30 | if (char const * const * const p = boost::get_error_info(e)) { 31 | std::cerr << *p << ": "; 32 | } 33 | if (std::exception const * const p = dynamic_cast(&e)) { 34 | std::cerr << p->what() << '\n'; 35 | } 36 | } 37 | 38 | } // namespace *unnamed* 39 | 40 | [[noreturn]] void TerminateHandlerSetter::terminate_handler_() noexcept 41 | try { 42 | using Stacktrace = boost::stacktrace::stacktrace; 43 | 44 | std::vector nested_exceptions; 45 | { 46 | std::exception_ptr const p = std::current_exception(); 47 | if (p == nullptr) { 48 | std::cerr << "`std::terminate' is called without throwing any exception.\n"; 49 | Stacktrace stacktrace; 50 | if (!stacktrace.empty()) { 51 | std::cerr << "Backtrace:\n" << stacktrace; 52 | } 53 | #if defined(KANACHAN_WITH_COVERAGE) 54 | __gcov_flush(); std::abort(); 55 | #else // defined(KANACHAN_WITH_COVERAGE) 56 | std::abort(); 57 | #endif // defined(KANACHAN_WITH_COVERAGE) 58 | } 59 | nested_exceptions.push_back(p); 60 | } 61 | for (;;) { 62 | try { 63 | std::rethrow_exception(nested_exceptions.back()); 64 | } 65 | catch (boost::exception const &e) { 66 | try { 67 | std::rethrow_if_nested(e); 68 | break; 69 | } 70 | catch (...) { 71 | std::exception_ptr const p = std::current_exception(); 72 | nested_exceptions.push_back(p); 73 | } 74 | } 75 | catch (std::exception const &e) { 76 | try { 77 | std::rethrow_if_nested(e); 78 | break; 79 | } 80 | catch (...) { 81 | std::exception_ptr const p = std::current_exception(); 82 | nested_exceptions.push_back(p); 83 | } 84 | } 85 | } 86 | 87 | try { 88 | std::rethrow_exception(nested_exceptions.back()); 89 | } 90 | catch (boost::exception const &e) { 91 | std::cerr << "`std::terminate' is called after throwing an instance of `" 92 | << Kanachan::getTypeName(e) << "'.\n"; 93 | printErrorMessage(e); 94 | if (Stacktrace const * const p = boost::get_error_info(e)) { 95 | if (p->size() != 0) { 96 | std::cerr << "Backtrace:\n" << *p; 97 | } 98 | } 99 | } 100 | catch (std::exception const &e) { 101 | std::cerr << "`std::terminate' is called after throwing an instance of `" 102 | << Kanachan::getTypeName(e) << "'.\n" << e.what() << '\n'; 103 | } 104 | catch (...) { 105 | std::cerr << "`std::terminate' is called after throwing an instance of an unknown type.\n"; 106 | } 107 | nested_exceptions.pop_back(); 108 | 109 | while (!nested_exceptions.empty()) { 110 | try { 111 | std::rethrow_exception(nested_exceptions.back()); 112 | } 113 | catch (boost::exception const &e) { 114 | std::cerr << "A nesting exception of type `" << Kanachan::getTypeName(e) << "'.\n"; 115 | printErrorMessage(e); 116 | } 117 | catch (std::exception const &e) { 118 | std::cerr << "A nesting exception of type `" << Kanachan::getTypeName(e) << "'.\n"; 119 | std::cerr << e.what() << '\n'; 120 | } 121 | catch (...) { 122 | std::cerr << "A nesting exception of an unknown type.\n"; 123 | } 124 | nested_exceptions.pop_back(); 125 | } 126 | 127 | std::cerr << std::flush; 128 | #if defined(KANACHAN_WITH_COVERAGE) 129 | __gcov_flush(); std::abort(); 130 | #else // defined(KANACHAN_WITH_COVERAGE) 131 | std::abort(); 132 | #endif // defined(KANACHAN_WITH_COVERAGE) 133 | } 134 | catch (...) { 135 | #if defined(KANACHAN_WITH_COVERAGE) 136 | __gcov_flush(); std::abort(); 137 | #else // defined(KANACHAN_WITH_COVERAGE) 138 | std::abort(); 139 | #endif // defined(KANACHAN_WITH_COVERAGE) 140 | } 141 | 142 | TerminateHandlerSetter::TerminateHandlerSetter() noexcept 143 | { 144 | std::set_terminate(&terminate_handler_); 145 | } 146 | 147 | } // namespace Kanachan::Detail_ 148 | -------------------------------------------------------------------------------- /kanachan/training/bert/encoder.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.utils.checkpoint import checkpoint_sequential 4 | from kanachan.training.constants import ( 5 | NUM_TYPES_OF_SPARSE_FEATURES, MAX_NUM_ACTIVE_SPARSE_FEATURES, NUM_NUMERIC_FEATURES, 6 | NUM_TYPES_OF_PROGRESSION_FEATURES, MAX_LENGTH_OF_PROGRESSION_FEATURES, 7 | NUM_TYPES_OF_ACTIONS, MAX_NUM_ACTION_CANDIDATES 8 | ) 9 | from kanachan.training.positional_encoding import PositionalEncoding 10 | from kanachan.training.position_embedding import PositionEmbedding 11 | 12 | 13 | class Encoder(nn.Module): 14 | def __init__( 15 | self, *, position_encoder: str, dimension: int, num_heads: int, dim_feedforward: int, 16 | num_layers: int, activation_function: str, dropout: float, checkpointing: bool, 17 | device: torch.device, dtype: torch.dtype) -> None: 18 | if position_encoder not in ('positional_encoding', 'position_embedding'): 19 | raise ValueError(position_encoder) 20 | if dimension < 1: 21 | raise ValueError(dimension) 22 | if num_heads < 1: 23 | raise ValueError(num_heads) 24 | if dim_feedforward < 1: 25 | raise ValueError(dim_feedforward) 26 | if num_layers < 1: 27 | raise ValueError(num_layers) 28 | if activation_function not in ('relu', 'gelu'): 29 | raise ValueError(activation_function) 30 | if dropout < 0.0 or 1.0 <= dropout: 31 | raise ValueError(dropout) 32 | 33 | super(Encoder, self).__init__() 34 | 35 | self.sparse_embedding = nn.Embedding( 36 | NUM_TYPES_OF_SPARSE_FEATURES + 1, dimension, padding_idx=NUM_TYPES_OF_SPARSE_FEATURES, 37 | device=device, dtype=dtype) 38 | 39 | self.numeric_embedding = nn.Parameter( 40 | torch.randn(NUM_NUMERIC_FEATURES, dimension - 1, device=device, dtype=dtype)) 41 | 42 | self.progression_embedding = nn.Embedding( 43 | NUM_TYPES_OF_PROGRESSION_FEATURES + 1, dimension, 44 | padding_idx=NUM_TYPES_OF_PROGRESSION_FEATURES, device=device, dtype=dtype) 45 | if position_encoder == 'positional_encoding': 46 | self.position_encoder = PositionalEncoding( 47 | max_length=MAX_LENGTH_OF_PROGRESSION_FEATURES, dimension=dimension, dropout=dropout, 48 | device=device, dtype=dtype) 49 | elif position_encoder == 'position_embedding': 50 | self.position_encoder = PositionEmbedding( 51 | max_length=MAX_LENGTH_OF_PROGRESSION_FEATURES, dimension=dimension, dropout=dropout, 52 | device=device, dtype=dtype) 53 | else: 54 | raise NotImplementedError(position_encoder) 55 | 56 | self.candidates_embedding = nn.Embedding( 57 | NUM_TYPES_OF_ACTIONS + 2, dimension, padding_idx=NUM_TYPES_OF_ACTIONS + 1, 58 | device=device, dtype=dtype) 59 | 60 | encoder_layer = nn.TransformerEncoderLayer( 61 | dimension, num_heads, dim_feedforward=dim_feedforward, activation=activation_function, 62 | dropout=dropout, batch_first=True, device=device, dtype=dtype) 63 | self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) 64 | 65 | self.checkpointing = checkpointing 66 | 67 | def forward( 68 | self, sparse: torch.Tensor, numeric: torch.Tensor, progression: torch.Tensor, 69 | candidates: torch.Tensor) -> torch.Tensor: 70 | assert sparse.dim() == 2 71 | assert sparse.size(1) == MAX_NUM_ACTIVE_SPARSE_FEATURES 72 | assert numeric.dim() == 2 73 | assert numeric.size(1) == NUM_NUMERIC_FEATURES 74 | assert progression.dim() == 2 75 | assert progression.size(1) == MAX_LENGTH_OF_PROGRESSION_FEATURES 76 | assert candidates.dim() == 2 77 | assert candidates.size(1) == MAX_NUM_ACTION_CANDIDATES 78 | assert sparse.size(0) == numeric.size(0) 79 | assert sparse.size(0) == progression.size(0) 80 | assert sparse.size(0) == candidates.size(0) 81 | 82 | sparse = self.sparse_embedding(sparse) 83 | 84 | numeric = torch.unsqueeze(numeric, 2) 85 | numeric_embedding = torch.unsqueeze(self.numeric_embedding, 0) 86 | numeric_embedding = numeric_embedding.expand(numeric.size(0), -1, -1) 87 | numeric = torch.cat((numeric, numeric_embedding), 2) 88 | 89 | progression = self.progression_embedding(progression) 90 | progression = self.position_encoder(progression) 91 | 92 | candidates = self.candidates_embedding(candidates) 93 | 94 | embedding = torch.cat((sparse, numeric, progression, candidates), 1) 95 | 96 | if self.checkpointing: 97 | encoder_layers = self.encoder.layers 98 | encode = checkpoint_sequential(encoder_layers, len(encoder_layers), embedding) 99 | else: 100 | encode = self.encoder(embedding) 101 | 102 | return encode 103 | -------------------------------------------------------------------------------- /src/annotation/player_state.hpp: -------------------------------------------------------------------------------- 1 | #if !defined(KANACHAN_ANNOTATION_PLAYER_STATE_HPP_INCLUDE_GUARD) 2 | #define KANACHAN_ANNOTATION_PLAYER_STATE_HPP_INCLUDE_GUARD 3 | 4 | #include "common/throw.hpp" 5 | #include "common/mahjongsoul.pb.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace Kanachan{ 13 | 14 | class PostZimoAction; 15 | 16 | class PlayerState 17 | { 18 | private: 19 | friend class Kanachan::PostZimoAction; 20 | 21 | public: 22 | PlayerState( 23 | std::uint_fast8_t seat, 24 | std::uint_fast32_t mode_id, 25 | std::uint_fast32_t level, 26 | std::uint_fast8_t game_rank, 27 | std::int_fast32_t game_score, 28 | std::int_fast32_t delta_grading_point); 29 | 30 | PlayerState(PlayerState const &) = default; 31 | 32 | PlayerState &operator=(PlayerState const &) = delete; 33 | 34 | void onNewRound(lq::RecordNewRound const &record); 35 | 36 | void onZimo(lq::RecordDealTile const &record); 37 | 38 | private: 39 | void handIn_(); 40 | 41 | public: 42 | void onDapai(lq::RecordDiscardTile const &record); 43 | 44 | private: 45 | void onMyChi_(std::array const &tiles); 46 | 47 | void onMyPeng_(std::array const &tiles); 48 | 49 | void onMyDaminggang_(std::array const &tiles); 50 | 51 | public: 52 | void onChiPengGang(lq::RecordChiPengGang const &record); 53 | 54 | private: 55 | void onMyJiagang_(std::uint_fast8_t tile); 56 | 57 | void onMyAngang_(std::uint_fast8_t tile); 58 | 59 | public: 60 | void onGang(lq::RecordAnGangAddGang const &record); 61 | 62 | void onLiuju( 63 | lq::RecordLiuJu const &record, std::uint_fast8_t seat, 64 | lq::RecordNewRound const &next_record); 65 | 66 | std::uint_fast8_t getSeat() const; 67 | 68 | std::uint_fast8_t getLeftTileCount() const; 69 | 70 | std::uint_fast8_t getLevel() const; 71 | 72 | std::uint_fast8_t getRank( 73 | std::array const &player_states) const; 74 | 75 | std::int_fast32_t getInitialScore() const; 76 | 77 | std::int_fast32_t getCurrentScore() const; 78 | 79 | std::vector getDiscardableTiles() const; 80 | 81 | std::uint_fast8_t getZimopai() const; 82 | 83 | std::uint_fast8_t getGameRank() const; 84 | 85 | std::int_fast32_t getGameScore() const; 86 | 87 | std::int_fast32_t getDeltaGradingPoint() const; 88 | 89 | void print(std::array const &player_states, 90 | std::ostream &os) const; 91 | 92 | private: 93 | static constexpr std::array hand_offset_{ 94 | 0u, 1u, 5u , 9u, 13u, 17u, 20u, 24u, 28u, 32u, 95 | 36u, 37u, 41u, 45u, 49u, 53u, 56u, 60u, 64u, 68u, 96 | 72u, 73u, 77u, 81u, 85u, 89u, 92u, 96u, 100u, 104u, 97 | 108u, 112u, 116u, 120u, 124u, 128u, 132u, 136u 98 | }; 99 | 100 | // Match state 101 | std::uint_fast8_t room_ = std::numeric_limits::max(); // [0u, 5u) 102 | std::uint_fast8_t num_rounds_type_ = std::numeric_limits::max(); // [0u, 2u) 103 | std::uint_fast8_t seat_ = std::numeric_limits::max(); // [0u, 4u) 104 | 105 | // Round state 106 | std::uint_fast8_t chang_ = std::numeric_limits::max(); // [0u, 3u) 107 | std::uint_fast8_t ju_ = std::numeric_limits::max(); // [0u, 4u) 108 | std::uint_fast8_t ben_ = std::numeric_limits::max(); // integral 109 | std::uint_fast8_t liqibang_ = std::numeric_limits::max(); // integral 110 | std::array dora_indicators_{ 111 | std::numeric_limits::max(), // [0, 37u) 112 | std::numeric_limits::max(), // [0, 37u), optional 113 | std::numeric_limits::max(), // [0, 37u), optional 114 | std::numeric_limits::max(), // [0, 37u), optional 115 | std::numeric_limits::max() // [0, 37u), optional 116 | }; 117 | std::uint_fast8_t count_ = std::numeric_limits::max(); // [0u, 70u) 118 | 119 | // Player state 120 | std::uint_fast8_t level_ = std::numeric_limits::max(); // [0u, 16u) 121 | std::int_fast32_t initial_score_ = std::numeric_limits::max(); // integral 122 | std::int_fast32_t score_ = std::numeric_limits::max(); // integral 123 | std::array hand_{}; // combination 124 | std::uint_fast8_t zimo_pai_ = std::numeric_limits::max(); // [0u, 37u), optional 125 | 126 | // Match result 127 | std::uint_fast8_t game_rank_ = std::numeric_limits::max(); // [0, 4u) 128 | std::int_fast32_t game_score_ = std::numeric_limits::max(); // integral 129 | std::int_fast32_t delta_grading_point_ = std::numeric_limits::max(); // integral 130 | }; // class PlayerState 131 | 132 | } // namespace Kanachan 133 | 134 | #endif // !defined(KANACHAN_ANNOTATION_PLAYER_STATE_HPP_INCLUDE_GUARD) 135 | --------------------------------------------------------------------------------