├── .gitignore ├── LICENSE ├── README.md ├── aiwolf ├── __init__.py ├── agent.py ├── client.py ├── constant.py ├── content.py ├── gameinfo.py ├── gamesetting.py ├── judge.py ├── player.py ├── py.typed ├── utterance.py └── vote.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiwolf package 2 | The Python package for creating AI Werewolf agent. 3 | ## How to use 4 | Simply install aiwolf package from this repogitory using pip as follows. 5 | ``` 6 | pip install git+https://github.com/AIWolfSharp/aiwolf-python.git 7 | ``` 8 | ## Sample 9 | Sample agent which uses this package is published at [AIWolfSharp/sample-python-agent](https://github.com/AIWolfSharp/sample-python-agent). 10 | ## Tutorial 11 | * You can see a tutorial for making Python agent [here](http://aiwolf.org/en/howtowagent). 12 | ## Links 13 | * [Artificial Intelligence based Werewolf|Let's play "Werewolf" with AI!](http://aiwolf.org/en/) 14 | ## HISTORY 15 | * Version 1.3.0: Add Topic.RAW and RawContentBuilder for NL utterances. 16 | * Version 1.2.0: Create new property alive_agent_list in GameInfo. 17 | * Version 1.1.0: Create agent_list property in GameInfo. 18 | * Version 1.0.0: The initial release. -------------------------------------------------------------------------------- /aiwolf/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # __init__.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | from aiwolf.agent import Agent as Agent 18 | from aiwolf.agent import Role as Role 19 | from aiwolf.agent import Species as Species 20 | from aiwolf.agent import Status as Status 21 | from aiwolf.client import TcpipClient as TcpipClient 22 | from aiwolf.constant import Constant as Constant 23 | from aiwolf.content import AgreeContentBuilder as AgreeContentBuilder 24 | from aiwolf.content import AndContentBuilder as AndContentBuilder 25 | from aiwolf.content import AttackContentBuilder as AttackContentBuilder 26 | from aiwolf.content import AttackedContentBuilder as AttackedContentBuilder 27 | from aiwolf.content import BecauseContentBuilder as BecauseContentBuilder 28 | from aiwolf.content import ComingoutContentBuilder as ComingoutContentBuilder 29 | from aiwolf.content import Content as Content 30 | from aiwolf.content import ContentBuilder as ContentBuilder 31 | from aiwolf.content import DayContentBuilder as DayContentBuilder 32 | from aiwolf.content import DisagreeContentBuilder as DisagreeContentBuilder 33 | from aiwolf.content import DivinationContentBuilder as DivinationContentBuilder 34 | from aiwolf.content import DivinedResultContentBuilder as DivinedResultContentBuilder 35 | from aiwolf.content import EmptyContentBuilder as EmptyContentBuilder 36 | from aiwolf.content import EstimateContentBuilder as EstimateContentBuilder 37 | from aiwolf.content import GuardContentBuilder as GuardContentBuilder 38 | from aiwolf.content import GuardedAgentContentBuilder as GuardedAgentContentBuilder 39 | from aiwolf.content import IdentContentBuilder as IdentContentBuilder 40 | from aiwolf.content import InquiryContentBuilder as InquiryContentBuilder 41 | from aiwolf.content import NotContentBuilder as NotContentBuilder 42 | from aiwolf.content import Operator as Operator 43 | from aiwolf.content import OrContentBuilder as OrContentBuilder 44 | from aiwolf.content import OverContentBuilder as OverContentBuilder 45 | from aiwolf.content import RequestContentBuilder as RequestContentBuilder 46 | from aiwolf.content import SkipContentBuilder as SkipContentBuilder 47 | from aiwolf.content import Topic as Topic 48 | from aiwolf.content import VoteContentBuilder as VoteContentBuilder 49 | from aiwolf.content import VotedContentBuilder as VotedContentBuilder 50 | from aiwolf.content import XorContentBuilder as XorContentBuilder 51 | from aiwolf.gameinfo import GameInfo as GameInfo 52 | from aiwolf.gamesetting import GameSetting as GameSetting 53 | from aiwolf.judge import Judge as Judge 54 | from aiwolf.player import AbstractPlayer as AbstractPlayer 55 | from aiwolf.utterance import Talk as Talk 56 | from aiwolf.utterance import Utterance as Utterance 57 | from aiwolf.utterance import UtteranceType as UtteranceType 58 | from aiwolf.utterance import Whisper as Whisper 59 | from aiwolf.vote import Vote as Vote 60 | -------------------------------------------------------------------------------- /aiwolf/agent.py: -------------------------------------------------------------------------------- 1 | # 2 | # agent.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """agent module.""" 18 | from __future__ import annotations 19 | 20 | import re 21 | from enum import Enum 22 | from typing import ClassVar, Match, Optional, Pattern 23 | 24 | 25 | class Agent: 26 | """A player agent in AIWolf game.""" 27 | 28 | _agent_map: ClassVar[dict[int, Agent]] = {} 29 | 30 | _agent_pattern: ClassVar[Pattern[str]] = re.compile(r"(Agent\[(\d+)\]|ANY)") 31 | 32 | @staticmethod 33 | def compile(input: str) -> Agent: 34 | """Convert the string into the corresponding Agent. 35 | 36 | Args: 37 | input: The string representing an Agent. 38 | 39 | Returns: 40 | The Agent converted from the given string. 41 | """ 42 | m: Optional[Match[str]] = Agent._agent_pattern.match(input) 43 | if m: 44 | if m.group(1) == "ANY": 45 | return Agent(0xff) 46 | else: 47 | return Agent(int(m.group(2))) 48 | return Agent(0) 49 | 50 | def __new__(cls: type[Agent], idx: int) -> Agent: 51 | if idx < 0: 52 | raise ValueError("agent index must not be negative") 53 | if idx in cls._agent_map.keys(): 54 | return cls._agent_map[idx] 55 | cls._agent_map[idx] = super().__new__(cls) 56 | return cls._agent_map[idx] 57 | 58 | def __init__(self, idx: int) -> None: 59 | """Initialize a new instance of Agent. 60 | 61 | Args: 62 | idx: The index number of the Agent. 63 | """ 64 | self._agent_idx: int = idx 65 | 66 | @property 67 | def agent_idx(self) -> int: 68 | """The index number of this Agent.""" 69 | return self._agent_idx 70 | 71 | def __str__(self) -> str: 72 | return "Agent[" + "{:02}".format(self.agent_idx) + "]" 73 | 74 | 75 | class Role(Enum): 76 | """Enumeration type for role.""" 77 | 78 | UNC = "UNC" 79 | """Uncertain.""" 80 | 81 | BODYGUARD = "BODYGUARD" 82 | """Bodyguard.""" 83 | 84 | FOX = "FOX" 85 | """Fox.""" 86 | 87 | FREEMASON = "FREEMASON" 88 | """Freemason.""" 89 | 90 | MEDIUM = "MEDIUM" 91 | """Medium.""" 92 | 93 | POSSESSED = "POSSESSED" 94 | """Possessed human.""" 95 | 96 | SEER = "SEER" 97 | """Seer.""" 98 | 99 | VILLAGER = "VILLAGER" 100 | """Villager.""" 101 | 102 | WEREWOLF = "WEREWOLF" 103 | """Werewolf.""" 104 | 105 | ANY = "ANY" 106 | """Wildcard.""" 107 | 108 | 109 | class Species(Enum): 110 | """Enumeration type for species.""" 111 | 112 | UNC = "UNC" 113 | """Uncertain.""" 114 | 115 | HUMAN = "HUMAN" 116 | """Human.""" 117 | 118 | WEREWOLF = "WEREWOLF" 119 | """Werewolf.""" 120 | 121 | ANY = "ANY" 122 | """Wildcard.""" 123 | 124 | 125 | class Status(Enum): 126 | """Enumeration type for player's status (ie. alive or dead).""" 127 | 128 | UNC = "UNC" 129 | """Uncertain.""" 130 | 131 | ALIVE = "ALIVE" 132 | """Alive.""" 133 | 134 | DEAD = "DEAD" 135 | """Dead.""" 136 | -------------------------------------------------------------------------------- /aiwolf/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # client.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """client module.""" 18 | import json 19 | import socket 20 | from typing import Optional, TypedDict 21 | 22 | from aiwolf.gameinfo import GameInfo, _GameInfo 23 | from aiwolf.gamesetting import GameSetting, _GameSetting 24 | from aiwolf.player import AbstractPlayer 25 | from aiwolf.utterance import Talk, Whisper, _Utterance 26 | 27 | 28 | class _Packet(TypedDict): 29 | gameInfo: Optional[_GameInfo] 30 | gameSetting: Optional[_GameSetting] 31 | request: str 32 | talkHistory: Optional[list[_Utterance]] 33 | whisperHistory: Optional[list[_Utterance]] 34 | 35 | 36 | class TcpipClient: 37 | """Client agent that communiates with the server via TCP/IP connection.""" 38 | 39 | def __init__(self, player: AbstractPlayer, name: Optional[str], host: str, port: int, request_role: str) -> None: 40 | """Initialize a new instance of TcpipClient. 41 | 42 | Args: 43 | player: An AbstractPlayer to be connect with the server. 44 | name: The name of the player agent. 45 | host: The hostname of the server. 46 | port: The port number the server is waiting on. 47 | request_role: The name of role that the player agent wants to be. 48 | """ 49 | self.player: AbstractPlayer = player 50 | self.name: Optional[str] = name 51 | self.host: str = host 52 | self.port: int = port 53 | self.request_role: str = request_role 54 | self.game_info: Optional[GameInfo] = None 55 | self.last_game_info: Optional[GameInfo] = None 56 | self.sock: Optional[socket.socket] = None 57 | 58 | def _send_response(self, response: Optional[str]) -> None: 59 | if isinstance(self.sock, socket.socket) and isinstance(response, str): 60 | self.sock.send((response + "\n").encode("utf-8")) 61 | 62 | def _get_response(self, packet: _Packet) -> Optional[str]: 63 | request: str = packet["request"] 64 | if request == "NAME": 65 | return self.name if self.name is not None else self.player.get_name() 66 | elif request == "ROLE": 67 | return self.request_role 68 | game_info0: Optional[_GameInfo] = packet["gameInfo"] 69 | self.game_info = GameInfo(game_info0) if game_info0 is not None else None 70 | if self.game_info is None: 71 | self.game_info = self.last_game_info 72 | else: 73 | self.last_game_info = self.game_info 74 | if self.game_info is None: 75 | return None 76 | talk_history0: Optional[list[_Utterance]] = packet["talkHistory"] 77 | if talk_history0 is not None: 78 | for talk0 in talk_history0: 79 | talk: Talk = Talk.compile(talk0) 80 | talk_list: list[Talk] = self.game_info.talk_list 81 | if len(talk_list) == 0: 82 | talk_list.append(talk) 83 | else: 84 | last_talk: Talk = talk_list[-1] 85 | if talk.day > last_talk.day or (talk.day == last_talk.day and talk.idx > last_talk.idx): 86 | talk_list.append(talk) 87 | whisper_history0: Optional[list[_Utterance]] = packet["whisperHistory"] 88 | if whisper_history0 is not None: 89 | for whisper0 in whisper_history0: 90 | whisper: Whisper = Whisper.compile(whisper0) 91 | whisper_list: list[Whisper] = self.game_info.whisper_list 92 | if len(whisper_list) == 0: 93 | whisper_list.append(whisper) 94 | else: 95 | last_whisper: Whisper = whisper_list[-1] 96 | if whisper.day > last_whisper.day or (whisper.day == last_whisper.day and whisper.idx > last_whisper.idx): 97 | whisper_list.append(whisper) 98 | if request == "INITIALIZE": 99 | game_setting0: Optional[_GameSetting] = packet["gameSetting"] 100 | if game_setting0 is not None: 101 | self.player.initialize(self.game_info, GameSetting(game_setting0)) 102 | return None 103 | else: 104 | self.player.update(self.game_info) 105 | if request == "DAILY_INITIALIZE": 106 | self.player.day_start() 107 | return None 108 | elif request == "DAILY_FINISH": 109 | return None 110 | elif request == "FINISH": 111 | self.player.finish() 112 | return None 113 | elif request == "VOTE": 114 | return json.dumps({"agentIdx": self.player.vote().agent_idx}, separators=(",", ":")) 115 | elif request == "ATTACK": 116 | return json.dumps({"agentIdx": self.player.attack().agent_idx}, separators=(",", ":")) 117 | elif request == "GUARD": 118 | return json.dumps({"agentIdx": self.player.guard().agent_idx}, separators=(",", ":")) 119 | elif request == "DIVINE": 120 | return json.dumps({"agentIdx": self.player.divine().agent_idx}, separators=(",", ":")) 121 | elif request == "TALK": 122 | return self.player.talk().text 123 | elif request == "WHISPER": 124 | return self.player.whisper().text 125 | return None 126 | 127 | def connect(self) -> None: 128 | """Connect to the server.""" 129 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 130 | self.sock.settimeout(0.001) 131 | self.sock.connect((self.host, self.port)) 132 | line: str = "" 133 | while True: 134 | try: 135 | line += self.sock.recv(8192).decode("utf-8") 136 | if line == "": 137 | break 138 | except socket.timeout: 139 | pass 140 | line_list: list[str] = line.split("\n", 1) 141 | for i in range(len(line_list) - 1): 142 | if len(line_list[i]) > 0: 143 | self._send_response(self._get_response(json.loads(line_list[i]))) 144 | line = line_list[-1] 145 | try: 146 | self._send_response(self._get_response(json.loads(line))) 147 | line = "" 148 | except ValueError: 149 | pass 150 | self.sock.close() 151 | return None 152 | -------------------------------------------------------------------------------- /aiwolf/constant.py: -------------------------------------------------------------------------------- 1 | # 2 | # constant.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """constant module.""" 18 | import warnings 19 | from typing import Final 20 | 21 | from aiwolf.agent import Agent 22 | 23 | AGENT_NONE: Final[Agent] = Agent(0) 24 | AGENT_UNSPEC: Final[Agent] = AGENT_NONE 25 | AGENT_ANY: Final[Agent] = Agent(0xff) 26 | 27 | 28 | class Constant: 29 | """Constant class that defines some constants.""" 30 | 31 | warnings.warn("Constant class will be deprecated in the next version.", PendingDeprecationWarning) 32 | 33 | AGENT_NONE: Final[Agent] = Agent(0) 34 | """Agent that does not exisit in this game.""" 35 | 36 | AGENT_UNSPEC: Final[Agent] = AGENT_NONE 37 | """Agent that means no agent specified. (Alias of AGENT_NONE)""" 38 | 39 | AGENT_ANY: Final[Agent] = Agent(0xff) 40 | """Agent that means any of the agents in this game.""" 41 | -------------------------------------------------------------------------------- /aiwolf/content.py: -------------------------------------------------------------------------------- 1 | # 2 | # content.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """content module.""" 18 | from __future__ import annotations 19 | 20 | import copy 21 | import re 22 | from enum import Enum 23 | from typing import ClassVar, Match, Optional, Pattern 24 | 25 | from aiwolf.agent import Agent, Role, Species 26 | from aiwolf.constant import AGENT_ANY, AGENT_NONE, AGENT_UNSPEC 27 | from aiwolf.utterance import Talk, Utterance, UtteranceType, Whisper 28 | 29 | 30 | class Content: 31 | """Content class expressing the content of an uteerance.""" 32 | 33 | @staticmethod 34 | def _get_contents(input: str) -> list[Content]: 35 | return [Content.compile(s) for s in Content._get_content_strings(input)] 36 | 37 | @staticmethod 38 | def _get_content_strings(input: str) -> list[str]: 39 | strings: list[str] = [] 40 | length: int = len(input) 41 | stack_ptr: int = 0 42 | start: int = 0 43 | for i in range(0, length): 44 | if input[i] == "(": 45 | if stack_ptr == 0: 46 | start = i 47 | stack_ptr += 1 48 | elif input[i] == ")": 49 | stack_ptr -= 1 50 | if stack_ptr == 0: 51 | strings.append(input[start+1:i]) 52 | return strings 53 | 54 | def __init__(self, builder: ContentBuilder) -> None: 55 | """Initialize a new instance of Content. 56 | 57 | Args: 58 | builder: A ContentBuilder used for initialization. 59 | """ 60 | self.topic: Topic = builder._topic 61 | """The topic of this Content.""" 62 | 63 | self.subject: Agent = builder._subject 64 | """The Agent that is the subject of this Content.""" 65 | 66 | self.target: Agent = builder._target 67 | """The Agent that is the object of this Content.""" 68 | 69 | self.role: Role = builder._role 70 | """The role this Content refers to.""" 71 | 72 | self.result: Species = builder._result 73 | """The species this Content refers to.""" 74 | 75 | self.utterance: Utterance = builder._utterance 76 | """The utterance this Content refers to.""" 77 | 78 | self.operator: Operator = builder._operator 79 | """The operator in this Content.""" 80 | 81 | self.content_list: list[Content] = builder._content_list 82 | """The list of the operands in this Content.""" 83 | 84 | self.day: int = builder._day 85 | """The date added to the operand in this Content.""" 86 | 87 | self.text: str = "" 88 | """The text representing this Content.""" 89 | 90 | self._complete_inner_subject() 91 | self._normalize_text() 92 | 93 | def _complete_inner_subject(self) -> None: 94 | if not self.content_list: 95 | return 96 | self.content_list = [self._process_inner_content(c) for c in self.content_list] 97 | 98 | def _process_inner_content(self, inner: Content) -> Content: 99 | if inner.subject is AGENT_UNSPEC: 100 | if self.operator is Operator.INQUIRE or self.operator is Operator.REQUEST: 101 | return inner._copy_and_replace_subject(self.target) 102 | if self.subject is not AGENT_UNSPEC: 103 | return inner._copy_and_replace_subject(self.subject) 104 | inner._complete_inner_subject() 105 | return inner 106 | 107 | def _copy_and_replace_subject(self, new_subject: Agent) -> Content: 108 | c: Content = self.clone() 109 | c.subject = new_subject 110 | c._complete_inner_subject() 111 | c._normalize_text() 112 | return c 113 | 114 | def _normalize_text(self) -> None: 115 | str_sub: str = "" if self.subject is AGENT_UNSPEC else "ANY " if self.subject is AGENT_ANY else str(self.subject)+" " 116 | str_tgt: str = "ANY" if self.target is AGENT_ANY or self.target is AGENT_UNSPEC else str(self.target) 117 | if self.topic is not Topic.OPERATOR: 118 | if self.topic is Topic.DUMMY: 119 | self.text = "" 120 | elif self.topic is Topic.RAW: 121 | pass 122 | elif self.topic is Topic.Skip: 123 | self.text = Utterance.SKIP 124 | elif self.topic is Topic.Over: 125 | self.text = Utterance.OVER 126 | elif self.topic is Topic.AGREE or self.topic is Topic.DISAGREE: 127 | self.text = str_sub + " ".join([self.topic.value, "TALK" if type(self.utterance) is Talk else "WHISPER", str(self.utterance.day), str(self.utterance.idx)]) 128 | elif self.topic is Topic.ESTIMATE or self.topic is Topic.COMINGOUT: 129 | self.text = str_sub + " ".join([self.topic.value, str_tgt, self.role.value]) 130 | elif self.topic is Topic.DIVINED or self.topic is Topic.IDENTIFIED: 131 | self.text = str_sub + " ".join([self.topic.value, str_tgt, self.result.value]) 132 | elif self.topic is Topic.ATTACK or self.topic is Topic.ATTACKED or self.topic is Topic.DIVINATION or self.topic is Topic.GUARD\ 133 | or self.topic is Topic.GUARDED or self.topic is Topic.VOTE or self.topic is Topic.VOTED: 134 | self.text = str_sub + " ".join([self.topic.value, str_tgt]) 135 | else: 136 | if self.operator is Operator.REQUEST or self.operator is Operator.INQUIRE: 137 | self.text = str_sub + " ".join([self.operator.value, str_tgt, "("+Content._strip_subject(self.content_list[0].text) + 138 | ")" if self.content_list[0].subject is self.target else "("+self.content_list[0].text+")"]) 139 | elif self.operator is Operator.BECAUSE or self.operator is Operator.XOR: 140 | self.text = str_sub + " ".join([self.operator.value] + ["("+Content._strip_subject(self.content_list[i].text) + 141 | ")" if self.content_list[i].subject is self.subject else "("+self.content_list[i].text+")" for i in [0, 1]]) 142 | elif self.operator is Operator.AND or self.operator is Operator.OR: 143 | self.text = str_sub + " ".join([self.operator.value] + ["("+Content._strip_subject(c.text) + ")" if c.subject is self.subject else "("+c.text+")" for c in self.content_list]) 144 | elif self.operator is Operator.NOT: 145 | self.text = str_sub + " ".join([self.operator.value, "("+Content._strip_subject(self.content_list[0].text) + 146 | ")" if self.content_list[0].subject is self.subject else "("+self.content_list[0].text+")"]) 147 | elif self.operator is Operator.DAY: 148 | self.text = str_sub + " ".join([self.operator.value, str(self.day), "("+Content._strip_subject(self.content_list[0].text) + 149 | ")" if self.content_list[0].subject is self.subject else "("+self.content_list[0].text+")"]) 150 | 151 | _strip_pattern: ClassVar[Pattern[str]] = re.compile(r"^(Agent\[\d+\]|ANY|)\s*([A-Z]+.*)$") 152 | 153 | @staticmethod 154 | def _strip_subject(input: str) -> str: 155 | m: Optional[Match[str]] = Content._strip_pattern.match(input) 156 | if m: 157 | return m.group(2) 158 | return input 159 | 160 | def clone(self) -> Content: 161 | """Clone this Content. 162 | 163 | Returns: 164 | The cloned Content. 165 | """ 166 | content: Content = copy.copy(self) 167 | content.utterance = copy.copy(self.utterance) 168 | content.content_list = [c.clone() for c in self.content_list] 169 | return content 170 | 171 | _regex_agent: ClassVar[str] = r"\s+(Agent\[\d+\]|ANY)" 172 | _regex_subject: ClassVar[str] = r"^(Agent\[\d+\]|ANY|)\s*" 173 | _regex_talk: ClassVar[str] = r"\s+([A-Z]+)\s+day(\d+)\s+ID:(\d+)" 174 | _regex_role_species: ClassVar[str] = r"\s+([A-Z]+)" 175 | _regex_paren: ClassVar[str] = r"(\(.*\))" 176 | _regex_digit: ClassVar[str] = r"(\d+)" 177 | _agree_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + "(AGREE|DISAGREE)" + _regex_talk + "$") 178 | _estimate_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + "(ESTIMATE|COMINGOUT)" + _regex_agent + _regex_role_species + "$") 179 | _divined_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + "(DIVINED|IDENTIFIED)" + _regex_agent + _regex_role_species + "$") 180 | _attack_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + "(ATTACK|ATTACKED|DIVINATION|GUARD|GUARDED|VOTE|VOTED)" + _regex_agent + "$") 181 | _request_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + "(REQUEST|INQUIRE)" + _regex_agent + r"\s+" + _regex_paren + "$") 182 | _because_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + r"(BECAUSE|AND|OR|XOR|NOT|REQUEST)\s+" + _regex_paren + "$") 183 | _day_pattern: ClassVar[Pattern[str]] = re.compile(_regex_subject + r"DAY\s+" + _regex_digit + r"\s+" + _regex_paren + "$") 184 | _skip_pattern: ClassVar[Pattern[str]] = re.compile("^(Skip|Over)$") 185 | 186 | @staticmethod 187 | def compile(text: str) -> Content: 188 | """Convert the uttered text into a Content. 189 | 190 | Args: 191 | text: The uttered text. 192 | 193 | Returns: 194 | The Content converted from the given text. 195 | """ 196 | trimmed: str = text.strip() 197 | m_agree: Optional[Match[str]] = Content._agree_pattern.match(trimmed) 198 | m_estimate: Optional[Match[str]] = Content._estimate_pattern.match(trimmed) 199 | m_divined: Optional[Match[str]] = Content._divined_pattern.match(trimmed) 200 | m_attack: Optional[Match[str]] = Content._attack_pattern.match(trimmed) 201 | m_request: Optional[Match[str]] = Content._request_pattern.match(trimmed) 202 | m_because: Optional[Match[str]] = Content._because_pattern.match(trimmed) 203 | m_day: Optional[Match[str]] = Content._day_pattern.match(trimmed) 204 | m_skip: Optional[Match[str]] = Content._skip_pattern.match(trimmed) 205 | content: Content = Content(SkipContentBuilder()) 206 | if m_skip: 207 | content.topic = Topic[m_skip.group(1)] 208 | elif m_agree: 209 | content.subject = Agent.compile(m_agree.group(1)) 210 | content.topic = Topic[m_agree.group(2)] 211 | if UtteranceType[m_agree.group(3)] is UtteranceType.TALK: 212 | content.utterance = Talk(int(m_agree.group(5)), AGENT_NONE, int(m_agree.group(4)), "", 0) 213 | else: 214 | content.utterance = Whisper(int(m_agree.group(5)), AGENT_NONE, int(m_agree.group(4)), "", 0) 215 | elif m_estimate: 216 | content.subject = Agent.compile(m_estimate.group(1)) 217 | content.topic = Topic[m_estimate.group(2)] 218 | content.target = Agent.compile(m_estimate.group(3)) 219 | content.role = Role[m_estimate.group(4)] 220 | elif m_divined: 221 | content.subject = Agent.compile(m_divined.group(1)) 222 | content.topic = Topic[m_divined.group(2)] 223 | content.target = Agent.compile(m_divined.group(3)) 224 | content.result = Species[m_divined.group(4)] 225 | elif m_attack: 226 | content.subject = Agent.compile(m_attack.group(1)) 227 | content.topic = Topic[m_attack.group(2)] 228 | content.target = Agent.compile(m_attack.group(3)) 229 | elif m_request: 230 | content.topic = Topic.OPERATOR 231 | content.subject = Agent.compile(m_request.group(1)) 232 | content.operator = Operator[m_request.group(2)] 233 | content.target = Agent.compile(m_request.group(3)) 234 | content.content_list = Content._get_contents(m_request.group(4)) 235 | elif m_because: 236 | content.topic = Topic.OPERATOR 237 | content.subject = Agent.compile(m_because.group(1)) 238 | content.operator = Operator[m_because.group(2)] 239 | content.content_list = Content._get_contents(m_because.group(3)) 240 | if content.operator is Operator.REQUEST: 241 | content.target = AGENT_ANY if content.content_list[0].subject is AGENT_UNSPEC else content.content_list[0].subject 242 | elif m_day: 243 | content.topic = Topic.OPERATOR 244 | content.subject = Agent.compile(m_day.group(1)) 245 | content.operator = Operator.DAY 246 | content.day = int(m_day.group(2)) 247 | content.content_list = Content._get_contents(m_day.group(3)) 248 | else: 249 | content.topic = Topic.Skip 250 | content._complete_inner_subject() 251 | content._normalize_text() 252 | return content 253 | 254 | def equals(self, other: Content) -> bool: 255 | """Show whether or not the given Content is equivalent to this Content. 256 | 257 | Args: 258 | other: The Content to be compared. 259 | 260 | Returns: 261 | True if other is equivalent to this, otherwise false. 262 | """ 263 | return self.text == other.text 264 | 265 | def __eq__(self, __o: object) -> bool: 266 | if not isinstance(__o, Content): 267 | return NotImplemented 268 | return self is __o or self.text == __o.text 269 | 270 | 271 | class Topic(Enum): 272 | """Enumeration type for topic.""" 273 | 274 | DUMMY = "DUMMY" 275 | """Dummy topic.""" 276 | 277 | ESTIMATE = "ESTIMATE" 278 | """Estimation.""" 279 | 280 | COMINGOUT = "COMINGOUT" 281 | """Comingout.""" 282 | 283 | DIVINATION = "DIVINATION" 284 | """Divination.""" 285 | 286 | DIVINED = "DIVINED" 287 | """Report of a divination.""" 288 | 289 | IDENTIFIED = "IDENTIFIED" 290 | """Report of an identification.""" 291 | 292 | GUARD = "GUARD" 293 | """Guard.""" 294 | 295 | GUARDED = "GUARDED" 296 | """Report of a guard.""" 297 | 298 | VOTE = "VOTE" 299 | """Vote.""" 300 | 301 | VOTED = "VOTED" 302 | """Report of a vote.""" 303 | 304 | ATTACK = "ATTACK" 305 | """Attack.""" 306 | 307 | ATTACKED = "ATTACKED" 308 | """Report of an attack.""" 309 | 310 | AGREE = "AGREE" 311 | """Agreement.""" 312 | 313 | DISAGREE = "DISAGREE" 314 | """Disagreement.""" 315 | 316 | Over = "Over" 317 | """There is nothing to talk/whisper.""" 318 | 319 | Skip = "Skip" 320 | """Skip this turn.""" 321 | 322 | OPERATOR = "OPERATOR" 323 | """Operator.""" 324 | 325 | RAW = "RAW" 326 | """Containing raw text.""" 327 | 328 | 329 | class Operator(Enum): 330 | """Enumeration type for operator.""" 331 | 332 | NOP = "NOP" 333 | """No operation.""" 334 | 335 | REQUEST = "REQUEST" 336 | """Request for the action.""" 337 | 338 | INQUIRE = "INQUIRE" 339 | """Inquiry.""" 340 | 341 | BECAUSE = "BECAUSE" 342 | """Reason for the action.""" 343 | 344 | DAY = "DAY" 345 | """DATE.""" 346 | 347 | NOT = "NOT" 348 | """Negation.""" 349 | 350 | AND = "AND" 351 | """Conjunctive clause.""" 352 | 353 | OR = "OR" 354 | """Disjunctive clause.""" 355 | 356 | XOR = "XOR" 357 | """Exclusive disjunctive clause.""" 358 | 359 | 360 | class ContentBuilder: 361 | """A class for the builder classes to build Content of all kinds.""" 362 | 363 | def __init__(self) -> None: 364 | """Initialize a new instance of ContentBuilder.""" 365 | self._subject: Agent = AGENT_UNSPEC 366 | self._target: Agent = AGENT_ANY 367 | self._topic: Topic = Topic.DUMMY 368 | self._role: Role = Role.UNC 369 | self._result: Species = Species.UNC 370 | self._utterance: Utterance = Utterance() 371 | self._operator: Operator = Operator.NOP 372 | self._content_list: list[Content] = [] 373 | self._day: int = -1 374 | self._text: str = "" 375 | 376 | 377 | class AgreeContentBuilder(ContentBuilder): 378 | """Builder class for agreeing to the utterance.""" 379 | 380 | def __init__(self, utterance_type: UtteranceType, day: int, idx: int, *, subject: Agent = AGENT_UNSPEC) -> None: 381 | """Initialize a new instance of AgreeContentBuilder. 382 | 383 | Args: 384 | utterance_type: The type of the utterance. 385 | day: The date of the utterance. 386 | idx: The index number of the utterance. 387 | subject(optional): The agent that agrees. Defaults to AGENT_UNSPEC. 388 | """ 389 | super().__init__() 390 | self._topic = Topic.AGREE 391 | self._subject = subject 392 | if utterance_type is UtteranceType.TALK: 393 | self._utterance = Talk(day, AGENT_NONE, idx, "", 0) 394 | else: 395 | self._utterance = Whisper(day, AGENT_NONE, idx, "", 0) 396 | 397 | 398 | class DisagreeContentBuilder(AgreeContentBuilder): 399 | """Builder class for disagreeing to the utterance.""" 400 | 401 | def __init__(self, utterance_type: UtteranceType, day: int, idx: int, *, subject: Agent = AGENT_UNSPEC) -> None: 402 | """Initialize a new instance of DisagreeContentBuilder. 403 | 404 | Args: 405 | utterance_type: The type of the utterance. 406 | day: The date of the utterance. 407 | idx: The index number of the utterance. 408 | subject(optional): The agent that disagrees. Defaults to AGENT_UNSPEC. 409 | """ 410 | super().__init__(utterance_type, day, idx, subject=subject) 411 | self._topic = Topic.DISAGREE 412 | 413 | 414 | class AttackContentBuilder(ContentBuilder): 415 | """Builder class for expressing the will to attack.""" 416 | 417 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 418 | """Initialize a new instance of AttackContentBuilder. 419 | 420 | Args: 421 | target: The agent the utterer wants to attack. 422 | subject(optional): The agent that expresses the will to attack. Defaults to AGENT_UNSPEC. 423 | """ 424 | super().__init__() 425 | self._topic = Topic.ATTACK 426 | self._subject = subject 427 | self._target = target 428 | 429 | 430 | class AttackedContentBuilder(AttackContentBuilder): 431 | """Builder class for the report of an attack.""" 432 | 433 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 434 | """Initialize a new instance of AttackedContentBuilder. 435 | 436 | Args: 437 | target: The attacked agent. 438 | subject(optional): The agent that reports the attack. Defaults to AGENT_UNSPEC. 439 | """ 440 | super().__init__(target, subject=subject) 441 | self._topic = Topic.ATTACKED 442 | 443 | 444 | class DivinationContentBuilder(AttackContentBuilder): 445 | """Builder class for expressing a divination.""" 446 | 447 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 448 | """Initialize a new instance of DivinationContentBuilder. 449 | 450 | Args: 451 | target: The agent that is an object of the divination. 452 | subject(optional): The agent that does the divination. Defaults to AGENT_UNSPEC. 453 | """ 454 | super().__init__(target, subject=subject) 455 | self._topic = Topic.DIVINATION 456 | 457 | 458 | class DivinedResultContentBuilder(ContentBuilder): 459 | """Builder class for the report of a divination.""" 460 | 461 | def __init__(self, target: Agent, result: Species, *, subject: Agent = AGENT_UNSPEC) -> None: 462 | """Initialize a new instance of DivinedResultContentBuilder. 463 | 464 | Args: 465 | target: The agent that was an object of the divination. 466 | result: The species as the result of the divination. 467 | subject(optional): The agent that did the divination. Defaults to AGENT_UNSPEC. 468 | """ 469 | super().__init__() 470 | self._topic = Topic.DIVINED 471 | self._subject = subject 472 | self._target = target 473 | self._result = result 474 | 475 | 476 | class GuardContentBuilder(AttackContentBuilder): 477 | """Builder class for expressing a guard.""" 478 | 479 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 480 | """Initialize a new instance of GuardContentBuilder. 481 | 482 | Args: 483 | target: The agent to be guarded. 484 | subject(optional): The agent that guards. Defaults to AGENT_UNSPEC. 485 | """ 486 | super().__init__(target, subject=subject) 487 | self._topic = Topic.GUARD 488 | 489 | 490 | class GuardedAgentContentBuilder(AttackContentBuilder): 491 | """Builder class for the report of a guard.""" 492 | 493 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 494 | """Initialize a new instance of GurdedAgentContentBuilder. 495 | 496 | Args: 497 | target: The agent that was guarded. 498 | subject(optional): The agent that guarded. Defaults to AGENT_UNSPEC. 499 | """ 500 | super().__init__(target, subject=subject) 501 | self._topic = Topic.GUARDED 502 | 503 | 504 | class VoteContentBuilder(AttackContentBuilder): 505 | """Builder class for expressing a vote.""" 506 | 507 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 508 | """Initialize a new instance of VoteContentBuilder. 509 | 510 | Args: 511 | target: The agent to be voted on. 512 | subject(optional): The agent that votes. Defaults to AGENT_UNSPEC. 513 | """ 514 | super().__init__(target, subject=subject) 515 | self._topic = Topic.VOTE 516 | 517 | 518 | class VotedContentBuilder(AttackContentBuilder): 519 | """Builder class for the report of a vote.""" 520 | 521 | def __init__(self, target: Agent, *, subject: Agent = AGENT_UNSPEC) -> None: 522 | """Initialize a new instance of VotedContentBuilder. 523 | 524 | Args: 525 | target: The agent that was voted on. 526 | subject(optional): The agent that voted. Defaults to AGENT_UNSPEC. 527 | """ 528 | super().__init__(target, subject=subject) 529 | self._topic = Topic.VOTED 530 | 531 | 532 | class ComingoutContentBuilder(ContentBuilder): 533 | """Builder class for expressing a comingout.""" 534 | 535 | def __init__(self, target: Agent, role: Role, *, subject: Agent = AGENT_UNSPEC) -> None: 536 | """Initialize a new instance of ComingoutContentBuilder. 537 | 538 | Args: 539 | target: The agent that is an object of the comingout. 540 | role: The role that is an object of the comingout. 541 | subject(optional): The agent that does the comingout. Defaults to AGENT_UNSPEC. 542 | """ 543 | super().__init__() 544 | self._topic = Topic.COMINGOUT 545 | self._subject = subject 546 | self._target = target 547 | self._role = role 548 | 549 | 550 | class EstimateContentBuilder(ComingoutContentBuilder): 551 | """Builder class for expressing a estimation.""" 552 | 553 | def __init__(self, target: Agent, role: Role, *, subject: Agent = AGENT_UNSPEC) -> None: 554 | """Initialize a new instance of EstimateContentBuilder. 555 | 556 | Args: 557 | target: The agent that is an object of the estimation. 558 | role: The estimated role of the agent. 559 | subject(optional): The agent that estimates. Defaults to AGENT_UNSPEC. 560 | """ 561 | super().__init__(target, role, subject=subject) 562 | self._topic = Topic.ESTIMATE 563 | 564 | 565 | class IdentContentBuilder(DivinedResultContentBuilder): 566 | """Builder class for the report of an identification.""" 567 | 568 | def __init__(self, target: Agent, result: Species, *, subject: Agent = AGENT_UNSPEC) -> None: 569 | """Initialize a new instance of IdentContentBuilder. 570 | 571 | Args: 572 | target: The agent that was an object of the identification. 573 | result: The species of the agent revealed as a result of the identification. 574 | subject(optional): The agent that did the identification. Defaults to AGENT_UNSPEC. 575 | """ 576 | super().__init__(target, result, subject=subject) 577 | self._topic = Topic.IDENTIFIED 578 | 579 | 580 | class RequestContentBuilder(ContentBuilder): 581 | """Builder class for expressing a request.""" 582 | 583 | def __init__(self, target: Agent, action: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 584 | """Initialize a new instance of RequestContentBuilder. 585 | 586 | Args: 587 | target: The agent that is an object of the request. 588 | action: The requested action. 589 | subject(optional): The agent that requests. Defaults to AGENT_UNSPEC. 590 | """ 591 | super().__init__() 592 | self._topic = Topic.OPERATOR 593 | self._operator = Operator.REQUEST 594 | self._subject = subject 595 | self._target = target 596 | self._content_list.append(action.clone()) 597 | 598 | 599 | class InquiryContentBuilder(RequestContentBuilder): 600 | """Builder class for expressing an inquiry.""" 601 | 602 | def __init__(self, target: Agent, action: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 603 | """Initialize a new instance of InquiryContentBuilder. 604 | 605 | Args: 606 | target: The agent that reveives the inquiry. 607 | action: The matter inquired. 608 | subject(optional): The agent that makes the inquiry. Defaults to AGENT_UNSPEC. 609 | """ 610 | super().__init__(target, action, subject=subject) 611 | self._operator = Operator.INQUIRE 612 | 613 | 614 | class BecauseContentBuilder(ContentBuilder): 615 | """Builder class for expressing a reason.""" 616 | 617 | def __init__(self, reason: Content, action: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 618 | """Initialize a new instance of BecauseContentBuilder. 619 | 620 | Args: 621 | reason: The reason for the action. 622 | action: The action based on the reason. 623 | subject(optional): The agent that expresses the action and its reason. Defaults to AGENT_UNSPEC. 624 | """ 625 | super().__init__() 626 | self._topic = Topic.OPERATOR 627 | self._operator = Operator.BECAUSE 628 | self._subject = subject 629 | self._content_list.append(reason.clone()) 630 | self._content_list.append(action.clone()) 631 | 632 | 633 | class AndContentBuilder(ContentBuilder): 634 | """Builder class for expressing a conjunctive clause.""" 635 | 636 | def __init__(self, contents: list[Content], *, subject: Agent = AGENT_UNSPEC) -> None: 637 | """Initialize a new instance of AndContentBuilder. 638 | 639 | Args: 640 | contents: The series of the conjuncts. 641 | subject(optional): The agent that expresses the conjunctie clause. Defaults to AGENT_UNSPEC. 642 | 643 | Raises: 644 | ValueError: In case of empty conjuncts, ValueError is raised. 645 | """ 646 | super().__init__() 647 | self._topic = Topic.OPERATOR 648 | self._operator = Operator.AND 649 | self._subject = subject 650 | if not contents: 651 | raise ValueError("Invalid argument: contents is empty") 652 | self._content_list.extend([c.clone() for c in contents]) 653 | 654 | 655 | class OrContentBuilder(AndContentBuilder): 656 | """Builder class for expressing a disjunctive clause.""" 657 | 658 | def __init__(self, contents: list[Content], *, subject: Agent = AGENT_UNSPEC) -> None: 659 | """Initialize a new instance of OrContentBuilder. 660 | 661 | Args: 662 | contents: The series of the disjuncts. 663 | subject(optional): The agent that expresses the disjunctive clause. Defaults to AGENT_UNSPEC. 664 | """ 665 | super().__init__(contents, subject=subject) 666 | self._operator = Operator.OR 667 | 668 | 669 | class XorContentBuilder(BecauseContentBuilder): 670 | """Builder class for expressing a exclusive disjunctive clause.""" 671 | 672 | def __init__(self, disjunct1: Content, disjunct2: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 673 | """Initialize a new instance of XorContentBuilder. 674 | 675 | Args: 676 | disjunct1: The first disjunct. 677 | disjunct2: The second disjunct. 678 | subject(optional): The agent that expresses the exclusive disjunctive clause. Defaults to AGENT_UNSPEC. 679 | """ 680 | super().__init__(disjunct1, disjunct2, subject=subject) 681 | self._operator = Operator.XOR 682 | 683 | 684 | class NotContentBuilder(ContentBuilder): 685 | """Builder class for expressing a negation.""" 686 | 687 | def __init__(self, content: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 688 | """Initialize a new instance of NotContentBuilder. 689 | 690 | Args: 691 | content: The content to be negated. 692 | subject(optional): The agent that expresses the negation. Defaults to AGENT_UNSPEC. 693 | """ 694 | super().__init__() 695 | self._topic = Topic.OPERATOR 696 | self._operator = Operator.NOT 697 | self._subject = subject 698 | self._content_list.append(content.clone()) 699 | 700 | 701 | class DayContentBuilder(ContentBuilder): 702 | """Builder class for adding a date to the Content.""" 703 | 704 | def __init__(self, day: int, content: Content, *, subject: Agent = AGENT_UNSPEC) -> None: 705 | """Initialize a new instance of DayContentBuilder. 706 | 707 | Args: 708 | day: The date of the Content. 709 | content: The content to which the date is added. 710 | subject(optional): The agent that adds the date to the Content. Defaults to AGENT_UNSPEC. 711 | """ 712 | super().__init__() 713 | self._topic = Topic.OPERATOR 714 | self._operator = Operator.DAY 715 | self._subject = subject 716 | self._day = day 717 | self._content_list.append(content.clone()) 718 | 719 | 720 | class SkipContentBuilder(ContentBuilder): 721 | """Builder class for the skip of this turn's utterance.""" 722 | 723 | def __init__(self) -> None: 724 | """Initialize a new instance of SkipContentBuilder.""" 725 | super().__init__() 726 | self._topic = Topic.Skip 727 | 728 | 729 | class OverContentBuilder(ContentBuilder): 730 | """Builder class for expressing that there is nothing to utter.""" 731 | 732 | def __init__(self) -> None: 733 | """Initialize a new instance of OverContentBuilder.""" 734 | super().__init__() 735 | self._topic = Topic.Over 736 | 737 | 738 | class RawContentBuilder(ContentBuilder): 739 | """Builder class for expressing an raw text.""" 740 | 741 | def __init__(self, text: str) -> None: 742 | """Initialize a new instance of RawContentBuilder.""" 743 | super().__init__() 744 | self._topic = Topic.RAW 745 | self._text = text 746 | 747 | 748 | class EmptyContentBuilder(ContentBuilder): 749 | """Builder class for expressing an empty Content.""" 750 | 751 | def __init__(self) -> None: 752 | """Initialize a new instance of EmptyContentBuilder.""" 753 | super().__init__() 754 | self._topic = Topic.DUMMY 755 | -------------------------------------------------------------------------------- /aiwolf/gameinfo.py: -------------------------------------------------------------------------------- 1 | # 2 | # gameinfo.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """gameinfo module.""" 18 | from typing import Optional, TypedDict 19 | 20 | from aiwolf.agent import Agent, Role, Status 21 | from aiwolf.judge import Judge, _Judge 22 | from aiwolf.utterance import Talk, Whisper, _Utterance 23 | from aiwolf.vote import Vote, _Vote 24 | 25 | 26 | class _GameInfo(TypedDict): 27 | agent: int 28 | attackVoteList: list[_Vote] 29 | attackedAgent: int 30 | cursedFox: int 31 | day: int 32 | divineResult: _Judge 33 | executedAgent: int 34 | existingRoleList: list[str] 35 | guardedAgent: int 36 | lastDeadAgentList: list[int] 37 | latestAttackVoteList: list[_Vote] 38 | latestExecutedAgent: int 39 | latestVoteList: list[_Vote] 40 | mediumResult: _Judge 41 | remainTalkMap: dict[str, int] 42 | remainWhisperMap: dict[str, int] 43 | roleMap: dict[str, str] 44 | statusMap: dict[str, str] 45 | talkList: list[_Utterance] 46 | voteList: list[_Vote] 47 | whisperList: list[_Utterance] 48 | 49 | 50 | class GameInfo: 51 | """Class for game information.""" 52 | 53 | def __init__(self, game_info: _GameInfo) -> None: 54 | """Initializes a new instance of GameInfo. 55 | 56 | Args: 57 | game_info: The _GameInfo used for initialization. 58 | """ 59 | self.me: Agent = Agent(game_info["agent"]) 60 | """The agent who recieves this GameInfo.""" 61 | 62 | self.attack_vote_list: list[Vote] = [Vote.compile(v) for v in game_info["attackVoteList"]] 63 | """The list of votes for attack.""" 64 | 65 | self.attacked_agent: Optional[Agent] = GameInfo._get_agent(game_info["attackedAgent"]) 66 | """The agent decided to be attacked as a result of werewolves' vote.""" 67 | 68 | self.cursed_fox: Optional[Agent] = GameInfo._get_agent(game_info["cursedFox"]) 69 | """The fox killed by curse.""" 70 | 71 | self.day: int = game_info["day"] 72 | """Current day.""" 73 | 74 | self.divine_result: Optional[Judge] = Judge.compile(game_info["divineResult"]) if game_info["divineResult"] is not None else None 75 | """The result of the dvination.""" 76 | 77 | self.executed_agent: Optional[Agent] = GameInfo._get_agent(game_info["executedAgent"]) 78 | """The agent executed last night.""" 79 | 80 | self.existing_role_list: list[Role] = [Role[r] for r in game_info["existingRoleList"]] 81 | """The list of existing roles in this game.""" 82 | 83 | self.guarded_agent: Optional[Agent] = GameInfo._get_agent(game_info["guardedAgent"]) 84 | """The agent guarded last night.""" 85 | 86 | self.last_dead_agent_list: list[Agent] = [Agent(a) for a in game_info["lastDeadAgentList"]] 87 | """The list of agents who died last night.""" 88 | 89 | self.latest_attack_vote_list: list[Vote] = [Vote.compile(v) for v in game_info["latestAttackVoteList"]] 90 | """The latest list of votes for attack.""" 91 | 92 | self.latest_executed_agent: Optional[Agent] = GameInfo._get_agent(game_info["latestExecutedAgent"]) 93 | """The latest executed agent.""" 94 | 95 | self.latest_vote_list: list[Vote] = [Vote.compile(v) for v in game_info["latestVoteList"]] 96 | """The latest list of votes for execution.""" 97 | 98 | self.medium_result: Optional[Judge] = Judge.compile(game_info["mediumResult"]) if game_info["mediumResult"] is not None else None 99 | """The result of the inquest.""" 100 | 101 | self.remain_talk_map: dict[Agent, int] = {Agent(int(k)): v for k, v in game_info["remainTalkMap"].items()} 102 | """The number of opportunities to talk remaining.""" 103 | 104 | self.remain_whisper_map: dict[Agent, int] = {Agent(int(k)): v for k, v in game_info["remainWhisperMap"].items()} 105 | """The number of opportunities to whisper remaining.""" 106 | 107 | self.role_map: dict[Agent, Role] = {Agent(int(k)): Role[v] for k, v in game_info["roleMap"].items()} 108 | """The known roles of agents.""" 109 | 110 | self.status_map: dict[Agent, Status] = {Agent(int(k)): Status[v] for k, v in game_info["statusMap"].items()} 111 | """The statuses of all agents.""" 112 | 113 | self.talk_list: list[Talk] = [Talk.compile(u) for u in game_info["talkList"]] 114 | """The list of today's talks.""" 115 | 116 | self.vote_list: list[Vote] = [Vote.compile(v) for v in game_info["voteList"]] 117 | """The list of votes for execution.""" 118 | 119 | self.whisper_list: list[Whisper] = [Whisper.compile(u) for u in game_info["whisperList"]] 120 | """The list of today's whispers.""" 121 | 122 | @staticmethod 123 | def _get_agent(idx: int) -> Optional[Agent]: 124 | return None if idx < 0 else Agent(idx) 125 | 126 | @property 127 | def agent_list(self) -> list[Agent]: 128 | """The list of existing agents.""" 129 | return list(self.status_map.keys()) 130 | 131 | @property 132 | def alive_agent_list(self) -> list[Agent]: 133 | """The list of alive agents.""" 134 | return [i[0] for i in self.status_map.items() if i[1] == Status.ALIVE] 135 | 136 | @property 137 | def my_role(self) -> Role: 138 | """The role of the player who receives this GameInfo.""" 139 | return self.role_map[self.me] 140 | -------------------------------------------------------------------------------- /aiwolf/gamesetting.py: -------------------------------------------------------------------------------- 1 | # 2 | # gamesetting.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """gamesetting module.""" 18 | from typing import TypedDict 19 | 20 | from aiwolf.agent import Role 21 | 22 | 23 | class _GameSetting(TypedDict): 24 | enableNoAttack: bool 25 | enableNoExecution: bool 26 | enableRoleRequest: bool 27 | maxAttackRevote: int 28 | maxRevote: int 29 | maxSkip: int 30 | maxTalk: int 31 | maxTalkTurn: int 32 | maxWhisper: int 33 | maxWhisperTurn: int 34 | playerNum: int 35 | randomSeed: int 36 | roleNumMap: dict[str, int] 37 | talkOnFirstDay: bool 38 | timeLimit: int 39 | validateUtterance: bool 40 | votableInFirstDay: bool 41 | voteVisible: bool 42 | whisperBeforeRevote: bool 43 | 44 | 45 | class GameSetting: 46 | """Class for game settings.""" 47 | 48 | def __init__(self, game_setting: _GameSetting) -> None: 49 | """Initializes a new instance of GameSetting. 50 | 51 | Args: 52 | game_setting: The _GameSetting used for initialization. 53 | """ 54 | self.enable_no_attack: bool = game_setting["enableNoAttack"] 55 | """Whether or not the game permit to attack no one.""" 56 | 57 | self.enable_no_execution: bool = game_setting["enableNoExecution"] 58 | """Whether or not executing nobody is allowed.""" 59 | 60 | self.enable_role_request: bool = game_setting["enableRoleRequest"] 61 | """Whether or not role request is allowed.""" 62 | 63 | self.max_attack_revote: int = game_setting["maxAttackRevote"] 64 | """The maximum number of revotes for attack.""" 65 | 66 | self.max_revote: int = game_setting["maxRevote"] 67 | """The maximum number of revotes.""" 68 | 69 | self.max_skip: int = game_setting["maxSkip"] 70 | """The maximum permissible length of the succession of SKIPs.""" 71 | 72 | self.max_talk: int = game_setting["maxTalk"] 73 | """The maximum number of talks.""" 74 | 75 | self.max_talk_turn: int = game_setting["maxTalkTurn"] 76 | """The maximum number of turns of talk.""" 77 | 78 | self.max_whisper: int = game_setting["maxWhisper"] 79 | """The maximum number of whispers a day.""" 80 | 81 | self.max_whisper_turn: int = game_setting["maxWhisperTurn"] 82 | """The maximum number of turns of whisper.""" 83 | 84 | self.player_num: int = game_setting["playerNum"] 85 | """The number of players.""" 86 | 87 | self.random_seed: int = game_setting["randomSeed"] 88 | """The random seed.""" 89 | 90 | self.role_num_map: dict[Role, int] = {Role[k]: v for k, v in game_setting["roleNumMap"].items()} 91 | """The number of each role.""" 92 | 93 | self.talk_on_first_day: bool = game_setting["talkOnFirstDay"] 94 | """Whether or not there are talks on the first day.""" 95 | 96 | self.time_limit: int = game_setting["timeLimit"] 97 | """The upper limit for the response time to the request.""" 98 | 99 | self.validate_utterance: bool = game_setting["validateUtterance"] 100 | """Whether or not the uttered text is validated.""" 101 | 102 | self.votable_on_first_day: bool = game_setting["votableInFirstDay"] 103 | """Whether or not there is vote on the first day.""" 104 | 105 | self.vote_visible: bool = game_setting["voteVisible"] 106 | """Whether or not agent can see who vote to who.""" 107 | 108 | self.whisper_before_revote: bool = game_setting["whisperBeforeRevote"] 109 | """Whether or not werewolf can whisper before the revote for attack.""" 110 | -------------------------------------------------------------------------------- /aiwolf/judge.py: -------------------------------------------------------------------------------- 1 | # 2 | # judge.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """judge module.""" 18 | from __future__ import annotations 19 | 20 | from typing import TypedDict 21 | 22 | from aiwolf.agent import Agent, Species 23 | from aiwolf.constant import AGENT_NONE 24 | 25 | 26 | class _Judge(TypedDict): 27 | agent: int 28 | day: int 29 | target: int 30 | result: str 31 | 32 | 33 | class Judge: 34 | """The judgement whether the player is a human or a werewolf.""" 35 | 36 | def __init__(self, agent: Agent = AGENT_NONE, day: int = -1, target: Agent = AGENT_NONE, result: Species = Species.UNC) -> None: 37 | """Initialize a new instance of Judge. 38 | 39 | Args: 40 | agent(optional): The agent that judged. Defaults to C.AGENT_NONE. 41 | day(optional): The date of the judgement. Defaults to -1. 42 | target(optional): The judged agent. Defaults to C.AGENT_NONE. 43 | result(optional): The result of the judgement. Defaults to Species.UNC. 44 | """ 45 | self.agent: Agent = agent 46 | """The agent that judged.""" 47 | 48 | self.day: int = day 49 | """The date of the judgement.""" 50 | 51 | self.target: Agent = target 52 | """The judged agent.""" 53 | 54 | self.result: Species = result 55 | """The result of the judgement.""" 56 | 57 | @staticmethod 58 | def compile(judge: _Judge) -> Judge: 59 | """Convert a _Judge into the corresponding Judge. 60 | 61 | Args: 62 | judge: The _Judge to be converted. 63 | 64 | Returns: 65 | The Judge converted from the given _Judge. 66 | """ 67 | j: Judge = Judge() 68 | j.agent = Agent(judge['agent']) 69 | j.day = judge['day'] 70 | j.target = Agent(judge['target']) 71 | j.result = Species[judge['result']] 72 | return j 73 | 74 | def __eq__(self, __o: object) -> bool: 75 | if not isinstance(__o, Judge): 76 | return NotImplemented 77 | return self is __o or (type(self) == type(__o) and self.agent == __o.agent and self.day == __o.day 78 | and self.target == __o.target and self.result == __o.result) 79 | -------------------------------------------------------------------------------- /aiwolf/player.py: -------------------------------------------------------------------------------- 1 | # 2 | # player.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """player module.""" 18 | from abc import ABC, abstractmethod 19 | 20 | from aiwolf.agent import Agent 21 | from aiwolf.content import Content 22 | from aiwolf.gameinfo import GameInfo 23 | from aiwolf.gamesetting import GameSetting 24 | 25 | 26 | class AbstractPlayer(ABC): 27 | """Abstract class that defines the functions every player agents must have.""" 28 | 29 | @abstractmethod 30 | def attack(self) -> Agent: 31 | """Return the agent this werewolf wants to attack. 32 | 33 | The agent that does not exist means not wanting to attack any other. 34 | 35 | Returns: 36 | The agent this werewolf wants to attack. 37 | """ 38 | pass 39 | 40 | @abstractmethod 41 | def day_start(self) -> None: 42 | """Called when the day starts.""" 43 | pass 44 | 45 | @abstractmethod 46 | def divine(self) -> Agent: 47 | """Return the agent this seer wants to divine. 48 | 49 | The agent that does not exist means no divination. 50 | 51 | Returns: 52 | The agent this seer wants to divine. 53 | """ 54 | pass 55 | 56 | @abstractmethod 57 | def finish(self) -> None: 58 | """Called when the game finishes.""" 59 | pass 60 | 61 | def get_name(self) -> str: 62 | """Return this player's name. 63 | 64 | Returns: 65 | This player's name. 66 | """ 67 | return type(self).__name__ 68 | 69 | @abstractmethod 70 | def guard(self) -> Agent: 71 | """Return the agent this bodyguard wants to guard. 72 | 73 | The agent that does not exist means no guard. 74 | 75 | Returns: 76 | The agent this bodyguard wants to guard. 77 | """ 78 | pass 79 | 80 | @abstractmethod 81 | def initialize(self, game_info: GameInfo, game_setting: GameSetting) -> None: 82 | """Called when the game starts.""" 83 | pass 84 | 85 | @abstractmethod 86 | def talk(self) -> Content: 87 | """Return this player's talk. 88 | 89 | Returns: 90 | This player's talk. 91 | """ 92 | pass 93 | 94 | @abstractmethod 95 | def update(self, game_info: GameInfo) -> None: 96 | """Called when the game information is updated.""" 97 | pass 98 | 99 | @abstractmethod 100 | def vote(self) -> Agent: 101 | """Return the agent this player wants to exclude from this game. 102 | 103 | Returning the agent that does not exist results in ramdom vote. 104 | 105 | Returns: 106 | The agent this player wants to exclude from this game. 107 | """ 108 | pass 109 | 110 | @abstractmethod 111 | def whisper(self) -> Content: 112 | """Return this player's whisper. 113 | 114 | Returns: 115 | This player's whisper. 116 | """ 117 | pass 118 | -------------------------------------------------------------------------------- /aiwolf/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIWolfSharp/aiwolf-python/1a04b91caff3403296fdbb126a6e15de2307fc02/aiwolf/py.typed -------------------------------------------------------------------------------- /aiwolf/utterance.py: -------------------------------------------------------------------------------- 1 | # 2 | # utterance.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """utterance module.""" 18 | from __future__ import annotations 19 | 20 | from enum import Enum 21 | from typing import Final, TypedDict 22 | 23 | from aiwolf.agent import Agent 24 | from aiwolf.constant import AGENT_NONE 25 | 26 | 27 | class UtteranceType(Enum): 28 | """Enumeration type for the kind of utterance.""" 29 | 30 | TALK = "TALK" 31 | """Talk.""" 32 | 33 | WHISPER = "WHISPER" 34 | """Whisper.""" 35 | 36 | 37 | class _Utterance(TypedDict): 38 | day: int 39 | agent: int 40 | idx: int 41 | text: str 42 | turn: int 43 | 44 | 45 | class Utterance: 46 | """Class for utterance.""" 47 | 48 | OVER: Final[str] = "Over" 49 | """The string that nothing to say.""" 50 | 51 | SKIP: Final[str] = "Skip" 52 | """The string that means skip this turn.""" 53 | 54 | def __init__(self, day: int = -1, agent: Agent = AGENT_NONE, idx: int = -1, text: str = "", turn: int = -1) -> None: 55 | """Initialize a new instance of Utterance. 56 | 57 | Args: 58 | day(opional): The date of the utterance. Defaults to -1. 59 | agent(optional): The agent that utters. Defaults to C.AGENT_NONE. 60 | idx(optional): The index number of the utterance. Defaults to -1. 61 | text(optional): The uttered text. Defaults to "". 62 | turn(optional): The turn of the utterance. Defaults to -1. 63 | """ 64 | self.day: int = day 65 | """The date of this utterance.""" 66 | 67 | self.agent: Agent = agent 68 | """The agent who uttered.""" 69 | 70 | self.idx: int = idx 71 | """The index number of this utterance.""" 72 | 73 | self.text: str = text 74 | """The contents of this utterance.""" 75 | 76 | self.turn: int = turn 77 | """The turn of this utterance.""" 78 | 79 | def __eq__(self, __o: object) -> bool: 80 | if not isinstance(__o, Utterance): 81 | return NotImplemented 82 | return self is __o or (type(self) == type(__o) and self.day == __o.day and self.agent == __o.agent 83 | and self.idx == __o.idx and self.text == __o.text and self.turn == __o.turn) 84 | 85 | 86 | class Talk(Utterance): 87 | """Talk class.""" 88 | 89 | def __init__(self, day: int = -1, agent: Agent = AGENT_NONE, idx: int = -1, text: str = "", turn: int = -1) -> None: 90 | """Initialize a new instance of Talk. 91 | 92 | Args: 93 | day(opional): The date of the utterance. Defaults to -1. 94 | agent(optional): The agent that utters. Defaults to C.AGENT_NONE. 95 | idx(optional): The index number of the utterance. Defaults to -1. 96 | text(optional): The uttered text. Defaults to "". 97 | turn(optional): The turn of the utterance. Defaults to -1. 98 | """ 99 | super().__init__(day, agent, idx, text, turn) 100 | 101 | @staticmethod 102 | def compile(utterance: _Utterance) -> Talk: 103 | """Convert a _Utterance into the corresponding Talk. 104 | 105 | Args: 106 | utterance: The _Utterance to be converted. 107 | 108 | Returns: 109 | The Talk converted from the given _Utterance. 110 | """ 111 | t = Talk() 112 | t.day = utterance["day"] 113 | t.agent = Agent(utterance["agent"]) 114 | t.idx = utterance["idx"] 115 | t.text = utterance["text"] 116 | t.turn = utterance["turn"] 117 | return t 118 | 119 | 120 | class Whisper(Utterance): 121 | """Whisper class.""" 122 | 123 | def __init__(self, day: int = -1, agent: Agent = AGENT_NONE, idx: int = -1, text: str = "", turn: int = -1) -> None: 124 | """Initialize a new instance of Whisper. 125 | 126 | Args: 127 | day(opional): The date of the utterance. Defaults to -1. 128 | agent(optional): The agent that utters. Defaults to C.AGENT_NONE. 129 | idx(optional): The index number of the utterance. Defaults to -1. 130 | text(optional): The uttered text. Defaults to "". 131 | turn(optional): The turn of the utterance. Defaults to -1. 132 | """ 133 | super().__init__(day, agent, idx, text, turn) 134 | 135 | @staticmethod 136 | def compile(utterance: _Utterance) -> Whisper: 137 | """Convert a _Utterance into the corresponding Whisper. 138 | 139 | Args: 140 | utterance: The _Utterance to be converted. 141 | 142 | Returns: 143 | The Whisper converted from the given _Utterance. 144 | """ 145 | w = Whisper() 146 | w.day = utterance["day"] 147 | w.agent = Agent(utterance["agent"]) 148 | w.idx = utterance["idx"] 149 | w.text = utterance["text"] 150 | w.turn = utterance["turn"] 151 | return w 152 | -------------------------------------------------------------------------------- /aiwolf/vote.py: -------------------------------------------------------------------------------- 1 | # 2 | # vote.py 3 | # 4 | # Copyright 2022 OTSUKI Takashi 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """vote module.""" 18 | from __future__ import annotations 19 | 20 | from typing import TypedDict 21 | 22 | from aiwolf.agent import Agent 23 | from aiwolf.constant import AGENT_NONE 24 | 25 | 26 | class _Vote(TypedDict): 27 | agent: int 28 | day: int 29 | target: int 30 | 31 | 32 | class Vote: 33 | """Information of the vote for execution/attack.""" 34 | 35 | def __init__(self, agent: Agent = AGENT_NONE, day: int = -1, target: Agent = AGENT_NONE) -> None: 36 | """Initialize a new instance of Vote. 37 | 38 | Args: 39 | agent(optional): The agent that votes. Defaults to C.AGENT_NONE. 40 | day(optional): The date of the vote. Defaults to -1. 41 | target(optional): The agent to be voted on. Defaults to C.AGENT_NONE. 42 | """ 43 | self.agent: Agent = agent 44 | """The agent that votes.""" 45 | 46 | self.day: int = day 47 | """The date of the vote.""" 48 | 49 | self.target: Agent = target 50 | """The agent to be voted on.""" 51 | 52 | @staticmethod 53 | def compile(vote: _Vote) -> Vote: 54 | """Convert a _Vote into the corresponding Vote. 55 | 56 | Args: 57 | vote: The _Vote to be converted. 58 | 59 | Returns: 60 | The Vote converted from the given _Vote. 61 | """ 62 | v = Vote() 63 | v.agent = Agent(vote["agent"]) 64 | v.day = vote["day"] 65 | v.target = Agent(vote["target"]) 66 | return v 67 | 68 | def __eq__(self, __o: object) -> bool: 69 | if not isinstance(__o, Vote): 70 | return NotImplemented 71 | return self is __o or (type(self) == type(__o) and self.agent == __o.agent 72 | and self.day == __o.day and self.target == __o.target) 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="aiwolf", 5 | version="1.3.0", 6 | author="OTSUKI Takashi", 7 | author_email="aiwolfsharp@outlook.com", 8 | url="https://github.com/AIWolfSharp/aiwolf-python", 9 | description="The package for creating AIWolf agent in Python", 10 | # long_description="This is a Python port of AIWolfLib, a .NET package.", 11 | long_description_content_type="text/markdown", 12 | license="Apache Software License", 13 | packages=setuptools.find_packages(), 14 | package_data={ 15 | "aiwolf": ["py.typed"], 16 | }, 17 | classifiers=[ # see https://pypi.org/pypi?:action=list_classifiers 18 | # "Development Status :: 3 - Alpha", 19 | # "Development Status :: 4 - Beta", 20 | "Development Status :: 5 - Production/Stable", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python :: 3.8" 24 | ], 25 | ) 26 | --------------------------------------------------------------------------------