diff --git a/.gitignore b/.gitignore index fa94661..869850f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ credentials.json +**/__pycache__/ + # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ccbf2d7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anthony Costarelli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5bf2b65..1243ba4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,42 @@ -# Setup -In the repo root: +# [GameBench: Evaluating Strategic Reasoning Abilities of LLM Agents](https://gamebench-website.vercel.app/) + +This repository contains both the code for the benchmark and the data we collected so far. + +The code is available under the MIT license, and the data are available under the CC-BY license. + +The match data is located in [`matches.json`](https://github.com/Joshuaclymer/GameBench/tree/main/matches.json). + +### Setup +In the repository root: ``` conda create -n gameenv python=3.10 conda activate gameenv pip install -e . ``` -Ask Josh for the credentials file. +You must provide your own OpenAI API key in a file `credentials.json` at the top-level directory. It should have the format: +```json +{ + "openai_api_key": "your_openai_api_key_here" +} +``` + +### Replicating figures + +The Python script [`generate_all_results.py`](https://github.com/Joshuaclymer/GameBench/tree/main/generate_all_results.py) generates all the figures from the paper into [`figures/`](https://github.com/Joshuaclymer/GameBench/tree/main/figures/). Use the command: + +```py +python3 generate_all_results.py +``` + +### Collecting data + +The scripts provided in [`scripts/`](https://github.com/Joshuaclymer/GameBench/tree/main/scripts/) run some individual games with preconfigured settings. You can run/modify these scripts or create another. To run a script, execute: +```sh +sh ./scripts/.sh +``` + +Alternatively, you can run `api.play_game.play_game` directly from a Python script created in the top-level directory. ### `llm-reasoners` dependency @@ -19,4 +49,4 @@ Ask Josh for the credentials file. journal={arXiv preprint arXiv:2305.14992}, year={2023} } -``` \ No newline at end of file +``` diff --git a/agents/gpt.py b/agents/gpt.py index 5fc1e2f..15c005f 100644 --- a/agents/gpt.py +++ b/agents/gpt.py @@ -1,3 +1,4 @@ +from collections import defaultdict from dataclasses import dataclass, field from api.classes import Agent, AvailableActions, Action, Observation, Rules import random @@ -5,6 +6,10 @@ import api.util as util import ast import json +from PIL import Image +import base64 +from io import BytesIO +import re action_format_instructions_no_openended = """\ @@ -27,6 +32,15 @@ api_key=util.load_json("credentials.json")["openai_api_key"] ) +tokens = defaultdict(int) +def completions(*args, **kwargs): + ret = openai_client.chat.completions.create(*args, **kwargs) + + model = kwargs["model"] + tokens[f"{model}_input"] += ret.usage.prompt_tokens + tokens[f"{model}_output"] += ret.usage.completion_tokens + print("*******************", tokens) + return ret @dataclass class OpenAITextAgent(Agent): @@ -48,15 +62,66 @@ def take_action( available_actions: AvailableActions, show_state: bool, ): + messages = [{"role": "system", "content": self.system_message}] valid_actions = [] prompt = f"You are playing a game called {rules.title}. The rules are as follows:\n{rules.summary}\n" if rules.additional_details != None: prompt += "The following are headings with additional information about the rules that you can expand by taking the action Explain().\n" - details_dict = {f"H{i+1}": topic for i, topic in enumerate(rules.additional_details)} + details_dict = { + f"H{i+1}": topic for i, topic in enumerate(rules.additional_details) + } prompt += json.dumps(details_dict, indent=4) - valid_actions.extend(f"Explain({h})" for h in list(details_dict.keys())) + #valid_actions.extend(f"Explain({h})" for h in list(details_dict.keys())) prompt += f"\n# Observation\nThe following describes the current state of the game:\n{observation.text}\n" + if observation.image is not None: + if self.openai_model == "gpt-4-1106-preview": + self.print("Image observation recieved.") + buffered = BytesIO() + observation.image.save(buffered, format="JPEG") + base64_image = base64.b64encode(buffered.getvalue()) + messages.append( + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + }, + }, + ], + } + ) + prompt = "" + else: + self.print("Image observation recieved. Using GPT4 to generate text description.") + buffered = BytesIO() + image.save(buffered, format="JPEG") + base64_image = base64.b64encode(buffered.getvalue()) + + imagedesc = completions( + model="gpt-4-vision-preview", + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt + }, + { + "type": "image", + "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}, + }, + ], + } + ], + ).choices[0].message.content + prompt += imagedesc + observation.image = None + assert available_actions.predefined != {} or available_actions.openended != {} prompt += f"\n# Actions\n" prompt += f"{available_actions.instructions}\n" @@ -83,16 +148,17 @@ def take_action( ): prompt += "Return the action Explain() to receive additional info about what any of the above actions do.\n" - messages = [{"role": "system", "content": self.system_message}] - # Chain of Thought if self.mode == 1: prompt += "First, let's reason out loud about which action you should take to maximize your probability of winning." messages.append({"role": "user", "content": prompt}) response = ( - openai_client.chat.completions.create( - model=self.openai_model, messages=messages + completions( + model=self.openai_model + if observation.image is None + else "gpt-4-vision-preview", + messages=messages, ) .choices[0] .message.content @@ -109,8 +175,11 @@ def take_action( messages.append({"role": "user", "content": prompt}) response = ( - openai_client.chat.completions.create( - model=self.openai_model, messages=messages + completions( + model=self.openai_model + if observation.image is None + else "gpt-4-vision-preview", + messages=messages, ) .choices[0] .message.content @@ -118,9 +187,7 @@ def take_action( messages.append({"role": "assistant", "content": response}) prompt = "" - self.print( - f"GPT listed the following actions as possibilities: {response}" - ) + self.print(f"GPT listed the following actions as possibilities: {response}") prompt += "\nTo summarize, if you choose a predefined action, you must return json with an 'action' key which contains one of the following valid actions:\n" prompt += str(list(available_actions.predefined)) @@ -131,8 +198,10 @@ def take_action( result = None for _ in range(self.max_retries): response = ( - openai_client.chat.completions.create( - model=self.openai_model, + completions( + model=self.openai_model + if observation.image is None + else "gpt-4-vision-preview", response_format={"type": "json_object"}, messages=messages, ) @@ -143,17 +212,44 @@ def take_action( self.print("GPT responded with", response) try: - action = ast.literal_eval(response) + action = ast.literal_eval(response.strip()) + action["action"] except: self.print("GPT returned invalid JSON") continue - if action["action"] in available_actions.openended and "openended_response" not in action: - self.print("GPT chose openended action but didn't include response", action) + if ( + action["action"] in available_actions.openended + and "openended_response" not in action + ): + self.print( + "GPT chose openended action but didn't include response", action + ) error_message = "You chose an openended action, and so your json must have an 'openended_response' key." messages.append({"role": "user", "content": error_message}) continue + try: + explain = re.findall(r"Explain\((H\d+)\)", action["action"]) + if len(explain): + self.print("GPT is asking for rules explanation.") + rule = details_dict[explain[0]] + desc = rules.additional_details[rule] + messages.append({"role": "user", "content": desc}) + continue + + explain = re.findall(r"Explain\((.+)\)", action["action"]) + if len(explain): + self.print("GPT is asking for action explanation.") + desc = available_actions.predefined.get(explain[0], "") + available_actions.openended.get(explain[0], "") + messages.append({"role": "user", "content": desc}) + continue + except: + self.print("GPT tried asking for an expalanation but failed.") + error_message = "This is an invalid Explain action." + messages.append({"role": "user", "content": error_message}) + continue + if action["action"] in valid_actions: self.print("GPT chose valid action", action) result = action @@ -167,9 +263,10 @@ def take_action( messages.append({"role": "user", "content": error_message}) if result == None: self.print( - f"WARNING: GPT returned an a random action after {self.max_retries} tries" + f"WARNING: GPT returned too many invalid actions after {self.max_retries} tries" ) return Action(action_id=None) + return Action( action_id=result["action"], openended_response=result.get("openended_response"), @@ -177,25 +274,37 @@ def take_action( @dataclass -class ChatGPTText(OpenAITextAgent): +class GPT3(OpenAITextAgent): + openai_model: str = "gpt-3.5-turbo-1106" + agent_type_id: str = "gpt-3" + mode: int = 0 + +@dataclass +class GPT3CoT(OpenAITextAgent): openai_model: str = "gpt-3.5-turbo-1106" - agent_type_id: str = "gpt-3.5" + agent_type_id: str = "gpt-3-cot" + mode: int = 1 +@dataclass +class GPT3BaP(OpenAITextAgent): + openai_model: str = "gpt-3.5-turbo-1106" + agent_type_id: str = "gpt-3-bap" + mode: int = 2 @dataclass -class GPT4Text(OpenAITextAgent): +class GPT4(OpenAITextAgent): openai_model: str = "gpt-4-1106-preview" agent_type_id: str = "gpt-4" - + mode: int = 0 @dataclass -class ChainOfThought(OpenAITextAgent): +class GPT4CoT(OpenAITextAgent): openai_model: str = "gpt-4-1106-preview" - agent_type_id: str = "cot" + agent_type_id: str = "gpt-4-cot" mode: int = 1 @dataclass -class BabbleAndPrune(OpenAITextAgent): +class GPT4BaP(OpenAITextAgent): openai_model: str = "gpt-4-1106-preview" - agent_type_id: str = "b&p" - mode: int = 2 + agent_type_id: str = "gpt-4-bap" + mode: int = 2 \ No newline at end of file diff --git a/agents/random_agent.py b/agents/random_agent.py index a958b45..fb74785 100644 --- a/agents/random_agent.py +++ b/agents/random_agent.py @@ -7,5 +7,5 @@ class RandomAgent(Agent): agent_type_id : str = "random" def take_action(self, rules : Rules, observation: Observation, available_actions: AvailableActions, show_state : bool): - actions = list(available_actions.predefined.keys()) - return Action(action_id=random.choice(actions)) \ No newline at end of file + actions = list(available_actions.predefined.keys()) + list(available_actions.openended.keys()) + return Action(action_id=random.choice(actions), openended_response="") \ No newline at end of file diff --git a/agents/rap/agent.py b/agents/rap/agent.py index ca718a4..20e1e49 100644 --- a/agents/rap/agent.py +++ b/agents/rap/agent.py @@ -14,9 +14,9 @@ class ReasoningViaPlanning(Agent, WorldModel, SearchConfig): """Inherents Agent from api.classes, and WorldModel and SearchConfig from the llm-agents library.""" - agent_type_id: str = "rap" + agent_type_id: str = "gpt4-rap" transparent_reasoning: bool = False - agent_type: int = 0 # 0 = random replies, 1 = human interaction, 2 = openai + agent_type: int = 2 # 0 = random replies, 1 = human interaction, 2 = openai context_builder: Callable[[str, str], ContextType] = None completions: CompletionsFunction = None diff --git a/agents/rap/chat.py b/agents/rap/chat.py index 07401dc..4a8058c 100644 --- a/agents/rap/chat.py +++ b/agents/rap/chat.py @@ -71,7 +71,7 @@ def probabilities( top_logprobs = ( openai_client.chat.completions.create( - model="gpt-3.5-turbo-1106", + model=model, messages=context, logprobs=True, top_logprobs=n, @@ -156,7 +156,7 @@ def image_description(image: Image, rules: Rules) -> str: "content": [ { "type": "text", - "text": "You are playing a game called {rules.title}. The rules are as follows: {rules.summary}.\nThis image is your observation of the game. Describe what's going on in the image.", + "text": f"You are playing a game called {rules.title}. The rules are as follows: {rules.summary}.\nThis image is your observation of the game. Describe what's going on in the image.", }, { "type": "image", diff --git a/agents/rap/context_templates.json b/agents/rap/context_templates.json index 45ac9c8..108ccac 100644 --- a/agents/rap/context_templates.json +++ b/agents/rap/context_templates.json @@ -42,7 +42,7 @@ "This is an open-ended action. Write a description of what you're going to do." ], "goal": [ - "Will you eventually win from this position? yes/no" + "{prefix}Will you eventually win from this position? yes/no" ], "prefix": "You are now playing a game called {title}.\nThe rules are as follows {summary}. {topics}\nYour observation of the game is between and :\n\n{observation}\n\n", "example": "You are playing a game called monty hall.\nThe rules of the game are as follows: there are three doors, behind one of which there is a prize. Select the door with the prize.\nYour observation of the state is between and :\n\nAll three doors are closed.\n\n", diff --git a/agents/rap/reasoners/LICENSE b/agents/rap/reasoners/LICENSE new file mode 100644 index 0000000..e4f139c --- /dev/null +++ b/agents/rap/reasoners/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/agents/rap/reasoners/README.txt b/agents/rap/reasoners/README.txt new file mode 100644 index 0000000..e0fab7a --- /dev/null +++ b/agents/rap/reasoners/README.txt @@ -0,0 +1,7 @@ +This module comes from the llm-reasoners package: +https://github.com/maitrix-org/llm-reasoners/blob/main/ + +This module is licensed under the Apache License 2.0. + +No files were modified. This `reasoners/` module was taken from the original +repository, leaving out other code files from the original. \ No newline at end of file diff --git a/api/classes.py b/api/classes.py index 430de24..72b6f2e 100644 --- a/api/classes.py +++ b/api/classes.py @@ -30,7 +30,7 @@ class Agent: agent_type_id : str @abstractmethod - def take_action(self, rules : dict, observation: Observation, available_actions : AvailableActions): + def take_action(self, rules : dict, observation: Observation, available_actions : AvailableActions, show_state : bool) -> Action: pass @dataclass diff --git a/api/play_game.py b/api/play_game.py index 92e72d1..ada43f6 100644 --- a/api/play_game.py +++ b/api/play_game.py @@ -19,19 +19,19 @@ def play_game(agent_1_path, agent_2_path, game_path, num_matches = 1, save_resul player_1_total = 0 player_2_total = 0 - all_ratings = util.load_json("elo_ratings.json") + #all_ratings = util.load_json("elo_ratings.json") - game_elos = all_ratings.get(game_class.id, {}) + #game_elos = all_ratings.get(game_class.id, {}) - if agent_1_id not in game_elos: - game_elos[agent_1_id] = 1500 - if agent_2_id not in game_elos: - game_elos[agent_2_id] = 1500 + #if agent_1_id not in game_elos: + # game_elos[agent_1_id] = 1500 + #if agent_2_id not in game_elos: + # game_elos[agent_2_id] = 1500 - agent_1_rating = game_elos[agent_1_id] - agent_2_rating = game_elos[agent_2_id] - print(f"{agent_1_id} elo: ", agent_1_rating) - print(f"{agent_2_id} elo: ", agent_2_rating) + #agent_1_rating = game_elos[agent_1_id] + #agent_2_rating = game_elos[agent_2_id] + #print(f"{agent_1_id} elo: ", agent_1_rating) + #print(f"{agent_2_id} elo: ", agent_2_rating) # Get historical win percentage matches = util.load_json("matches.json") @@ -47,11 +47,11 @@ def play_game(agent_1_path, agent_2_path, game_path, num_matches = 1, save_resul print(f'{agent_1_id} avg score: ', agent_1_total/num_matches) print(f'{agent_2_id} avg score: ', 1 - agent_1_total / num_matches) - Q1 = 10**(agent_1_rating / 400) - Q2 = 10**(agent_2_rating / 400) + #Q1 = 10**(agent_1_rating / 400) + #Q2 = 10**(agent_2_rating / 400) - agent_1_expected_score = Q1 / (Q1 + Q2) - agent_2_expected_score = Q2 / (Q1 + Q2) + #agent_1_expected_score = Q1 / (Q1 + Q2) + #agent_2_expected_score = Q2 / (Q1 + Q2) for _ in range(num_matches): if random.choice([0,1]): @@ -81,14 +81,16 @@ def play_game(agent_1_path, agent_2_path, game_path, num_matches = 1, save_resul util.save_json(matches, "matches.json") print("Saved match information") - agent_1_rating = agent_1_rating + K * (player_1_score - agent_1_expected_score) - agent_2_rating = agent_2_rating + K * (player_2_score - agent_2_expected_score) - all_ratings[game_class.id][agent_1_id] = agent_1_rating - all_ratings[game_class.id][agent_2_id] = agent_2_rating - print("Updated elos:") - print(f"{agent_1_id}: ", agent_1_rating) - print(f"{agent_2_id}: ", agent_2_rating) - util.save_json(all_ratings, "elo_ratings.json") + #agent_1_rating = agent_1_rating + K * (player_1_score - agent_1_expected_score) + #agent_2_rating = agent_2_rating + K * (player_2_score - agent_2_expected_score) + # Without below line, we get a KeyError: '' + #all_ratings.setdefault(game_class.id, {}) + #all_ratings[game_class.id][agent_1_id] = agent_1_rating + #all_ratings[game_class.id][agent_2_id] = agent_2_rating + #print("Updated elos:") + #print(f"{agent_1_id}: ", agent_1_rating) + #print(f"{agent_2_id}: ", agent_2_rating) + #util.save_json(all_ratings, "elo_ratings.json") print("") print(f"Agent 1 ({agent_1_id}) average score: ", player_1_total/num_matches) diff --git a/build/lib/agents/human_agent.py b/build/lib/agents/human_agent.py index e23e4c8..a716069 100644 --- a/build/lib/agents/human_agent.py +++ b/build/lib/agents/human_agent.py @@ -2,15 +2,17 @@ class HumanAgent(Agent): agent_type_id = "human" - def take_action(self, rules, observation, available_actions: AvailableActions, show_state : bool): + def take_action(self, rules, observation, available_actions: AvailableActions): print(observation.text) + if observation.image: + observation.image.show() print(available_actions.predefined) - print(available_actions.openended) - action = input("Enter action: ") - openended_list = list(available_actions.openended.keys()) - predefined_list = list(available_actions.predefined.keys()) - if action in predefined_list: - return Action(action_id=available_actions.predefined[action].action_id) - elif action in openended_list: - return Action(action_id=available_actions.openended[action].action_id, openended_response=input("Enter open-ended response: ")) + #print(available_actions.openended) + action = input("Enter action: ") + openended_list = available_actions.openended + predefined_list = available_actions.predefined + if predefined_list and action in [a.action_id for a in predefined_list]: + return Action(action_id=action) + elif openended_list and action in [a.action_id for a in openended_list]: + return Action(action_id=action, openended_response=input("Enter open-ended response: ")) \ No newline at end of file diff --git a/create_rules_appendix.py b/create_rules_appendix.py new file mode 100644 index 0000000..4be1bc8 --- /dev/null +++ b/create_rules_appendix.py @@ -0,0 +1,28 @@ +import api.util as util + +game_paths = [ + "games.arctic_scavengers.arctic_scavengers.ArcticScavengers", + "games.are_you_the_traitor.aytt.AreYouTheTraitor", + "games.two_rooms_and_a_boom.two_rooms.TwoRoomsAndaBoom", + "games.air_land_sea.game.AirLandSea", + "games.codenames.game.CodenamesGame", + "games.hive.game.HiveGame", + "games.santorini.santorini.Santorini", + "games.pit.pit.PitGame", + "games.sea_battle.SeaBattle" +] + +latex = "" +for path in game_paths: + game_class = util.import_class(path) + rules = game_class.rules + + latex += "\\textbf{" + rules.title + "} " + rules.summary + "\n\n" + if rules.additional_details: + latex += "\\begin{itemize}" + for detail, comment in rules.additional_details.items(): + latex += "\n\t\\item \\textbf{" + detail + "} " + comment + latex += "\n\\end{itemize}\n" + +with open("rules_appendix.tex", "w") as f: + f.write(latex) \ No newline at end of file diff --git a/elo_ratings.json b/elo_ratings.json deleted file mode 100644 index d9804ee..0000000 --- a/elo_ratings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "tic_tac_toe": { - "random": 1484.2166867609455, - "gpt-3.5": 1494.800566147782, - "gpt-4": 1493.4909476931325 - }, - "two_rooms_and_a_boom": { - }, - - "pit": { - "random": 1511.7084861590854, - "gpt-3.5": 1494.800566147782, - "gpt-4": 1493.4909476931325 - } -} diff --git a/figures/air_land_sea.png b/figures/air_land_sea.png new file mode 100644 index 0000000..955bd11 Binary files /dev/null and b/figures/air_land_sea.png differ diff --git a/figures/arctic_scavengers.png b/figures/arctic_scavengers.png new file mode 100644 index 0000000..e857d38 Binary files /dev/null and b/figures/arctic_scavengers.png differ diff --git a/figures/are_you_the_traitor.png b/figures/are_you_the_traitor.png new file mode 100644 index 0000000..b65dcbb Binary files /dev/null and b/figures/are_you_the_traitor.png differ diff --git a/figures/average_score.png b/figures/average_score.png new file mode 100644 index 0000000..f13e50b Binary files /dev/null and b/figures/average_score.png differ diff --git a/figures/codenames.png b/figures/codenames.png new file mode 100644 index 0000000..31e406f Binary files /dev/null and b/figures/codenames.png differ diff --git a/figures/hive.png b/figures/hive.png new file mode 100644 index 0000000..abbb2a3 Binary files /dev/null and b/figures/hive.png differ diff --git a/figures/num_matches_per_agent.png b/figures/num_matches_per_agent.png new file mode 100644 index 0000000..9575c0a Binary files /dev/null and b/figures/num_matches_per_agent.png differ diff --git a/figures/num_matches_per_game.png b/figures/num_matches_per_game.png new file mode 100644 index 0000000..847ecf4 Binary files /dev/null and b/figures/num_matches_per_game.png differ diff --git a/figures/overall.jpg b/figures/overall.jpg new file mode 100644 index 0000000..2cd5a85 Binary files /dev/null and b/figures/overall.jpg differ diff --git a/figures/overall_probabilities.png b/figures/overall_probabilities.png new file mode 100644 index 0000000..d333d9e Binary files /dev/null and b/figures/overall_probabilities.png differ diff --git a/figures/overall_rating.png b/figures/overall_rating.png new file mode 100644 index 0000000..612f66c Binary files /dev/null and b/figures/overall_rating.png differ diff --git a/figures/pit.png b/figures/pit.png new file mode 100644 index 0000000..563c9ff Binary files /dev/null and b/figures/pit.png differ diff --git a/figures/rating_scatter.png b/figures/rating_scatter.png new file mode 100644 index 0000000..11f48ff Binary files /dev/null and b/figures/rating_scatter.png differ diff --git a/figures/ratings_table.tex b/figures/ratings_table.tex new file mode 100644 index 0000000..d18abe5 --- /dev/null +++ b/figures/ratings_table.tex @@ -0,0 +1,15 @@ +\begin{tabular}{lcccccccccc} +\toprule +Agent & & \multicolumn{9}{c}{Score} \\ +\cmidrule(lr){2-11} + & Overall & ALS & ARC & AYT & CN & HV & PT & SN & TRB & SB \\ +\midrule +random & -0.50 & 1.07 & \textbf{0.48} & -2.52 & -2.67 & -1.15 & \underline{0.63} & 0.37 & -0.79 & 0.05 \\ +human & \textbf{1.76} & \underline{1.49} & \underline{0.45} & 1.92 & 1.26 & \textbf{3.63} & \textbf{1.29} & -0.89 & \underline{1.70} & \textbf{1.25} \\ +gpt-3 & -0.48 & 1.26 & -0.05 & -1.84 & -2.06 & \underline{1.27} & \underline{0.63} & -0.01 & -2.51 & -0.41 \\ +gpt-3-cot & 0.06 & 0.03 & 0.22 & \underline{2.42} & 0.45 & -0.44 & \underline{0.63} & \underline{0.53} & -2.76 & 0.26 \\ +gpt-4 & -0.89 & -7.38 & -0.12 & -2.73 & -0.65 & -1.31 & -4.42 & -0.08 & 0.62 & -1.40 \\ +gpt-4-cot & \underline{0.16} & \textbf{2.13} & 0.27 & -0.19 & \textbf{2.41} & -1.13 & 0.63 & -0.53 & 1.22 & \underline{0.62} \\ +gpt-4-rap & -0.10 & 1.41 & -1.25 & \textbf{2.94} & \underline{1.26} & -0.86 & \underline{0.63} & \textbf{0.62} & \textbf{2.51} & -0.37 \\ +\bottomrule +\end{tabular} diff --git a/figures/santorini.png b/figures/santorini.png new file mode 100644 index 0000000..3f25587 Binary files /dev/null and b/figures/santorini.png differ diff --git a/figures/scores_table.tex b/figures/scores_table.tex new file mode 100644 index 0000000..26cdf75 --- /dev/null +++ b/figures/scores_table.tex @@ -0,0 +1,15 @@ +\begin{tabular}{lcccccccccc} +\toprule +Agent & & \multicolumn{9}{c}{Score} \\ +\cmidrule(lr){2-11} + & Overall & ALS & ARC & AYT & CN & HV & PT & SN & TRB & SB \\ +\midrule +random & 0.49 & 0.72 & \textbf{0.60} & 0.25 & 0.18 & 0.41 & \underline{0.50} & 0.56 & 0.52 & \underline{0.58} \\ +human & \textbf{0.85} & \textbf{1.00} & NaN & NaN & NaN & \textbf{1.00} & \textbf{1.00} & 0.43 & NaN & \textbf{0.78} \\ +gpt-3 & 0.48 & 0.64 & 0.43 & 0.43 & 0.63 & \underline{0.80} & \underline{0.50} & 0.47 & 0.27 & 0.40 \\ +gpt-3-cot & 0.60 & 0.43 & \underline{0.50} & \underline{0.93} & \underline{0.89} & 0.60 & \underline{0.50} & \textbf{0.61} & 0.33 & 0.55 \\ +gpt-4 & 0.31 & 0.00 & 0.42 & 0.33 & 0.83 & 0.33 & 0.31 & 0.42 & 0.71 & 0.20 \\ +gpt-4-cot & 0.60 & \underline{0.81} & \underline{0.50} & 0.64 & \textbf{1.00} & 0.50 & \underline{0.50} & 0.37 & \underline{0.75} & 0.51 \\ +gpt-4-rap & \underline{0.62} & NaN & 0.33 & \textbf{1.00} & NaN & 0.50 & NaN & \underline{0.58} & \textbf{1.00} & 0.26 \\ +\bottomrule +\end{tabular} diff --git a/figures/sea_battle.png b/figures/sea_battle.png new file mode 100644 index 0000000..5954483 Binary files /dev/null and b/figures/sea_battle.png differ diff --git a/figures/spider_plot.png b/figures/spider_plot.png new file mode 100644 index 0000000..a54bef5 Binary files /dev/null and b/figures/spider_plot.png differ diff --git a/figures/two_rooms_and_a_boom.png b/figures/two_rooms_and_a_boom.png new file mode 100644 index 0000000..3fcc125 Binary files /dev/null and b/figures/two_rooms_and_a_boom.png differ diff --git a/gamebench.egg-info/PKG-INFO b/gamebench.egg-info/PKG-INFO index ea089d8..e2d59f6 100644 --- a/gamebench.egg-info/PKG-INFO +++ b/gamebench.egg-info/PKG-INFO @@ -13,6 +13,8 @@ Description-Content-Type: text/markdown Requires-Dist: fire Requires-Dist: Pillow Requires-Dist: openai +Requires-Dist: santorinai +Requires-Dist: colorama # Setup In the repo root: diff --git a/gamebench.egg-info/requires.txt b/gamebench.egg-info/requires.txt index c625527..55a0a8c 100644 --- a/gamebench.egg-info/requires.txt +++ b/gamebench.egg-info/requires.txt @@ -1,3 +1,5 @@ fire Pillow openai +santorinai +colorama diff --git a/games/air_land_sea/Air-Land-Sea-Rules-Grammar-Checked.rtf b/games/air_land_sea/Air-Land-Sea-Rules-Grammar-Checked.rtf new file mode 100644 index 0000000..43b1134 --- /dev/null +++ b/games/air_land_sea/Air-Land-Sea-Rules-Grammar-Checked.rtf @@ -0,0 +1,90 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2759 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;} +{\colortbl;\red255\green255\blue255;\red12\green12\blue12;\red255\green255\blue255;} +{\*\expandedcolortbl;;\cssrgb\c5098\c5098\c5098;\cssrgb\c100000\c100000\c100000;} +\margl1440\margr1440\vieww28300\viewh17140\viewkind0 +\deftab720 +\pard\pardeftab720\sa400\partightenfactor0 + +\f0\fs32 \cf2 \cb3 \expnd0\expndtw0\kerning0 +\outl0\strokewidth0 \strokec2 Hands:\ +Both players are dealt 6 cards to start each battle. Hands are kept secret from each other. The rest of the cards (the deck) are set aside and kept hidden from both players.\ +Playing the Game:\ +Air, Land, & Sea is played over a series of rounds called Battles. To win a battle, you must either: control more Theaters than your opponent after both players have played all of their cards; or convince your opponent to withdraw.\ +If you win a battle, you earn Victory Points (VPs) represented by tokens. The first player to reach 12 VPs wins the game!\ +Structure of a Battle:\ +During a Battle, the players take turns playing one card at a time, trying to control more Theaters than their opponent.\ +You don\'92t draw cards during a Battle, so be sure to plan carefully and make the most of the 6 cards you are dealt!\ +\ +Theaters:\ +Each of the three Theater boards creates a \'93column\'94 between the players: one for Air, one for Land, and one for Sea. These columns are called Theaters. Cards are always played into these three Theaters. If a card is in a particular Theater\'92s column, we say that the card is \'93in that Theater.\'94\ +\ +Theaters that are next to each other are called \'93adjacent Theaters.\'94\ +\ +A player owns all of the cards on their side of the Theater boards. During your turn, you will play cards only on your side of the Theaters.\ +\ +Battle Cards:\ +Cards are played to advance your war effort and how they are played will ultimately determine who wins the war (the game).\ +\ +Strength: Each card has a Strength value. If the total Strength of all the cards on your side of the Theater is higher than the total Strength of all the cards on your opponent\'92s side of that Theater, you \'93control\'94 that Theater.\ +\ +Tactical Abilities: Most cards have a Tactical Ability along with Strength, which takes effect as soon as the card is played \'93face up\'94 to a Theater. These abilities are either \'93Instant\'94 or \'93Ongoing.\'94\ +\ +Type:\ +There are three types of cards: \'93Air,\'94 \'93Land,\'94 and \'93Sea\'94 cards, which relate to the three Theaters. Normally, you may only play a card \'93face up\'94 to its matching Theater: Air cards in the Air Theater, and so on.\ +\ +Facedown Cards: Cards can also be played \'93facedown\'94 as a \'93wild card\'94 in any Theater. Facedown cards always have a Strength of 2. \'93Facedown\'94 cards do not have any Tactical Abilities. You may see your own facedown cards at any time, but you may not see your opponent's \'93facedown\'94 cards.\ +\ +Covered Cards: When a card is played to a Theater that already contains cards, the newly played card is placed so that it overlaps the previously played card, while still showing the top portion of it. Any card overlapped by another is called a \'93covered card.\'94 Similarly, any card that is not overlapped by another card is referred to as \'93uncovered.\'94\ +\ +Resolving Battle:\ +During a Battle, players take turns starting with the player who has the 1st Player Supreme Commander card.\ +On your turn, you must take only one of these three actions: Deploy, Improvise, Withdraw.\ +\ +Deploy: Play one card from your hand, \'93face up.\'94 When you play a card, you must follow these deployment restrictions: You can only play cards on your side of the Theater boards. The card must be the same type as the Theater you play it to. If you have other cards in that Theater already, you must place the new card so that it covers (partially overlaps) those cards.\ +Improvise: Play one card from your hand, "facedown," to any Theater. "Facedown" cards are treated as "wild cards" and can be played to any Theater regardless of which type they are.\ +Withdraw: If you think your chances of winning the current Battle are low, you may withdraw. If you do, your opponent wins the Battle and gains VPs depending on how many cards are left in your hand. See the Supreme Commander cards for more specific information.\ +\ +Supreme Commander Cards: The 1st Player Supreme Commander wins tied Theaters and gains the following number of VPs based on the number of cards left in their opponent's hand if their opponent withdraws: 4+ cards = 2 VPs, 2-3 cards = 3 VPs, 1 card = 4 VPs, 0 cards = 6 VPs. The 2nd Player Supreme Commander loses tied Theaters and gains the following number of VPs based on the number of cards left in their opponent\'92s hand if their opponent withdraws: 5+ cards = 2 VPs, 3-4 cards = 3 VPs, 2 cards = 4 VPs, 0-1 cards = 6 VPs.\ +\ +Once you have finished your action, your opponent begins their turn. The players continue to alternate taking turns until one of them withdraws or both players have played all of their cards.\ +\ +Tactical Abilities:\ +Most cards have Tactical Abilities described on the card. When you play a card face up from your hand, or if a facedown card is flipped over, its Tactical Ability takes effect immediately. There are two kinds of Tactical Abilities: "Instant" and "Ongoing," indicated on the card.\ +\ +You must carry out the effects of a Tactical Ability unless they contain the word "may."\ +\ +If a Tactical Ability is impossible to perform, that ability is ignored and has no effect.\ +\ +Instant Abilities: These take effect immediately after the card is played or if the card is revealed by being flipped face up. Once the Instant Ability is resolved, it has no further effect (unless somehow that card is played or revealed again).\ +Note: Because instant abilities take effect when flipped face up, it is possible for multiple instant abilities to take effect around the same time. In these situations, always resolve the instant abilities in the order they happened and fully resolve each ability before moving on to the next.\ +2nd Note: Once an instant ability begins taking effect, it always resolves fully, even if it gets flipped facedown before completing.\ +\ +Ongoing Abilities: These are always in effect as long as the card is face up. If a card with an Ongoing Ability is flipped "facedown," the ability no longer has any effect (unless that card is revealed again).\ +Example: The Escalation Tactical Ability increases the Strength of all of your facedown cards to 4 as long as the Escalation card remains "face up." If that card were flipped over by another Tactical Ability, your "facedown" cards would go back to being Strength 2.\ +\ +Tactical Ability Key Terms:\ +Flip: Many Tactical Abilities allow you to flip a card. Flipping a card means either turning it "face up" if it is "facedown" or turning a "facedown" card so it is "face up."\ +Unless the ability states otherwise, you may flip any card \'97 yours or your opponent's.\ +\ +Uncovered/Covered: Many Tactical Abilities only affect uncovered or covered cards. If an ability does not specify uncovered or covered, such as Transport or Redeploy, assume the ability can affect any card.\ +\ +Play: Some Tactical Abilities instruct you to play a card, or only take effect in response to a card being played. The word "play" describes any time a player takes a card from their hand and places it in a Theater.\ +\ +Non-Matching Theaters: Means that a card is not in the Theater of its type. The card does not suffer any penalty for being in the "wrong" Theater.\ +\ +Destroy: Some Tactical Abilities instruct you to destroy a card. Destroyed cards are always placed facedown on the bottom of the deck. If a card is destroyed immediately after it is played, such as by Blockade, then that card does not get to use its Tactical Ability.\ +\ +Occupied: When counting the number of cards that occupy a Theater, always count both players' cards towards that total.\ +\ +Move: When a card is moved to a different Theater. It stays on the same side of the Theaters it was already on and remains owned by the same player. Moved cards are placed on top of any cards already in the Theater it was moved to. It covers those cards.\ +\ +Ending Battles: There are two ways a Battle can end: If a player withdraws, their opponent wins the Battle. Or if both players have played all of the cards in their hand. At this point, the player who controls the most Theaters wins the Battle.\ +\ +In order to control a Theater, you must have a higher total Strength there than your opponent has in that Theater. If your Strengths are tied, the 1st Player wins the tie and controls that Theater. If there are no cards on either side of the Theater, the 1st player controls that Theater.\ +\ +Scoring Victory Points:\ +If neither player withdraws, the winner of the Battle scores 6 VPs. If one of the players withdraws, the other player scores VPs based on the number of cards left in the withdrawing player's hand (see the Supreme Commander Cards for details). After scoring VPs, check if the victor has enough VPs to win the game (12 VPs). If they don\'92t, fight another Battle.\ +\ +Setting up Battles:\ +All cards are collected and shuffled together to create a new deck. Deal each player a new hand of 6 cards. Next, the Theater cards are rotated clockwise so that the rightmost Theater is moved to the far left of the Theater lineup. Lastly, players swap Supreme Commander cards. The player who was 1st in the last battle is now 2nd.} \ No newline at end of file diff --git a/games/air_land_sea/Air-Land-Sea-Rules-raw.rtf b/games/air_land_sea/Air-Land-Sea-Rules-raw.rtf new file mode 100644 index 0000000..68c65a9 --- /dev/null +++ b/games/air_land_sea/Air-Land-Sea-Rules-raw.rtf @@ -0,0 +1,108 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2759 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\margl1440\margr1440\vieww9840\viewh17140\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 Hands:\ +Both players are dealt 6 cards to start each battle. Hands are kept secret from each other.\ +The rest of the cards (the deck) are set aside and kept hidden from both players.\ +\ +Playing the game:\ +Air, Land, & Sea is played over a series of rounds called Battles. To win a battle, you must either: Control more Theaters than your opponent after both players have played all of their cards; or Convince your opponent to withdraw.\ +\ +If you win a battle you earn Victory Points (VPs) represented by tokens. The first player to reach 12 VPs wins the game!\ +\ +Structure of a battle:\ +During a Battle, the players take turns playing one card at a time, trying to control more Theaters than their opponent.\ +\ +You don\'92t draw cards during a Battle, so be sure to plan carefully and make the most of the 6 cards you are dealt!\ +\ +Theaters:\ +Each of the three Theater boars creates a \'93 column\'94 between the players: one for Air, one for Land, and one for Sea. These columns are called Theaters. Cards are always played into these three Theaters. If a card is in a particular Theater\'92s column we say that the card is \'93in that Theater\'94\ +\ +Theters that are next to each other are called \'93adjacent Theaters\'94.\ +\ +A player owns all of the cards on their side of the Theater boards. During your turn you will play cards only on your side of the Theaters.\ +\ +Battle Cards:\ +Cards are played to advance your war effort and howthey are played will ultimately determine who wins the war (the game)\ +\ +Strength: Each card has a Strength value. If the total Strength of all the cards on your side of the Theater is higher than the total Strength of all the cards on your ooponest side of that Theater you \'93control\'94 that Theater\ +\ +Tactical Abilities: Most cards have a Tactical Ability along with Strength, which takes effect as soon as the card is played \'93face up\'94 to a Theater. These abilities are either \'93Instant\'94 or \'93Ongoing\'94.\ +\ +Type:\ +There are three types of cards \'93Air\'94, \'93Land\'94, and \'93Sea\'94 cards. Which relate to the three Theaters. Normally you may only play a card \'93face\'94 to its matching Theater: Air cards in the Air Theater, and so on.\ +\ +Facedown Cards: Cards can also be played \'93facedown\'94 as a \'93wild card\'94 in any Theater. Facedown cards always have a Strength of 2. \'93Facedown\'94 cards do not have any Tactical Abilities. You may see you own facedown cards at any time, but you may not see you opponents \'93facedown\'94 cards.\ +\ +Covered Cards: When a card is played to a Theater that already contains cards, the newly played card is placed so that it overlaps the previously played card, while still showing the top portion of it. Any card overlapped by another is called a \'93covered card\'94. Similarly, any card that is not overlapped by another card is referred to as \'93uncovered\'94.\ +\ +Resolving Battle\ +During a Battle, players take turns starting with the player who has the 1st Player Supreme Commander card.\ +\ +On your turn, you must take only one of these three actions:\ +Deploy, Improvise, Withdraw.\ +\ +Deploy: Play one card from your hand, \'93face\'94. When you play a card, you must follow these deployment restrictions: You can only play cards on your side of the Theater boards. The card must be the same type as the Theater you play it to. If you have other cards in that Theater already, you must place the new card so that it covers (partially overlaps) those cards.\ +\ +Improvise: Play one card from your hand, \'93facedown\'94, to any Theater. \'93Facedown\'94 cards are treated as \'93wild cards\'94, and can be played to any Theater regardless of which type they are.\ +\ +Withdraw: If you think your chances of winning the current Battle are low, you may withdraw. If you do, your opponent wins the Battle and gains VPs depending on how many cards are left in your hand. See the Supreme Commander cards for more specific information.\ +\ +Supreme Commander Cards: The 1st Player Supreme Commander wins tied Theaters and gains the following number VPs based on number of cards left in their opponents hand if their opponent withdraws: 4+ cards = 2 VPs, 2-3 cards = 3 VPs, 1 card = 4 VPs, 0 cards = 6 VPs. The 2nd Player Supreme Commander loses tied Theaters and gains the following number VPs based on number of cards left in their opponent\'92s hand if their opponent withdraws: 5+ cards = 2 VPs, 3-4 cards = 3 VPs, 2 cards = 4 VPs, 0-1 cards = 6 VPs.\ +\ +Once you have finished your action, your opponent begins their turn. The players continue to alternate taking turns until one of them withdraws or both players have played all of their cards.\ +\ +Tactical Abilities:\ +Most cards have Tactical Abilities described on the card. When you play a card face from your hand, or if a facedown card is flipped over, its Tactical Ability takes effect immediately. There are two kinds of Tactical Abilities: \'93Instant\'94 and \'93Ongoing\'94, indicated on the card. \ +\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 You must carry out the effects of a Tactical Ability unless they contain the word \'93may\'94.\ +\ +If a Tactical Ability is impossible to perform, that ability is ignored and has no effect.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +Instant Abilities: These take effect immediately after the card is played or if the card is revealed by being flipped face. Once the Instant Ability is resolved, it has no further effect (unless somehow that card is played or revealed again)\ +\ +Example: The Transport Tactical Ability allows you to move any card you own to a different Theater immediately after you play the Transport card.\ +\ +Note: Because instant abilities take effect when flipped face, it is possible for multiple instant abilities too take effect around the same time. IN these situations, always resolve the instant abilities in the order they happened and fully resolve each ability before moving on to the next. \ +\ +2nd Note: Once an instant ability begins taking effect, it always resolves fully, even if it gets flipped facedown before completing.\ +\ +\ +Ongoing Abilities: These are always in effect as long as the card is face. If a card with an Ongoing Ability is flipped \'93facedown\'94, the ability no longer has any effect (unless that card is revealed again).\ +\ +Example: The Escalation Tactical Ability increases the Strength of all of your facedown cards to 4 as long as the Escalation cards remains \'93face\'94. If that card were flipped over by another Tactical Ability, your \'93facedown\'94 cards would go back to being Strength 2.\ +\ +Tactical Ability Key Terms:\ +Flip: Many Tactical Abilities allow you to flip a card. Flipping a card means either turning it \'93faceup\'94 if it is \'93facedown\'94 or turning a \'93facedown\'94 card so it is \'93face\'94.\ +\ +Unless the ability states otherwise, you may flip any card \'97 yours or your opoonent\'92s.\ +\ +Uncovered/Covered: Many Tactical Abilities only affect uncovered or covered cards. If an ability does not specify uncovered or covered, such as Transport or Redeploy, assume the ability can affect any card.\ +\ +Play: Some Tactical Abilities instruct you to play a card, or only take effect in response to a card being played. The word \'93play\'94 describes any time a player take a card from their hand and places it in a Theater.\ +\ +Non-Matching Theaters: Means that a card is not in the Theater of its type. The card does not suffer any penalty for being in the \'93wrong\'94 Theater.\ +\ +Destroy: Some Tactical Abilities instruct you to destroy a card. Destroyed cards are always placed facedown on the bottom of the deck. If a card is destroyed immediately after it is played, such as by Blockade, then that card does not get to use its Tactical Ability.\ +\ +Occupied: When counting the number of cards that Occupy a Theater, always count both player\'92s cards towards that total.\ +\ +Move: When a card is moved to a different Theater. It stays on the same side of the Theaters it was already on and remains owned by the same player. Moved cards are placed on top of any cards already in the Theater it was moved to. It covers those cards.\ +\ +Ending Battles: There are two ways a Battle can end:\ +If a player withdraw, their opponent wins the Battle. Or If both players have played all of the cards in their hand. At this point, the players who controls the most Theaters wins the Battle. \ +\ +In order to control a Theater, you must have a higher total Strength there than your opponent has in that Theater. If your Strengths are tied. The 1st Player wins the tie and controls that Theater. If there are no cards on either side of the Theater, the 1st player controls that Theater.\ +\ +Scoring Victory Points:\ +If neither player withdraws the winner of the Battle scores 6 VPs. If one of the players withdraws, the other player scores VPs based on the number of cards left in the withdrawing players hand. After scoring VPs, check if the victor has enough VPs to win the game (12 VPs). If they don\'92t fight another Battle.\ +\ +Setting up Battles:\ +All cards are collected and shuffled together to create a new deck. Deal each player a new hand of 6 cards. Next, the Theater cards are rotated clockwise so that the right most Theater is moved to the far left of the Theater lineup. Lastly, players swap Supreme Commander cards. The player who was 1st in the last battle is now 2nd.\ +} \ No newline at end of file diff --git a/games/air_land_sea/Air_Land_and_Sea_Rulebook_Revised.pdf b/games/air_land_sea/Air_Land_and_Sea_Rulebook_Revised.pdf new file mode 100644 index 0000000..5aca58b Binary files /dev/null and b/games/air_land_sea/Air_Land_and_Sea_Rulebook_Revised.pdf differ diff --git a/games/air_land_sea/README.md b/games/air_land_sea/README.md new file mode 100644 index 0000000..eec66c0 --- /dev/null +++ b/games/air_land_sea/README.md @@ -0,0 +1,6 @@ +to test without agents use: + +```bash +export PYTHONPATH=~/VSCode/GameBench +python ./games/air_land_sea/test.py +``` diff --git a/games/air_land_sea/board.py b/games/air_land_sea/board.py new file mode 100644 index 0000000..648808f --- /dev/null +++ b/games/air_land_sea/board.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass, field +from typing import List, Tuple, Optional +import random +from games.air_land_sea.cards import Card +from games.air_land_sea.effect_manager import EffectManager +import re + +@dataclass +class Theater: + name: str + player_1_cards: List[Card] = field(default_factory=list) + player_2_cards: List[Card] = field(default_factory=list) + + def __post_init__(self): + self.player_cards = [self.player_1_cards, self.player_2_cards] + + def is_uncovered(self, card: Card, player_id: int) -> bool: + # the last one in the list is the uncovered card + return card == self.player_cards[player_id][-1] + # if player_id == 0: + # return card == self.player_1_cards[-1] + # elif player_id == 1: + # return card == self.player_2_cards[-1] + # else: + # raise ValueError("player_id must be 0 or 1") + + """ + Air Theater: + Player 1: [F-35 (5, Ongoing: +1 to adjacent Theaters, uncovered), Face down (2, covered)] + Player 2: [Face down (2, covered), Tank Buster (4, Instant: Destroy one face-down card, uncovered)] + """ + + def get_theater_string(self, owner_id: int): + # return a string representation of the theater with representation of covered/uncovered cards + theater_name = self.name # no "Theater" at the end + + # helper function + def process_cards(cards, player_id, owner_id): + total_string = "" + for i, card in enumerate(cards): + # might be easier to affect the current strength in the card class, only problem is how to handle dissipation + # all of owner's facedown cards are strength 4 + # owner sees the normal card but with "Facedown-" in front of the name and strength set to 2 + card_string = str(card) + # only if the owner is looking at their own card that is also facedown + if card.facedown == True: + card_string = re.sub(r' \(\d', f"> ({card.current_strength}-<{card.strength}>", card_string) + if (owner_id == player_id) or (owner_id == 3): + card_string = "Facedown-<" + card_string + else: + # viewing as opponent + # when viewing as opponent just make the whole card string equal "Facedown (2)" + card_string = f"Facedown ({card.current_strength})" + # replace $ with "uncovered" or "covered" + card_status = "uncovered" if self.is_uncovered(card, player_id) else "covered" + card_string = card_string.replace(")", ", " + card_status + ")") + card_string = re.sub(r' \(\d', f" ({card.current_strength}", card_string) + # handle commas + if i == 0: + total_string += card_string + else: + total_string += f", {card_string}" + return total_string + + player_1_total_string = process_cards(self.player_1_cards, 0, owner_id) + player_2_total_string = process_cards(self.player_2_cards, 1, owner_id) + + return_string = f"{theater_name} Theater:\nPlayer 1: [{player_1_total_string}]\nPlayer 2: [{player_2_total_string}]" + return return_string + +@dataclass +class Board: + theaters: List[Theater] = field(default_factory=lambda: [ + Theater('Air'), + Theater('Sea'), + Theater('Land') + ]) + # ongoing_effects: List[Card] = field(default_factory=list) # ongoing effects that affect the board + + def __post_init__(self): + # shuffle theaters into random order + random.shuffle(self.theaters) + pass + + def clear_cards(self): + for theater in self.theaters: + theater.player_1_cards.clear() + theater.player_2_cards.clear() + for player_id in range(2): + theater.player_cards[player_id].clear() + + def rotate_theaters(self): + # rotate the theaters clockwise + self.theaters.append(self.theaters.pop(0)) + + def get_board_string(self, owner_id: int): + # return a string representation of the board + return_string = "" + for theater in self.theaters: + return_string += theater.get_theater_string(owner_id) + "\n" + return return_string + + def get_theater_by_name(self, theater_name: str) -> Theater: + # return the theater with the given name + for theater in self.theaters: + if theater.name == theater_name: + return theater + return None + + def search_ongoing_effect_location(self, card: Card, effect_manager: EffectManager) -> List[Optional[int]]: + # this function checks if a card is in play and in effect manager as well as which theater it is in + target_theater = [None, None] + # returns a list of size 2 + for player_id in range(2): + if card in effect_manager.effect_cards[player_id]: + # if the player has Support, find adjacent theaters + for index, theater in enumerate(self.theaters): + if card in theater.player_cards[player_id]: + # if the theater has Support, place its index in the tuple associated with the player + target_theater[player_id] = index + if target_theater == [None, None]: + return None + return target_theater + + def search_card(self, card_name: str, theater_name: str) -> Card: + # return the card with the given name and theater + # theater = self.get_theater_by_name(theater_name) + + # print("inside board.search_card - card_name:{}, theater_name:{}".format(card_name, theater_name)) + # print("found theater:{}".format(theater)) + + for player_id in range(2): + for theater in self.theaters: + for card in theater.player_cards[player_id]: + if card.name == card_name and card.theater == theater_name: + return card + # print("inside board.search_card: could not find card in any theater") + return None + + def get_adjacent_theaters(self, theater_index) -> List[int]: + """ + Calculate the adjacent theaters based on the current theater index. + + :param theater_index: Index of the current theater (0, 1, or 2) + :return: A list of indexes for the adjacent theaters + """ + # Check for the middle theater, which has both neighbors + if theater_index == 1: + return [0, 2] + # For theaters at the ends (0 and 2), they only have one adjacent theater (1) + return [1] + + def get_theater_index(self, theater_name: str) -> int: + # return the index of the theater with the given name + for index, theater in enumerate(self.theaters): + if theater.name == theater_name: + return index + return None + + + def get_theater_strengths(self, effect_manager: EffectManager) -> List[Tuple[int, int]]: + # return the strength of each theater + # return the strength of the player in the theater + # check for Support card + # then apply to computed adjacent theaters if so + theater_strengths = [[], [], []] + support = Card('Support', 'Air', 1, 'Ongoing', 'You gain +3 strength in each adjacent theater') + support_search = self.search_ongoing_effect_location(support, effect_manager) + # find adjacent theaters if Support is in play to apply its effect + # say we get [None, 0] (player 2 has Support in the third theater) + for player_id in range(2): + if support_search is not None: + if support_search[player_id] is not None: + adj_theaters = self.get_adjacent_theaters(support_search[player_id]) + # calculate the strength of each theater based on the cards in it normally + for index, theater in enumerate(self.theaters): + strength = 0 + for card in theater.player_cards[player_id]: + strength += card.current_strength + # print(f'theater: {index}, card: {card.name}, current_strength: {card.current_strength}, strength: {card.strength}, facedown: {card.facedown}') + # apply Support effect if necessary + if support_search is not None: + if support_search[player_id] is not None: + if index in adj_theaters: + strength += 3 + theater_strengths[index].append(strength) + return theater_strengths + + def move_card(self, card : Card, to_theater : Theater): + # move card to the theater on the same side it is already on + for player_id in range(2): + for theater in self.theaters: + if card in theater.player_cards[player_id]: + theater.player_cards[player_id].remove(card) + # apparently to_theater is the same as the theater it is already in + to_theater.player_cards[player_id].append(card) + # print("inside move_card: moved card") + return + # print("could not find card in any theater") diff --git a/games/air_land_sea/cards.py b/games/air_land_sea/cards.py new file mode 100644 index 0000000..0c54940 --- /dev/null +++ b/games/air_land_sea/cards.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass, field +from typing import Optional, List +import random + +@dataclass +class Card: + name: str + theater: str + strength: int + tactical_ability_type: Optional[str] = None + tactical_ability_description: Optional[str] = None + facedown: bool = False + + def __post_init__(self): + self.current_strength = self.strength + + def __str__(self): + card_info = f"{self.name} ({self.strength}" + tactical_ablity_string = "" + # if self.tactical_ability_type and not self.facedown: + if self.tactical_ability_type: + tactical_ablity_string = f", {self.tactical_ability_type}: {self.tactical_ability_description}" + # $ represents the covered/uncovered status of the card (it will be manipulated by the theater class) + return f"{card_info}, {self.theater}{tactical_ablity_string})" + + def __repr__(self): + return self.__str__() + + def flip(self): + if self.facedown: + # print("flipping faceup") + # flip faceup + self.current_strength = self.strength + self.facedown = False + else: + # print("flipping facedown") + # flip facedown + self.current_strength = 2 + self.facedown = True + +@dataclass +class Deck: + cards: List[Card] = field(default_factory=lambda: [ + Card('Support', 'Air', 1, 'Ongoing', 'You gain +3 strength in each adjacent theater'), + Card('Air Drop', 'Air', 2, 'Instant', 'The next time you play a card, you may play it to a non-matching theater'), + Card('Maneuver', 'Air', 3, 'Instant', 'Flip an uncovered card in an adjacent theater'), + Card('Aerodrome', 'Air', 4, 'Ongoing', 'You may play cards of strength 3 or less to non-matching theaters'), + Card('Containment', 'Air', 5, 'Ongoing', 'If any player plays a facedown card, destroy that card'), + Card('Heavy Bombers', 'Air', 6), + Card('Transport', 'Sea', 1, 'Instant', 'You may move 1 of your cards to a different theater'), + Card('Escalation', 'Sea', 2, 'Ongoing', 'All your facedown cards are now strength 4'), + Card('Maneuver', 'Sea', 3, 'Instant', 'Flip an uncovered card in an adjacent theater'), + Card('Redeploy', 'Sea', 4, 'Instant', 'You may return 1 of your facedown cards to your hand. If you do, play a card'), + Card('Blockade', 'Sea', 5, 'Ongoing', 'If any player plays a card to an adjacent theater occupied by at least 3 other cards, destroy that card'), + Card('Super Battleship', 'Sea', 6), + Card('Reinforce', 'Land', 1, 'Instant', 'Draw 1 card and play it facedown to an adjacent theater'), + Card('Ambush', 'Land', 2, 'Instant', 'Flip any uncovered card'), + Card('Maneuver', 'Land', 3, 'Instant', 'Flip an uncovered card in an adjacent theater'), + Card('Cover Fire', 'Land', 4, 'Ongoing', 'All cards covered by this card are now strength 4'), + Card('Disrupt', 'Land', 5, 'Ongoing', 'Starting with you, both players choose and flip 1 of their uncovered cards'), + Card('Heavy Tanks', 'Land', 6) + ]) + + def __post_init__(self): + self.shuffle() + + def shuffle(self): + random.shuffle(self.cards) + + def draw(self): + return self.cards.pop() + + def add(self, card: Card): + self.cards.insert(0, card) + + def deal(self): + # deal 6 cards from the deck + return [self.draw() for _ in range(6)] \ No newline at end of file diff --git a/games/air_land_sea/effect_manager.py b/games/air_land_sea/effect_manager.py new file mode 100644 index 0000000..1152572 --- /dev/null +++ b/games/air_land_sea/effect_manager.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass, field +from typing import List, Dict +from games.air_land_sea.cards import Card +from api.classes import AvailableActions, Action +import pprint + +@dataclass +class EffectManager: + player_1_effect_cards : List[Card] = field(default_factory=list) + player_2_effect_cards : List[Card] = field(default_factory=list) + + + def __post_init__(self): + self.effect_cards = [self.player_1_effect_cards, self.player_2_effect_cards] + + def non_matching_theaters(self, current_theater: str) -> List[str]: + # returns a list of theater names that are not the same as the input theater + theaters = ['Air', 'Sea', 'Land'] + theaters.remove(current_theater) + return theaters + + def modify_available_actions(self, available_actions : AvailableActions, player_hand : List[Card], player_id : int) -> AvailableActions: + # modifies the available actions based on the effects in play + # Aerodrome (3 strength or less to non matching theaters) + # Airdrop (1 time to non matching theater) + # called right after available actions are generated + + # first check for Aerodrome Effect in player's effect cards + # print("player id:", player_id) + if 'Aerodrome' in [card.name for card in self.effect_cards[player_id]]: + # allow player to play cards of strength 3 or less faceup to non matching theaters + # what does available actions look like? + # if n = number of cards there are 3n actions (n play faceup + 3n facedown for each theater) + # if 1 card is strength 3 or less, add 2 actions to play it faceup to non matching theaters + # print("inside aerodrome") + # print("player hand") + pprint.pprint(player_hand) + three_or_less = [card for card in player_hand if card.strength <= 3] + if len(three_or_less) > 0: + # if not empty, add the actions + num_available_actions = len(available_actions.predefined) + # print(f"num available actions: {num_available_actions}") + for ind, card in enumerate(three_or_less): + # identify non matching theaters of the card + non_matching_theaters = self.non_matching_theaters(card.theater) + for nm_ind, theater in enumerate(non_matching_theaters): + available_actions.predefined[str(num_available_actions + ind*2 + nm_ind)] = f"Play {card} faceup to {theater}." + # print("Available actions after Aerodrome") + # pprint.pprint(available_actions.predefined) + + # next check for Airdrop Effect in player's effect cards + if 'Air Drop' in [card.name for card in self.effect_cards[player_id]]: + # print("inside Air Drop") + # allow player to play 1 card faceup to non matching theater + num_available_actions = len(available_actions.predefined) + # make sure to remove effect after it is used, how? + # if n cards to play, add 2n actions (faceup play in all non matching theaters) + for ind, card in enumerate(player_hand): + non_matching_theaters = self.non_matching_theaters(card.theater) + for nm_ind, theater in enumerate(non_matching_theaters): + available_actions.predefined[str(num_available_actions + ind*2 + nm_ind)] = f"Play {card} faceup to {theater}." + # after this function is called, the effect is removed + # airdrop = Card('Air Drop', 'Air', 2, 'Instant', 'The next time you play a card, you may play it to a non-matching theater') + airdrop = [card for card in self.effect_cards[player_id] if card.name == 'Air Drop'][0] + # print("Available actions after Air Drop") + pprint.pprint(available_actions.predefined) + + # print("check if airdrop really got removed from effects") + # print("before") + # print(self.effect_cards[player_id]) + self.remove_effect(airdrop, player_id) + # print("after") + # print(self.effect_cards[player_id]) + + return available_actions + + def add_effect(self, card : Card, player_id : int): + # happens after post play triggers are checked + self.effect_cards[player_id].append(card) + pass + + def remove_effect(self, card : Card, player_id : int): + self.effect_cards[player_id].remove(card) + pass diff --git a/games/air_land_sea/game.py b/games/air_land_sea/game.py new file mode 100644 index 0000000..364204b --- /dev/null +++ b/games/air_land_sea/game.py @@ -0,0 +1,1047 @@ +from dataclasses import dataclass, field +import random +from abc import abstractmethod +from typing import List, Dict, Optional, Tuple +from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules +import ast +from .board import Board, Theater +from .player import Player +from .cards import Card, Deck +import random +from .effect_manager import EffectManager +import re +import pprint + +@dataclass +class AirLandSea(Game): + rules : Rules = Rules( + title="Air Land and Sea", + summary=("A strategic card game where two players compete" + "over a series of battles to control different Theaters" + "of war: Air, Land, and Sea. Each player is dealt 6 cards" + "representing various military units and tactics. " + "Players win a battle by controlling more Theaters than " + "their opponent or convincing their opponent to withdraw. " + "Victory Points (VPs) are earned by winning battles, " + "and the first player to reach 12 VPs wins the game. " + "Players must carefully manage their hand and strategically deploy cards to outmaneuver their opponent."), + additional_details = { + "Battle Structure": ("During a Battle, the players take turns playing one card at a time, trying to control more Theaters than their opponent." + "You don’t draw cards during a Battle, so be sure to plan carefully and make the most of the 6 cards you are dealt!"), + "Theaters": ("Each of the three Theater boards creates a 'column' between the players: one for Air, one for Land, and one for Sea. These columns are called Theaters. Cards are always played into these three Theaters. If a card is in a particular Theater’s column, we say that the card is 'in that Theater.'\n" + "Theaters that are next to each other are called 'adjacent Theaters.'" + "A player owns all of the cards on their side of the Theater boards. During your turn, you will play cards only on your side of the Theaters."), + "Battle Cards": ("Cards are played to advance your war effort and how they are played will ultimately determine who wins the war (the game).\n" + "Strength: Each card has a Strength value. If the total Strength of all the cards on your side of the Theater is higher than the total Strength of all the cards on your opponent’s side of that Theater, you 'control' that Theater.\n" + "Tactical Abilities: Most cards have a Tactical Ability along with Strength, which takes effect as soon as the card is played 'face up' to a Theater. These abilities are either 'Instant' or 'Ongoing.'"), + "Type of Battle Cards": ("There are three types of cards: 'Air,' 'Land,' and 'Sea' cards, which relate to the three Theaters. Normally, you may only play a card 'face up' to its matching Theater: Air cards in the Air Theater, and so on."), + "Facedown Cards": ("Cards can also be played 'facedown' as a 'wild card' in any Theater. Facedown cards always have a Strength of 2. 'Facedown' cards do not have any Tactical Abilities. You may see your own facedown cards at any time, but you may not see your opponent's 'facedown' cards."), + "Covered Cards": ("When a card is played to a Theater that already contains cards, the newly played card is placed so that it overlaps the previously played card, while still showing the top portion of it. Any card overlapped by another is called a 'covered card.' Similarly, any card that is not overlapped by another card is referred to as 'uncovered.'"), + "Resolving Battle": ("During a Battle, players take turns starting with the player who has the 1st Player me Commander card.\n" + "On your turn, you must take only one of these three actions: Deploy, Improvise, Withdraw.\n" + "Once you have finished your action, your opponent begins their turn. The players continue to alternate taking turns until one of them withdraws or both players have played all of their cards."), + "Possible actions:": ("Deploy: Play one card from your hand, 'face up.' When you play a card, you must follow these deployment restrictions: You can only play cards on your side of the Theater boards. The card must be the same type as the Theater you play it to. If you have other cards in that Theater already, you must place the new card so that it covers (partially overlaps) those cards.\n" + "Improvise: Play one card from your hand, 'facedown', to any Theater. 'Facedown' cards are treated as 'wild cards' and can be played to any Theater regardless of which type they are.\n" + "Withdraw: If you think your chances of winning the current Battle are low, you may withdraw. If you do, your opponent wins the Battle and gains VPs depending on how many cards are left in your hand. See the me Commander cards for more specific information."), + "me Commander Cards": ("Supreme Commander Cards: The 1st Player Supreme Commander wins tied Theaters and gains the following number of VPs based on the number of cards left in their opponent's hand if their opponent withdraws: 5+ cards = 2 VPs, 3-4 cards = 3 VPs, 2 cards = 4 VPs, 0-1 cards = 6 VPs.\n" + "The 2nd Player me Commander loses tied Theaters and gains the following number of VPs based on the number of cards left in their opponent’s hand if their opponent withdraws: 4+ cards = 2 VPs, 2-3 cards = 3 VPs, 1 card = 4 VPs, 0 cards = 6 VPs."), + "Tactical Abilities": ("Most cards have Tactical Abilities described on the card. When you play a card face up from your hand, or if a facedown card is flipped over, its Tactical Ability takes effect immediately. There are two kinds of Tactical Abilities: 'Instant' and 'Ongoing', indicated on the card.\n" + "You must carry out the effects of a Tactical Ability unless they contain the word 'may'.\n" + "If a Tactical Ability is impossible to perform, that ability is ignored and has no effect."), + "Instant Abilities": ("Instant Abilities take effect immediately after the card is played or if the card is revealed by being flipped face up. Once the Instant Ability is resolved, it has no further effect (unless somehow that card is played or revealed again).\n" + "Note: Because instant abilities take effect when flipped face up, it is possible for multiple instant abilities to take effect around the same time. In these situations, always resolve the instant abilities in the order they happened and fully resolve each ability before moving on to the next.\n" + "Once an instant ability begins taking effect, it always resolves fully, even if it gets flipped facedown before completing."), + "Ongoing Abilities": ("These are always in effect as long as the card is face up. If a card with an Ongoing Ability is flipped 'facedown', the ability no longer has any effect (unless that card is revealed again).\n" + "Example: The Escalation Tactical Ability increases the Strength of all of your facedown cards to 4 as long as the Escalation card remains 'face up'. If that card were flipped over by another Tactical Ability, your 'facedown' cards would go back to being Strength 2."), + "Tactical Ability Key Terms": + ("Flip: Many Tactical Abilities allow you to flip a card. Flipping a card means either turning it 'face up' if it is 'facedown' or turning a 'facedown' card so it is 'face up.'" + "Unless the ability states otherwise, you may flip any card — yours or your opponent's.\n" + "Uncovered/Covered: Many Tactical Abilities only affect uncovered or covered cards. If an ability does not specify uncovered or covered, such as Transport or Redeploy, assume the ability can affect any card.\n" + "Play: Some Tactical Abilities instruct you to play a card, or only take effect in response to a card being played. The word 'play' describes any time a player takes a card from their hand and places it in a Theater.\n" + "Non-Matching Theaters: Means that a card is not in the Theater of its type. The card does not suffer any penalty for being in the 'wrong' Theater.\n" + "Destroy: Some Tactical Abilities instruct you to destroy a card. Destroyed cards are always placed facedown on the bottom of the deck. If a card is destroyed immediately after it is played, such as by Blockade, then that card does not get to use its Tactical Ability.\n" + "Occupied: When counting the number of cards that occupy a Theater, always count both players' cards towards that total.\n" + "Move: When a card is moved to a different Theater. It stays on the same side of the Theaters it was already on and remains owned by the same player. Moved cards are placed on top of any cards already in the Theater it was moved to. It covers those cards."), + "Ending Battles": ("There are two ways a Battle can end: If a player withdraws, their opponent wins the Battle. Or if both players have played all of the cards in their hand. At this point, the player who controls the most Theaters wins the Battle." + "In order to control a Theater, you must have a higher total Strength there than your opponent has in that Theater. If your Strengths are tied, the 1st Player wins the tie and controls that Theater. If there are no cards on either side of the Theater, the 1st player controls that Theater."), + "Scoring Victory Points": ("If neither player withdraws, the winner of the Battle scores 6 VPs. If one of the players withdraws, the other player scores VPs based on the number of cards left in the withdrawing player's hand (see the me Commander Cards for details). After scoring VPs, check if the victor has enough VPs to win the game (12 VPs). If they don’t, fight another Battle."), + "Setting up Battles": ("All cards are collected and shuffled together to create a new deck. Deal each player a new hand of 6 cards. Next, the Theater cards are rotated clockwise so that the rightmost Theater is moved to the far left of the Theater lineup. Lastly, players swap me Commander cards. The player who was 1st in the last battle is now 2nd."), + } + ) + id : str = "air_land_sea" + # first key is the player who won's supreme commander + # subkey is the number of cards in the loser's hand + withdrawal_points: Dict[int, Dict[int, int]] = field(default_factory=lambda: { + 0 : { + 6: 2, + 5: 2, + 4: 3, + 3: 3, + 2: 4, + 1: 6, + 0: 6 + }, + 1 : { + 6: 2, + 5: 2, + 4: 2, + 3: 3, + 2: 3, + 1: 4, + 0: 6 + } + }) + + def init_game(self, agent1 : Agent, agent2 : Agent): + self.effect_manager = EffectManager() + self.board : Board = Board() # theater_order is randomized on init + self.deck = Deck() # shuffled on init + p1_hand = self.deck.deal() + p2_hand = self.deck.deal() + p1_supreme_commander = random.choice([0, 1]) + p2_supreme_commander = 1 - p1_supreme_commander + self.agents = [agent1(team_id = 0, agent_id = 0, **self.agent_1_kwargs), agent2(team_id = 1, agent_id = 1, **self.agent_2_kwargs)] + self.player1 = Player(id=0, supreme_commander=p1_supreme_commander, agent=self.agents[0], hand=p1_hand) + self.player2 = Player(id=1, supreme_commander=p2_supreme_commander, agent=self.agents[1], hand=p2_hand) + self.players = [self.player1, self.player2] + + def get_player_by_agent_id(self, agent_id : int) -> Player: + for player in self.players: + if player.id == agent_id: + return player + return None + + def apply_strength_effects(self): + # called in get_observation + # applies increase in current strength from Escalation and Cover Fire to applicable cards + # check effect manager for existence and index of escalation and(or) cover fire + + # Use ongoing effect location to determine if in play and in effect manager and for who and where + escalation = self.board.search_card("Escalation", "Sea") + escalation_search = self.board.search_ongoing_effect_location(escalation, self.effect_manager) + cover_fire = self.board.search_card("Cover Fire", "Land") + cover_fire_search = self.board.search_ongoing_effect_location(cover_fire, self.effect_manager) + + # find all facedown cards in one player's side of the board and set current strength to 4 + # but if escalation is not, then set all facedown cards to 2? + if escalation_search: + escalation_player_ids = [player_id for player_id, theater_ind in enumerate(escalation_search) if theater_ind] + if self.show_state: + print("escalation_player_ids:", escalation_player_ids) + for player_id in escalation_player_ids: + for theater in self.board.theaters: + for card in theater.player_cards[player_id]: + if card.facedown: + card.current_strength = 4 + else: + for player in self.players: + for theater in self.board.theaters: + for card in theater.player_cards[player.id]: + if card.facedown: + card.current_strength = 2 + + # find covered cards by cover_fire then make their current strength 4 + if cover_fire_search: + cover_fire_player_ids = [player_id for player_id, theater_ind in enumerate(cover_fire_search) if theater_ind] + if self.show_state: + print("cover fire search:", cover_fire_search) + print("cover fire player ids:", cover_fire_player_ids) + for player_id in cover_fire_player_ids: + theater_cards = self.board.theaters[cover_fire_search[player_id]].player_cards[player_id] + cover_fire_ind = theater_cards.index(cover_fire) + cards_to_affect = theater_cards[:cover_fire_ind] + if self.show_state: + print("cards to affect:", cards_to_affect) + for card in cards_to_affect: + card.current_strength = 4 + else: + for player in self.players: + for theater in self.board.theaters: + for card in theater.player_cards[player.id]: + if not card.facedown: + card.current_strength = card.strength + pass + + # generate observation and available actions for the agent + def get_observation(self, agent : Agent) -> Tuple[Observation, AvailableActions]: + player = self.get_player_by_agent_id(agent.agent_id) + # print("player_id", player.id) + # Observation includes + hand = player.hand + supreme_commander = "1st" if player.supreme_commander == 0 else "2nd" if player.supreme_commander == 1 else "error" + # print("supreme commander:",supreme_commander) + hand_size = str(len(player.hand)) + opponent = self.get_player_by_agent_id(1 - agent.agent_id) + opponent_hand_size = str(len(opponent.hand)) + # print("opponent hand size:",opponent_hand_size) + victory_points = str(player.victory_points) + # print("victory points:",victory_points) + # the opponent sees the name as facedown + # but the player sees the normal card but with "Facedown-" in front of the name and strength set to 2 + self.apply_strength_effects() + board_string = self.board.get_board_string(player.id) + hand_string = "" + for card in hand: + hand_string += " "+ str(card) + "\n" + + observation_text = ( + "\n" + "----- Player " + str(player.id + 1) + "'s action -----\n" + "Current Hand: \n" + hand_string + "" + "Current Supreme Commander: " + supreme_commander + "\n" + "Current Victory Points: " + victory_points + "\n" + "Current Hand Size: " + hand_size + "\n" + "Current Opponent Hand Size: " + opponent_hand_size + "\n" + "Current Board: \n" + board_string + ) + + # a dictionary formatted like so: + # { 'Play {card.name}' : 'Play {card} faceup to {card.theater}'}} + cards_to_play = {} + for action_id, card in enumerate(hand): + cards_to_play[str(action_id)] = f"Play {card} faceup to {card.theater}. Deploy." + # Facedown cards can be played to any theater, make 3 actions for each card for each of the 3 theaters. + # facedown action_id must increase counting up from len(hand) + for action_id, card in enumerate(hand, start=len(hand)): + cards_to_play[str(action_id)] = f"Play {card} facedown to Air. Improvise." + for action_id, card in enumerate(hand, start=len(hand)*2): + cards_to_play[str(action_id)] = f"Play {card} facedown to Land. Improvise." + for action_id, card in enumerate(hand, start=len(hand)*3): + cards_to_play[str(action_id)] = f"Play {card} facedown to Sea. Improvise." + + cards_to_play[str(len(hand)*4)] = "Withdraw from the battle. Opponent scores VPs based on the number of cards left in your hand." + + available_actions = AvailableActions( + instructions = "Select a card from your hand to play to a theater", + predefined = cards_to_play, + openended = {} + ) + return Observation(text=observation_text), available_actions + + def find_card_from_action(self, action : Action, available_actions: AvailableActions, agent : Agent=None) -> Card: + # the agent is the one is playing or flipping the card + action_id = action.action_id + action_desc = available_actions.predefined[action_id] + # find which card was just played + # can use name and theater to find the card + # name will suffice except for maneuver + # find name in string (comes after "Play" and before the '(') + + # card_name_pattern = r'Play\s+([^(]+)\s+\(' # old + card_name_pattern = r"(?:Play|Flip|Move|Return)\s+([^(]+)\s+\(" # checks between Play or Flip and the '(' + theater_pattern = r'\d,\s+(\w+)\s*[),]?' # checks after number and comma and before the the next comma (excluding whitespace) + card_name_match = re.search(card_name_pattern, action_desc) + if card_name_match: + card_name = card_name_match.group(1).strip() # .strip() to remove trailing spaces + else: + card_name = None + theater_match = re.search(theater_pattern, action_desc) + if theater_match: + theater_name = theater_match.group(1) + else: + theater_name = None + + + # print("inside find_card_from_action") + # print("card name:", card_name + ".end") + # print("theater:", theater + ".end") + found_card = None + if action_desc.startswith("Flip") or action_desc.startswith("Move") or action_desc.startswith("Return"): + # locate from board + if card_name == "Facedown": + # print("inside card_name == Facedown in find card") + # when card_name is Facedown it's the opponent's facedown card + # the case where the agent is flipping an unknown (ie. the oppponent's) uncovered facedown card + theater = self.find_theater_played_from_action(action, available_actions) + # print("theater found:", theater) + # choose the opponent's uncovered facedown card + opponent_id = 1 - agent.agent_id + found_card = theater.player_cards[opponent_id][-1] + else: + if action_desc.startswith("Move") and card_name.startswith("Facedown-<"): + # the case where we're moving a facedown card we own looks like "Facedown-" + card_name = re.search(r"<([^>]+)>", card_name).group(1) + theater_name = re.search(r'>,\s*(\w+)', action_desc).group(1) + # print("inside find_card_from_action Move Facedown-< format:") + # print("card_name:", card_name) + # print("theater_name:", theater_name) + + # locate from theater + # print("using flip to search board for card") + # print("running board.search_card in find_card") + found_card = self.board.search_card(card_name, theater_name) + else: + # locate from hand if play + # you need to locate it from their hand not the board + found_card = self.player1.search_hand(card_name, theater_name) + if found_card == None: + found_card = self.player2.search_hand(card_name, theater_name) + if found_card == None: + print("error: could not find card from action") + # print("found card in find_card:", found_card) + return found_card + + def find_theater_played_from_action(self, action : Action, available_actions: AvailableActions) -> Theater: + # returns string of theater name + action_id = action.action_id + action_desc = available_actions.predefined[action_id] + theater_pattern = r'to (\w+)\.' # after the word "to" and before the period + if action_desc.startswith("Flip") or action_desc.startswith("Return"): + theater_pattern = r'\)\s+in\s+(\w+)' # the word after the ')' and 'in' + theater_match = re.search(theater_pattern, action_desc) + if theater_match: + theater = theater_match.group(1) + else: + theater = None + # print("theater found in find_theater:", theater) + # print("inside find_theater_played_from_action") + # print("theater:", theater) + theater = self.board.get_theater_by_name(theater) + return theater + + def find_faceup_or_facedown_from_action(self, action : Action, available_actions : AvailableActions) -> bool: + action_id = action.action_id + action_desc = available_actions.predefined[action_id] + if 'faceup' not in action_desc: + return "facedown" + else: + return "faceup" + + def check_destroy_triggers(self, action : Action, available_actions : AvailableActions): + # returns a flag indicating whether to destroy the card or not + destroy = False + action_id = action.action_id + action_desc = available_actions.predefined[action_id] + # checking for Containment, Blockade + # first check for Containment Effect in any player's effect cards + if any(card.name == 'Containment' for player_cards in self.effect_manager.effect_cards for card in player_cards): + # if the description does not contain the word faceup then the action was to play a facedown card + if 'faceup' not in action_desc: + # destroy the card + destroy = True + if any(card.name == 'Blockade' for player_cards in self.effect_manager.effect_cards for card in player_cards): + # find just_played card its location + just_played_card = self.find_card_from_action(action, available_actions) + just_played_card_location = self.find_theater_played_from_action(action, available_actions) + + # print("inside blockade - just_played_card_location = ", just_played_card_location) + # print("inside blockade - just_played_card = ", just_played_card) + + # find theater of Blockade card by searching for it in every theater + for player in self.players: + for theater in self.board.theaters: + for card in theater.player_cards[player.id]: + if card.name == 'Blockade': + blockade_location = theater + break + + # print("inside blockade - blockade_location = ", blockade_location) + + # find adjacent theaters to the Blockade card's current theater + blockade_index = self.board.get_theater_index(blockade_location.name) + adjacent_theaters_indices = self.board.get_adjacent_theaters(blockade_index) + adjacent_theaters = [] + for theater_ind in adjacent_theaters_indices: + adjacent_theaters.append(self.board.theaters[theater_ind]) + + # print("inside blockade - adjacent_theaters = ", adjacent_theaters) + + # if the just_played card is in an adjacent theater to the Blockade card, then check if the adjacent theater has 3 or more cards already + for theater in adjacent_theaters: + if just_played_card_location == theater: + if len(theater.player_1_cards) + len(theater.player_2_cards) >= 3: + destroy = True + + # print("inside blockade - destroy = ", destroy) + + # old version + # blockade = Card('Blockade', 'Sea', 5, 'Ongoing', 'If any player plays a card to an adjacent theater occupied by at least 3 other cards, destroy that card') + # blockade_location = self.board.search_ongoing_effect_location(blockade, self.effect_manager) # a list or none + # # blockade_location looks like [p1_theater_id, p2_theater_id] + # # doesn't matter which player's it is, just need to find the adjacent theaters of both if they both exist + # # find adjacent theaters to the Blockade card's current position + # adjacent_theaters_p1 = self.board.get_adjacent_theaters(blockade_location[0]) + # adjacent_theaters_p2 = self.board.get_adjacent_theaters(blockade_location[1]) + # # merge the two lists + # adjacent_theaters = list(set(adjacent_theaters_p1 + adjacent_theaters_p2)) + + # # find location of just_played_card + # just_played_card_location = self.find_theater_played_from_action(action, available_actions) + + # # access theaters by indices + + # for theater_ind in adjacent_theaters: + # if len(self.board.theaters[theater_ind].player_1_cards) + len(self.board.theaters[theater_ind].player_2_cards) >= 3 and self.board.theaters[theater_ind] == just_played_card_location: + # destroy = True + return destroy + + # I pass in observation + available actions to agent, then it will choose one + def update(self, action : Action, available_actions : AvailableActions, agent : Agent) -> Optional[Agent]: + + # check for withdraw + if available_actions.predefined[action.action_id].startswith("Withdraw"): + # how do i signal to the outer function that the battle is over? + # how do i signal to the outer funciton that a player withdrew? + return agent + + # To make dissapearing and reappearing work for ongoing effects (by flip) + # on card flip, call add effect and resolve effect + # this will be done when i we process agent output in update() + if self.check_destroy_triggers(action, available_actions): + # remove card from hand and end turn + player = self.get_player_by_agent_id(agent.agent_id) + played_card = self.find_card_from_action(action, available_actions, agent) + player.hand.remove(played_card) + if self.show_state: + print("Destroyed card:", played_card) + print(player.hand) + return None + played_card, played_to_theater = self.play_card_from_action(action, available_actions, agent) + # print("played card facedown:",played_card.facedown) + if not (played_card.name == 'Heavy Bombers' or played_card.name == 'Super Battleship' or played_card.name == 'Heavy Tanks') and not played_card.facedown: + # print("resolving effect in update") + self.resolve_effect(played_card, agent, played_to_theater) + return None + + def play_card_from_action(self, action : Action, available_actions : AvailableActions, agent : Agent): + player = self.get_player_by_agent_id(agent.agent_id) + # take in action and available action string and turn it into playing a card + card = self.find_card_from_action(action, available_actions) # Card Object + theater = self.find_theater_played_from_action(action, available_actions) + faceup_or_facedown = self.find_faceup_or_facedown_from_action(action, available_actions) # string + is_faceup = True if faceup_or_facedown == 'faceup' else False + + player.play(card, is_faceup, theater, self.show_state) + return card, theater + + def flip_card_from_action(self, action : Action, available_actions : AvailableActions, agent : Agent) -> Tuple[Card, Theater]: + # find card name + card = self.find_card_from_action(action, available_actions, agent) + # print("found card") + # print(card) + # find theater + theater = self.find_theater_played_from_action(action, available_actions) + # print(theater) + # apply flip + for player in self.players: + for current_card in theater.player_cards[player.id]: + # print(id(card)) + # print(card) + # print(id(current_card)) + # print(current_card) + if current_card == card: + # print("found card") + current_card.flip() + if (current_card in self.effect_manager.effect_cards[player.id]) and current_card.facedown and current_card.name != "Air Drop": + # if self.show_state: + # print("effect cards before removing effect:") + # print(self.effect_manager.effect_cards[player.id]) + # pass + self.effect_manager.remove_effect(current_card, player.id) + # if self.show_state: + # print("removing effect:", current_card) + # print("effect cards after removing effect:") + # print(self.effect_manager.effect_cards[player.id]) + # pass + return current_card, theater + return None, None + + def resolve_effect(self, input_card : Card, agent : Agent, theater : Theater): + # takes in the card that was just played, the agent that played it, and the theater it was played to + player = self.get_player_by_agent_id(agent.agent_id) + opponent = self.get_player_by_agent_id(1 - agent.agent_id) + + self.effect_manager.add_effect(input_card, agent.agent_id) + if self.show_state: + print("inside resolve_effect - effect cards after adding effect:") + print(self.effect_manager.effect_cards) + # applies game logic of tactical abilities that happen immediately + # does not handle + # Support (calculated at end of game) + # 6 strength cards (Heavy Bombers, Super Battleship, Heavy Tanks) + # Containment + Blockade (take effect in post play triggers) + # Aerodrome + Airdrop (take effect after available actions are generated next turn) + # Handles + # Maneuver, Ambush, Transport, Redeploy, Reinforce, Disrupt (immediate extra action) + + # normal loop: + # get_observation + # modify_available_actions + # get agent output action + # check if withdraw + # update + # check_destroy_triggers + # apply action + # resolve_effect + # add_effect + # do effect + # remove effect + + + # the extra action procedures are to be coded in the game class + + if input_card.name == 'Maneuver': + # flip an uncovered card in an adjacent theater + # find which theater is adjacent to the theater the card was played in + # get_observation + modified available actions dict + # call get observation normally on the player + # generate available actions for the player that are only to flip an uncovered card in an adjacent theater + # get agent output action + # apply flip + # add effect + # resolve effect + observation, _ = self.get_observation(agent) + # modify available actions to only allow flipping an uncovered card in an adjacent theater + # generate actions to flip an uncovered card in an adjacent theater + uncovered_cards = [] + cards_to_flip = {} + theater_index = self.board.get_theater_index(theater.name) + adjacent_theaters_indices = self.board.get_adjacent_theaters(theater_index) + adjacent_theaters = [] + for theater_ind in adjacent_theaters_indices: + adjacent_theaters.append(self.board.theaters[theater_ind]) + # go through cards in each theater and find uncovered cards + # uncovered just means it is the last in the list (index is -1) + for theater in adjacent_theaters: + for player_id in range(2): + if theater.player_cards[player_id]: + uncovered_card = theater.player_cards[player_id][-1] + uncovered_cards.append((uncovered_card, theater, player_id)) + + for action_id, (card, theater, player_id) in enumerate(uncovered_cards): + if card.facedown: + # check if it is player's card or opponent's card + if player_id == player.id: + # player owns the facedown card and can see its contents + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} faceup." + else: + # opponent owns the facedown card and cannot see its contents + cards_to_flip[str(action_id)] = f"Flip Facedown (2) in {theater.name} faceup." + elif not card.facedown: + # faceup + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} facedown." + else: + if self.show_state: + print("error: card is neither faceup nor facedown") + + maneuver_available_actions = AvailableActions( + instructions = "Select an uncovered card from an adjacent theater to flip.", + predefined = cards_to_flip, + openended = {} + ) + if self.show_state: + print(observation.text) + print("maneuver_available_actions") + print(maneuver_available_actions.predefined) + # call take action + if len(maneuver_available_actions.predefined) == 0: + if self.show_state: + print("no cards to flip") + self.effect_manager.remove_effect(input_card, player.id) + return + # agent_output = agent.take_action(self.rules, observation, maneuver_available_actions, show_state=self.show_state) + agent_output = self.take_action_wrapper(agent, observation, maneuver_available_actions) + # print("player", player.id + 1) + flipped_card, target_theater = self.flip_card_from_action(agent_output, maneuver_available_actions, agent) + # print(input_card) + # print(id(input_card)) + self.effect_manager.remove_effect(input_card, player.id) + if flipped_card and not flipped_card.facedown and not (flipped_card.name == "Heavy Bombers" or flipped_card.name == "Super Battleship" or flipped_card.name == "Heavy Tanks"): + # search target_theater for the card and identify who owns it + for flipped_owner in self.players: + if flipped_card in target_theater.player_cards[flipped_owner.id]: + # resolve effect (if flipped faceup) for the player who owns the flipped card + self.resolve_effect(flipped_card, flipped_owner.agent, target_theater) + break + return + elif input_card.name == 'Ambush': + # flip any uncovered card + # get_observation + modified available actions dict + # get agent output action + # apply flip + # add effect + # resolve effect + observation, _ = self.get_observation(agent) + # modify available actions to only allow flipping an uncovered card in an adjacent theater + # generate actions to flip an uncovered card in an adjacent theater + uncovered_cards = [] + cards_to_flip = {} + # go through cards in each theater and find uncovered cards + # uncovered just means it is the last in the list (index is -1) + for theater in self.board.theaters: + for player_id in range(2): + if theater.player_cards[player_id]: + uncovered_card = theater.player_cards[player_id][-1] + uncovered_cards.append((uncovered_card, theater, player_id)) + + for action_id, (card, theater, player_id) in enumerate(uncovered_cards): + if card.facedown: + # check if it is player's card or opponent's card + if player_id == player.id: + # player owns the facedown card and can see its contents + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} faceup." + else: + # opponent owns the facedown card and cannot see its contents + cards_to_flip[str(action_id)] = f"Flip Facedown (2) in {theater.name} faceup." + elif not card.facedown: + # faceup + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} facedown." + else: + if self.show_state: + print("error: card is neither faceup nor facedown") + + ambush_available_actions = AvailableActions( + instructions = "Select any uncovered card to flip.", + predefined = cards_to_flip, + openended = {} + ) + if self.show_state: + print(observation.text) + print("ambush_available_actions") + print(ambush_available_actions.predefined) + # call take action + if len(ambush_available_actions.predefined) == 0: + if self.show_state: + print("no cards to flip") + self.effect_manager.remove_effect(input_card, player.id) + return + # agent_output = agent.take_action(self.rules, observation, ambush_available_actions, True) + agent_output = self.take_action_wrapper(agent, observation, ambush_available_actions) + # print("player", player.id + 1) + flipped_card, target_theater = self.flip_card_from_action(agent_output, ambush_available_actions, agent) + # print(input_card) + # print(id(input_card)) + if input_card in self.effect_manager.effect_cards[player.id]: + self.effect_manager.remove_effect(input_card, player.id) + if flipped_card and not flipped_card.facedown and not (flipped_card.name == "Heavy Bombers" or flipped_card.name == "Super Battleship" or flipped_card.name == "Heavy Tanks"): + # search target_theater for the card and identify who owns it + for flipped_owner in self.players: + if flipped_card in target_theater.player_cards[flipped_owner.id]: + # resolve effect (if flipped faceup) for the player who owns the flipped card + self.resolve_effect(flipped_card, flipped_owner.agent, target_theater) + break + return + elif input_card.name == 'Transport': + # move 1 of your cards to a different theater + # get_observation + modified available actions dict + observation, _ = self.get_observation(agent) + # generate available actions to move 1 of player's card to a different theater + player_cards = [] + cards_to_move = {} + for theater in self.board.theaters: + for card in theater.player_cards[player.id]: + player_cards.append((card, theater)) + + action_id = 0 + for target_theater in self.board.theaters: + for card, theater in player_cards: + if target_theater != theater: + if card.facedown: + card_string = str(card) + card_string = re.sub(r' \(\d', f"> (2-<{card.strength}>", card_string) + card_string = "Facedown-<" + card_string + cards_to_move[str(action_id)] = f"Move {card_string} in {theater.name} to {target_theater.name}." + else: + cards_to_move[str(action_id)] = f"Move {card} in {theater.name} to {target_theater.name}." + action_id += 1 + cards_to_move[str(action_id)] = "Do not move any cards." + + transport_available_actions = AvailableActions( + instructions = "Select one of your cards to move to a different theater. You may also choose to not move anything.", + predefined = cards_to_move, + openended = {} + ) + if self.show_state: + print(observation.text) + print("transport_available_actions") + pprint.pprint(transport_available_actions.predefined) + + # get agent output action + # action = agent.take_action(self.rules, observation, transport_available_actions, show_state=self.show_state) + action = self.take_action_wrapper(agent, observation, transport_available_actions) + # apply move + found_card = self.find_card_from_action(action, transport_available_actions, agent) + if not found_card: + # then we didn't want to move a card or didn't find one + self.effect_manager.remove_effect(input_card, player.id) + return + # move the card to the target theater + # find the target theater + target_theater = self.find_theater_played_from_action(action, transport_available_actions) + # print("moving card") + # print("target_theater:", target_theater) + self.board.move_card(found_card, target_theater) + self.effect_manager.remove_effect(input_card, player.id) + return + elif input_card.name == 'Redeploy': + # you may return 1 of your facedown cards to your hand. If you do, play a card + # Return + # get_observation + modified available actions dict + # get agent output action + # apply return + observation, _ = self.get_observation(agent) + + # generate available actions to return 1 of player's facedown cards to their hand + facedown_cards = [] + cards_to_return = {} + for theater in self.board.theaters: + for card in theater.player_cards[player.id]: + if card.facedown: + facedown_cards.append((card, theater)) + + action_id = 0 + for card, theater in facedown_cards: + cards_to_return[str(action_id)] = f"Return {card} in {theater.name} to your hand in order to play a card." + action_id += 1 + cards_to_return[str(action_id)] = "Do not return any cards." + + redeploy_available_actions = AvailableActions( + instructions = "Select one of your facedown cards to return to your hand. You may also choose to not return anything (not use this tactical ability).", + predefined = cards_to_return, + openended = {} + ) + if self.show_state: + print(observation.text) + print("redeploy_available_actions") + pprint.pprint(redeploy_available_actions.predefined) + # get agent output action + # action = agent.take_action(self.rules, observation, redeploy_available_actions, show_state=self.show_state) + action = self.take_action_wrapper(agent, observation, redeploy_available_actions) + + # apply return + if action.action_id == str(action_id): + # then we didn't want to return a card + self.effect_manager.remove_effect(input_card, player.id) + return + found_card = self.find_card_from_action(action, redeploy_available_actions, agent) + current_theater = self.find_theater_played_from_action(action, redeploy_available_actions) + + current_theater.player_cards[player.id].remove(found_card) + player.hand.append(found_card) + # make it not facedown, when it goes back to hand + found_card.flip() + + # play (normal loop) (maybe this is just calling the update function again on the same player) + # get_observation + # modify_available_actions + # get agent output action + # update + # check_destroy_triggers + # apply action + # resolve_effect + # add_effect + # do effect + # remove_effect + observation, available_actions = self.get_observation(agent) + + # remove withdraw from available actions here + # print("available actions before removing withdraw") + # pprint.pprint(available_actions.predefined) + available_actions.predefined.pop(str(len(player.hand)*4)) + # print("available actions after removing withdraw") + # pprint.pprint(available_actions.predefined) + modified_actions = self.effect_manager.modify_available_actions(available_actions, player.hand, player.id) + if self.show_state: + print(observation.text) + print("Actions after checking for Air Drop and Aerodrome ongoing effects:") + pprint.pprint(modified_actions.predefined) + # action = agent.take_action(self.rules, observation, modified_actions, show_state=self.show_state) + action = self.take_action_wrapper(agent, observation, modified_actions) + self.update(action, modified_actions, agent) + if input_card in self.effect_manager.effect_cards[player.id]: + self.effect_manager.remove_effect(input_card, player.id) + return + elif input_card.name == 'Reinforce': + # draw 1 card and play it facedown to an adjacent theater + # draw new card + # play (but only facedown actions) + # get_observation + modified available actions dict (only facedown) + # get agent output action + # update + # check_destroy_triggers + # apply action + + drawn_card = self.deck.draw() + player.hand.append(drawn_card) + observation, _ = self.get_observation(agent) + + if self.show_state: + print("inside reinforce") + print("drawing a card from self.deck") + print(drawn_card) + + # generate available actions to play the drawn card facedown to an adjacent theater + theater_ind = self.board.get_theater_index(theater.name) + adjacent_theaters_indices = self.board.get_adjacent_theaters(theater_ind) + adjacent_theaters = [] + for theater_ind in adjacent_theaters_indices: + adjacent_theaters.append(self.board.theaters[theater_ind]) + + places_to_play = {} + for action_id, target_theater in enumerate(adjacent_theaters): + places_to_play[str(action_id)] = f"Play {drawn_card} facedown to {target_theater.name}." + + reinforce_available_actions = AvailableActions( + instructions = "Select an adjacent theater to play the drawn card facedown.", + predefined = places_to_play, + openended = {} + ) + if self.show_state: + print(observation.text) + print("reinforce_available_actions") + pprint.pprint(reinforce_available_actions.predefined) + + # get agent output action + # action = agent.take_action(self.rules, observation, reinforce_available_actions, show_state=self.show_state) + action = self.take_action_wrapper(agent, observation, reinforce_available_actions) + # play the drawn card facedown to the target theater + target_theater = self.find_theater_played_from_action(action, reinforce_available_actions) + player.play(drawn_card, False, target_theater, self.show_state) + self.effect_manager.remove_effect(input_card, player.id) + pass + elif input_card.name == 'Disrupt': + # Starting with you, both players choose and flip 1 of their uncovered cards + # owner -> get_observation + modified available actions dict + # get agent output action + # apply flip + # add effect (if flipped faceup) + # resolve effect (if flipped faceup) + # opponent -> get_observation + modified available actions dict + # get agent output action + # apply flip + # add effect (if flipped faceup) + # resolve effect (if flipped faceup) + def disrupt_flip(current_player : Player, current_agent : Agent) -> Optional[Tuple[Card, Agent, Theater]]: + # print("inside disrupt_flip:", self.effect_manager.effect_cards) + observation, _ = self.get_observation(current_agent) + # generate available actions for player to flip one of their uncovered cards + player_uncovered_cards = [] + cards_to_flip = {} + for theater in self.board.theaters: + for card in theater.player_cards[current_player.id]: + if theater.is_uncovered(card, current_player.id): + player_uncovered_cards.append((card, theater)) + + action_id = 0 + for card, theater in player_uncovered_cards: + if card.facedown: + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} faceup." + else: + cards_to_flip[str(action_id)] = f"Flip {card} in {theater.name} facedown." + action_id += 1 + disrupt_available_actions = AvailableActions( + instructions = "Select one of your uncovered cards to flip.", + predefined = cards_to_flip, + openended = {} + ) + + if self.show_state: + print(observation.text) + print("disrupt_available_actions") + pprint.pprint(disrupt_available_actions.predefined) + + if len(disrupt_available_actions.predefined) == 0: + if self.show_state: + print("no cards to flip") + return None + + # get agent output action + # action = current_agent.take_action(self.rules, observation, disrupt_available_actions, show_state=self.show_state) + action = self.take_action_wrapper(agent, observation, disrupt_available_actions) + + # apply flip + flipped_card, target_theater = self.flip_card_from_action(action, disrupt_available_actions, current_agent) + # add effect (if flipped faceup) + # move the resolve effect call on flipped card to end + if not flipped_card.facedown and not (flipped_card.name == "Heavy Bombers" or flipped_card.name == "Super Battleship" or flipped_card.name == "Heavy Tanks"): + # return so we can resolve effect later for the player who owns the flipped card + # self.resolve_effect(flipped_card, current_agent, target_theater) + return flipped_card, current_agent, target_theater + return None + + effect_stack = [] + # Player's flip + if self.show_state: + print(f"Calling {player.id} disrupt flip") + player_param_tuple = disrupt_flip(player, agent) + if player_param_tuple: + effect_stack.append(player_param_tuple) + # opponent's flip + if self.show_state: + print(f"Calling {opponent.id} disrupt flip") + opponent_param_tuple = disrupt_flip(opponent, opponent.agent) + if opponent_param_tuple: + effect_stack.append(opponent_param_tuple) + + # finish disrupt effect before resolving flip effects + if input_card in self.effect_manager.effect_cards[player.id]: + self.effect_manager.remove_effect(input_card, player.id) + + # process effect stack in order + for param_tuple in effect_stack: + self.resolve_effect(*param_tuple) + return + else: + pass + + def take_action_wrapper(self, agent: Agent, observation: Observation, available_actions: AvailableActions) -> Action: + # check if the action id is in the available actions + # if not, let it try two more times (inside here) + # if third time is invalid, choose randomly + num_tries = 0 + while num_tries < 3: + try: + action = agent.take_action(self.rules, observation, available_actions, show_state=self.show_state) + if action.action_id in available_actions.predefined: + # If the action is valid, return it immediately + if self.show_state: + print("Action selected:", action.action_id) + return action + else: + # If the action_id is not in available actions, raise ValueError to trigger except block + raise IndexError("Invalid action selected.") + except Exception as e: + # Handle invalid action selection, either from user input or any other issue + if self.show_state: + print(e) + print("Invalid action selected. Please try again.") + num_tries += 1 + # If the loop exits due to reaching the maximum number of tries, choose a random action + random_action_id = random.choice(list(available_actions.predefined.keys())) + if self.show_state: + print("Selecting a random action due to repeated invalid selections.") + print("Random action selected:", random_action_id) + return Action(action_id=random_action_id) + + def battle_setup(self): + # clear board & clear each theater's cards + self.board.clear_cards() + self.deck = Deck() + # clear hands & deal new hands + for player in self.players: + player.hand = [] + player.hand = self.deck.deal() + # switch supreme commander + self.players[0].supreme_commander, self.players[1].supreme_commander = self.players[1].supreme_commander, self.players[0].supreme_commander + # new effect manager + self.effect_manager = EffectManager() + # rotate theaters + self.board.rotate_theaters() + + # Returns the scores for agent_1 and agent_2 after the game is finished. + # the high level gameplay loop + # is run after init_game is ran + def play(self) -> Tuple[float, float]: + if self.show_state: + print("Starting Game!") + # Game Setup (already basically done in init_game) + game_over = False + while not game_over: + # Battle Setup + # we'll need to reset game state for each battle + self.battle_setup() + battle_over = False + # determine who goes first based on supreme commander + current_agent_turn = None + for player in self.players: + if player.supreme_commander == 0: + current_agent_turn = player.agent + break + withdrawn_agent = None + if self.show_state: + print("Starting Battle!") + while not battle_over: + # process player turns + current_player = self.get_player_by_agent_id(current_agent_turn.agent_id) + observation, available_actions = self.get_observation(current_agent_turn) + modified_actions = self.effect_manager.modify_available_actions(available_actions, current_player.hand, current_player.id) + if self.show_state: + print(observation.text) + print("Current Effects in Play:",self.effect_manager.effect_cards) + print("printing actions after checking for Air Drop and Aerodrome ongoing effects") + pprint.pprint(modified_actions.predefined) + action = self.take_action_wrapper(current_agent_turn, observation, modified_actions) + # action = current_agent_turn.take_action(self.rules, observation, modified_actions, show_state=self.show_state) + if self.show_state: + print("action taken:", action) + withdrawn_agent = self.update(action, modified_actions, current_agent_turn) + # check if both players have 0 cards in hand or player withdrew to end battle + if withdrawn_agent or (len(self.players[0].hand) == 0 and len(self.players[1].hand) == 0): + battle_over = True + else: + # switch agent turn + if current_agent_turn == self.players[0].agent: + current_agent_turn = self.players[1].agent + else: + current_agent_turn = self.players[0].agent + # battle resolution (add victory points, check for win) + if self.show_state: + print(self.board.get_board_string(3)) # 3 is the owner id for spectating + print("Battle Over!") + # if a player withdrew + if withdrawn_agent: + # decide who won and lost + victor = self.get_player_by_agent_id(1 - withdrawn_agent.agent_id) + loser = self.get_player_by_agent_id(withdrawn_agent.agent_id) + # add victory points to opponent + loser_hand_size = len(loser.hand) + gained_victory_points = self.withdrawal_points[victor.supreme_commander][loser_hand_size] + victor.victory_points += gained_victory_points + if self.show_state: + print("Player", withdrawn_agent.agent_id + 1, "withdrew!") + print("Player", victor.id + 1, "won the battle and gained", gained_victory_points,"VPs!") + print("Player", victor.id + 1, "VPs:", victor.victory_points - gained_victory_points, "-->", victor.victory_points) + print("Player", loser.id + 1, "VPs:", loser.victory_points) + else: + theater_strengths = self.board.get_theater_strengths(self.effect_manager) + # find victor and loser + # theater strengths looks like [[p1_strength, p2_strength], [p1_strength, p2_strength], [p1_strength, p2_strength]] + player_1_theater_wins = 0 + player_2_theater_wins = 0 + for theater in theater_strengths: + if self.player1.supreme_commander == 0: + if theater[0] >= theater[1]: + player_1_theater_wins += 1 + else: + player_2_theater_wins += 1 + elif self.player2.supreme_commander == 0: + if theater[1] >= theater[0]: + player_2_theater_wins += 1 + else: + player_1_theater_wins += 1 + # victor has the most theater wins + if player_1_theater_wins > player_2_theater_wins: + victor = self.player1 + else: + victor = self.player2 + # add victory points to victor + victor.victory_points += 6 + if self.show_state: + print("Both players have no cards in hand!") + print(theater_strengths) + print("Player 1 won", player_1_theater_wins, "theaters") + print("Player 2 won", player_2_theater_wins, "theaters") + print("Player", victor.id + 1, "won the battle and gained 6 VPs!") + if victor.id == 0: + print("Player 1 VPs:", self.player1.victory_points - 6, "-->", self.player1.victory_points) + print("Player 2 VPs:", self.player2.victory_points) + else: + print("Player 2 VPs:", self.player2.victory_points - 6, "-->", self.player2.victory_points) + print("Player 1 VPs:", self.player1.victory_points) + # check for game over + if victor.victory_points >= 12: + game_over = True + if self.show_state: + print("Game Over!") + print("Player", victor.id + 1, "has won the game!") + + # Normalize the scores + total_victory_points = self.player1.victory_points + self.player2.victory_points + normalized_score = (float(self.player1.victory_points / total_victory_points), float(self.player2.victory_points / total_victory_points)) + if self.show_state: + print("Player 1 VPs:", self.player1.victory_points) + print("Player 2 VPs:", self.player2.victory_points) + print("Player 1 normalized score:", normalized_score[0]) + print("Player 2 normalized score:", normalized_score[1]) + # return scores on game end + return normalized_score diff --git a/games/air_land_sea/player.py b/games/air_land_sea/player.py new file mode 100644 index 0000000..38a98e4 --- /dev/null +++ b/games/air_land_sea/player.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass, field +from typing import List +from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules +from games.air_land_sea.cards import Card +from games.air_land_sea.board import Theater + +@dataclass +class Player: + id: int # either 0 or 1 + supreme_commander: int # either 0 or 1 + agent: Agent = None + hand: List[Card] = field(default_factory=list) + victory_points: int = 0 + + def play(self, card: Card, faceup: bool, theater: Theater, show_state: bool): + self.hand.remove(card) + if not faceup: + card.flip() + theater.player_cards[self.id].append(card) + if show_state: + if faceup: + print("Player", self.id + 1, "played", card, "to", theater.name, "faceup.") + else: + print("Player", self.id + 1, "played", card, "to", theater.name, "facedown.") + + # i have theater type and card name + def search_hand(self, card_name: str, theater_name: str) -> Card: + for card in self.hand: + if card.name == card_name and card.theater == theater_name: + return card + return None \ No newline at end of file diff --git a/games/air_land_sea/test.py b/games/air_land_sea/test.py new file mode 100644 index 0000000..270e3dd --- /dev/null +++ b/games/air_land_sea/test.py @@ -0,0 +1,169 @@ +from games.air_land_sea.game import AirLandSea +from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules +from agents.human_agent import HumanAgent +import api.util as util +from games.air_land_sea.cards import Card, Deck +import pprint + +def test(): + agent_1_path = "agents.human_agent.HumanAgent" + agent_2_path = "agents.human_agent.HumanAgent" + agent_1_class = util.import_class(agent_1_path) + agent_2_class = util.import_class(agent_2_path) + game = AirLandSea(show_state=True) + print("show state:", game.show_state) + print(game.show_state) + game.init_game(agent_1_class, agent_2_class) + agent_1 = game.agents[0] + agent_2 = game.agents[1] + # print("Game id:",game.id) + + # constant testing environment + game.player1.hand = [Card('Redeploy', 'Sea', 4, 'Instant', 'You may return 1 of your facedown cards to your hand. If you do, play a card'), + # Card('Maneuver', 'Sea', 3, 'Instant', 'Flip an uncovered card in an adjacent theater'), + Card('Aerodrome', 'Air', 4, 'Ongoing', 'You may play cards of strength 3 or less to non-matching theaters'), + Card('Cover Fire', 'Land', 4, 'Ongoing', 'All cards covered by this card are now strength 4'), + # Card('Containment', 'Air', 5, 'Ongoing', 'If any player plays a facedown card, destroy that card'), + Card('Reinforce', 'Land', 1, 'Instant', 'Draw 1 card and play it facedown to an adjacent theater'), + Card("Heavy Bombers", "Air", 6), + # Card('Disrupt', 'Land', 5, 'Ongoing', 'Starting with you, both players choose and flip 1 of their uncovered cards'), + Card('Escalation', 'Sea', 2, 'Ongoing', 'All your facedown cards are now strength 4'), + Card('Transport', 'Sea', 1, 'Instant', 'You may move 1 of your cards to a different theater')] + + game.player2.hand = [Card('Ambush', 'Land', 2, 'Instant', 'Flip any uncovered card'), + Card('Maneuver', 'Sea', 3, 'Instant', 'Flip an uncovered card in an adjacent theater'), + Card('Blockade', 'Sea', 5, 'Ongoing', 'If any player plays a card to an adjacent theater occupied by at least 3 other cards, destroy that card'), + Card('Air Drop', 'Air', 2, 'Instant', 'The next time you play a card, you may play it to a non-matching theater'), + Card('Reinforce', 'Land', 1, 'Instant', 'Draw 1 card and play it facedown to an adjacent theater'), + Card('Support', 'Air', 1, 'Ongoing', 'You gain +3 strength in each adjacent theater'),] + + game.player1.play(game.player1.hand[0], False, game.board.theaters[1], self.show_state) + game.player1.play(game.player1.hand[3], True, game.board.theaters[2], self.show_state) + game.player1.play(game.player1.hand[3], True, game.board.theaters[2], self.show_state) + game.player2.play(game.player2.hand[2], False, game.board.theaters[1], self.show_state) + game.player2.play(game.player2.hand[0], False, game.board.theaters[0], self.show_state) + game.player2.play(game.player2.hand[0], False, game.board.theaters[2], self.show_state) + # after a card is played faceup or flipped faceup its tactical ability takes effect immediately + # aka the effect manager is called + # the effect manager is also called when a card is flipped facedown too (to get rid of it) + + # Testing Specific Cards + target = Card('Maneuver', 'Sea', 3, 'Instant', 'Flip an uncovered card in an adjacent theater') + # target1 = Card('Aerodrome', 'Air', 4, 'Ongoing', 'You may play cards of strength 3 or less to non-matching theaters') + # target2 = Card('Redeploy', 'Sea', 4, 'Instant', 'You may return 1 of your facedown cards to your hand. If you do, play a card') + # game.player1.hand.append(target1) + # game.player2.hand.append(target2) + # game.player1.hand.append(target) + # print("Player 1 hand") + # print(game.player1.hand) + # play to second theater + # print("playing to second theater for p1") + # game.player1.play(target1, False, game.board.theaters[1], self.show_state) + # print("Player 1 hand after play") + # print(game.player1.hand) + # game.effect_manager.add_effect(target1, game.player1.id) + # play to third theater for p2 + # print("playing to third theater for p2") + # game.player2.play(target2, False, game.board.theaters[2], self.show_state) + # game.effect_manager.add_effect(target2, game.player2.id) + # print(game.board.get_board_string(game.player1.id)) + # print("Effect cards") + # print(game.effect_manager.effect_cards) + # print(game.board.search_ongoing_effect_location(support, game.effect_manager)) + + # print("testing get_adjacent_theaters") + # print(game.board.get_adjacent_theaters(1)) + + # print("testing get_theater_strengths") + # print(game.board.get_theater_strengths(game.effect_manager)) + + # analyzing the bug: + """ + player 1 plays redeploy to return air drop to their hand + player 1 then plays maneuver faceup to land + player 1 flips redeploy facedown + """ + + current_agent_turn = agent_1 + while True: + current_player = game.get_player_by_agent_id(current_agent_turn.agent_id) + print("current agent turn") + print(current_agent_turn.agent_id) + print("effects in play") + print(game.effect_manager.effect_cards) + # testing get observation + observation, available_actions = game.get_observation(current_agent_turn) + # observation, available_actions = game.get_observation(agent_2) + print("\n------------------------\n") + print("get observation") + pprint.pprint(available_actions.predefined) + print(observation.text) + + # testing modify_available_actions + # print("testing modify_available_actions") + # # pprint.pprint(available_actions.predefined) + print("calling modify available actions on:", current_player.id) + modified_actions = game.effect_manager.modify_available_actions(available_actions, current_player.hand, current_player.id) + # print("modified actions") + # pprint.pprint(modified_actions.predefined) + + # testing find_card_from_action + # action = Action(action_id="0") + # card = game.find_card_from_action(action, modified_actions) + # print("card from action") + # print(card) + + # Pick an action + # action = Action(action_id="5") + action = current_agent_turn.take_action(game.rules, observation, modified_actions, True) + game.update(action, modified_actions, current_agent_turn) + if game.player1.hand == [] and game.player2.hand == []: + break + # switch players + if current_agent_turn == agent_1: + print("changing turn from p1 to p2") + current_agent_turn = agent_2 + else: + print("changing turn from p2 to p1") + current_agent_turn = agent_1 + + + # testing check destroy trigger + # print("testing check destroy trigger") + # print(game.check_destroy_triggers(action, available_actions)) + + # testing find faceup or facedown status + # print("testing find faceup or facedown status") + # print(game.find_faceup_or_facedown_from_action(action, available_actions)) + + # test play_card_from_action + # print("testing play_card_from_action") + # played_card, played_to_theater = game.play_card_from_action(action, available_actions, agent_1) + # game.effect_manager.add_effect(played_card, agent_1.agent_id) + # print("observation of next player") + # next_observation, next_available_actions = game.get_observation(agent_2) + # # pprint.pprint(next_available_actions.predefined) + # print(next_observation.text) + + # Testing resolve effect + # print("testing resolve effect") + # game.resolve_effect(played_card, agent_1, played_to_theater) + + print(game.board.get_board_string(agent_1.agent_id)) + + # predefined action output: + # Action object with "action_id" == "guess_18" + # openended action output: + # Action object with action_id == "submit_clue" + # and openended_response == "conflict,2" # string + # need to reference available actions using action_id to see if the action was to play a facedown card + + print("Game over") + print(game.board.get_board_string(3)) + print(game.board.get_theater_strengths(game.effect_manager)) + + print("Test complete") + + +if __name__ == "__main__": + test() \ No newline at end of file diff --git a/games/arctic_scavengers/arctic_scavengers.py b/games/arctic_scavengers/arctic_scavengers.py index f7bd2d0..9b4d159 100644 --- a/games/arctic_scavengers/arctic_scavengers.py +++ b/games/arctic_scavengers/arctic_scavengers.py @@ -4,16 +4,16 @@ from games.arctic_scavengers.cards.game_cards import * import random import ast - + @dataclass class ArcticScavengers(Game): class Player: def __init__(self, agent): self.agent = agent self.cards = {"deck":[], "draw":[]} - self.actions = {"DIG": 0, "DRAW": 0, "HUNT": 0, "HIRE": 0, "TRASH":0, "SNIPER":0, "SABOTEUR":0} + self.actions = {"DIG": 0, "DRAW": 0, "HUNT": 0, "HIRE": 0, "TRASH":0, "SNIPER":0, "SABOTEUR":0} self.food = 0 - + def create_deck(self, deck): self.cards["deck"] = [Refugee()] * 4 + [Scavenger()] * 3 + [Brawler()] + [Spear()] + [Shovel()] random.shuffle(self.cards["deck"]) @@ -39,7 +39,7 @@ def calculate_fight_score(self): if action == "FIGHT": score += card.actions[action].value return score - + def calculate_people(self): score = 0 for card in self.cards["draw"]: @@ -78,7 +78,7 @@ def init_game(self, agent1 : Agent, agent2 : Agent): for player in self.players: player.create_deck(self.deck) self.game_winner = None - + def observation_resource_gather(self, player : Player) -> Tuple[Observation, AvailableActions]: context = "This is your draw hand, and the information on your cards." for card in player.cards["draw"]: @@ -95,7 +95,7 @@ def observation_resource_gather(self, player : Player) -> Tuple[Observation, Ava context += "\n" + str(pile[0]) context += "\nThe HUNT action allows you to generate food during a single round, that can be used as currency for hiring a single mercenary card." context += "\nAny card you play of type MODIFIER must be combined with a STANDARD card. The MODIFIER card will add to the score of the STANDARD card." - observation = Observation(text=context) + observation = Observation(text=context) s = "Choose cards to use and discard in an action, otherwise say STOP and your current hand will remain for the skirmish. Remember that each of the actions DIG, DRAW, HIRE, HUNT, TRASH can only be used once." # s += "\nThe DIG action allows you to draw one or more cards from the top of the junkyard pile, determined by the sum of DIG values you play. You may choose a maximum of one card to place in your reserve deck, and return any other cards to the bottom of the junkyard pile." # s += "\nThe DRAW action allows you to draw one or more cards from your reserve deck, adding them to your playing hand. The number is determined by the sum of the DRAW values you play." @@ -108,7 +108,7 @@ def observation_resource_gather(self, player : Player) -> Tuple[Observation, Ava s += "\nFor example, [\"HIRE\", [\"Pills\"], \"Saboteur\"]." s += "\nFor STOP, return [\"STOP\", []]" available_actions = AvailableActions( - instructions = s, + instructions = s, openended = { "DIG": "Draw one or more cards from the top of the junkyard pile, determined by the sum of DIG values you play. You may choose a maximum of one card to place in your reserve deck, and return any other cards to the bottom of the junkyard pile.", "DRAW": "Draw one or more cards from your reserve deck, adding them to your playing hand. The number is determined by the sum of the DRAW values you play.", @@ -124,7 +124,7 @@ def observation_resource_gather(self, player : Player) -> Tuple[Observation, Ava def observation_skirmish(self, player : Player, other : int) -> Tuple[Observation, AvailableActions]: context = "This is the hand you have brought to the skirmish. Your opponent can see your hand." for card in player.cards["draw"]: - context += "\n" + str(card) + context += "\n" + str(card) context += "\n This is the hand your opponent has brought to the skirmish. You can see their hand." for card in self.players[other].cards["draw"]: context += "\n" + str(card) @@ -136,7 +136,7 @@ def observation_skirmish(self, player : Player, other : int) -> Tuple[Observatio s += "\nFor the actions SNIPER or SABOTEUR, return as an openended response a list of the action name and the card title of your opponent that you are performing this action on. Both list values must be strings." s += "\nFor the action STOP, return [\"STOP\", ""]" available_actions = AvailableActions( - instructions = s, + instructions = s, openended = { "SNIPER": "Snipe one tribe member, forcing it to be discarded.", "SABOTEUR": "Disarm one tool card, forcing it to be discarded.", @@ -145,7 +145,7 @@ def observation_skirmish(self, player : Player, other : int) -> Tuple[Observatio predefined = {} ) return observation, available_actions - + def observation_respond_to_action(self, player : Player, action : Action) -> Tuple[Observation, AvailableActions]: context = "Your opponent has announced that they are taking the following action." context += "\n" + str(action.action_id) + " " + str(action.openended_response) @@ -157,7 +157,7 @@ def observation_respond_to_action(self, player : Player, action : Action) -> Tup s += "\nOtherwise, for the actions SNIPER or SABOTEUR, return as an openended response a list of the action name and the card title of your opponent that you are performing this action on. You must ensure that the list value is a string." s += "\nFor example, [\"Brawler\"]." available_actions = AvailableActions( - instructions = s, + instructions = s, openended = { "SNIPER": "Snipe one tribe member, forcing it to be discarded.", "SABOTEUR": "Disarm one tool card, forcing it to be discarded.", @@ -166,7 +166,7 @@ def observation_respond_to_action(self, player : Player, action : Action) -> Tup predefined = {} ) return observation, available_actions - + def observation_dig_cards(self, dig_cards : List[Card], player : Player) -> Tuple[Observation, AvailableActions]: context = "These are the cards you have drawn from the junkyard pile after taking the DIG action." for card in dig_cards: @@ -174,7 +174,7 @@ def observation_dig_cards(self, dig_cards : List[Card], player : Player) -> Tupl observation = Observation(text=context) s = "Choose a card to place in your reserve deck, and return the rest to the bottom of the junkyard pile." available_actions = AvailableActions( - instructions = s, + instructions = s, predefined = { f"{card.title}" : None for card in dig_cards }, @@ -186,7 +186,7 @@ def update_resource_gather(self, action : Action, available_actions : Available if self.show_state: print(action.action_id) print(action.openended_response) - if action2: + if action2: print(action2.action_id) print(repr(action2.openended_response)) print("Player 1 cards: " + str(player.cards)) @@ -240,12 +240,15 @@ def update_resource_gather(self, action : Action, available_actions : Available else: try: action_items = list(ast.literal_eval(str(action.openended_response))) + if len(action_items) == 0: + raise ValueError except: - action_items = [[random.choice(player.cards["draw"])]] - types = [a for a in action_items[0][0].actions.keys()] + choice = random.choice(player.cards["draw"]) + action_items = [[choice]] + types = [a for a in choice.actions.keys()] if choice.actions else [] action_items.insert(0, random.choice(types + ["TRASH"])) # No random hiring or stopping - + valid = True player_cards = [c.title for c in player.cards["draw"]] if id in ["DIG", "DRAW", "HUNT"]: @@ -291,7 +294,7 @@ def update_resource_gather(self, action : Action, available_actions : Available break for card in player.cards["draw"]: if card.title == card_name: - if "MEDICINE" in card.actions: + if card.actions != None and "MEDICINE" in card.actions: med_currency += card.actions["MEDICINE"].value if food_cost > player.food or med_cost > med_currency: valid = False @@ -301,7 +304,7 @@ def update_resource_gather(self, action : Action, available_actions : Available mercenary_names.append(m[0].title) if action_items[2] not in mercenary_names: valid = False - if player.actions[id] > 0: # If action has already been taken + if id not in player.actions or player.actions[id] > 0: # If action has already been taken valid = False if self.show_state: print("Action already taken") if self.show_state: print(valid) @@ -332,7 +335,7 @@ def update_resource_gather(self, action : Action, available_actions : Available if mercenary and mercenary[0].title == action_items[2]: action_items[-1] = mercenary[0] break - + if id == "DRAW": draw_value = 0 for card in action_items[-1]: @@ -482,7 +485,7 @@ def play(self) -> Tuple[float, float]: if self.show_state: print("Size of contested resources deck: " + str(len(self.deck.contested_resources))) initiator = count % 2 - #### DRAWING PHASE #### + #### DRAWING PHASE #### for player in self.players: player.reset_actions() player.draw_hand() @@ -492,7 +495,7 @@ def play(self) -> Tuple[float, float]: self.play_resource_gather(player, 1-initiator) player = self.players[1 - initiator] self.play_resource_gather(player, initiator) - + #### SKIRMISH PHASE #### player = self.players[initiator] self.play_skirmish(player, 1-initiator) @@ -511,6 +514,6 @@ def play(self) -> Tuple[float, float]: self.players[1].cards["deck"].append(self.deck.contested_resources.pop(0)) else: self.deck.junkyard.append(self.deck.contested_resources.pop(0)) - + count += 1 return (1, 0) if self.players[0].calculate_people() > self.players[1].calculate_people() else (0, 1) \ No newline at end of file diff --git a/games/chain_reaction/chain_reaction.py b/games/chain_reaction/chain_reaction.py deleted file mode 100644 index 2683390..0000000 --- a/games/chain_reaction/chain_reaction.py +++ /dev/null @@ -1,165 +0,0 @@ -from dataclasses import dataclass, field -import random -from abc import abstractmethod -from typing import List, Dict, Optional, Tuple -from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules -import ast - -@dataclass -class ChainReaction(Game): - rules : Rules = Rules( - title="Chain Reaction", - summary="Trigger a cascade of reactions by placing orbs strategically on the grid to eliminate opponents orbs and take over the board.", - additional_details = ["The gameplay takes place in an MxN board. The size of the board used in this implementation is 11x6.", - "For each cell in the board, we define a critical mass. The critical mass is equal to the number of orthogonally adjacent cells. That would be 4 for interior cells, 3 for edge cells, and 2 for cells in the corners.", - "All cells are initially empty. The Red and the Green player take turns to place orbs of their corresponding colors. The Red/Green player can only place an (red/green) orb in an empty cell or a cell which already contains one or more red/green orbs. When two or more orbs are placed in the same cell, they stack up.", - "The state of each cell is given by a two digit number, where the first digit is the occupying player ID and the second digit is the number of orbs in the cell. If the state is '--' the cell is empty.", - "When a cell is loaded with a number of orbs equal to its critical mass, the stack immediately explodes. As a result of the explosion, to each of the orthogonally adjacent cells, an orb is added and the initial cell looses as many orbs as its critical mass.", - "The explosions might result in overloading of an adjacent cell and the chain reaction of explosion continues until every cell is stable.", - "When a red cell explodes and there are green cells around, the green cells are converted to red and the other rules of explosions still follow. The same rule is applicable for other colors.", - "The winner is the one who eliminates all other player's orbs." - ] - ) - id : str = "chain_reaction" - - ## At every location we have a 2 digit identifier '--' or 'AgentOrb' where 'Agent' is the id corresponding to the agent and 'Orbs' is the number of orbs - def init_game(self, agent1 : Agent, agent2 : Agent): - - ## default board is 11x6 - self.states = [{ - "board" : [ - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'], - ['--','--','--','--','--','--'] - ], - }] - self.agents = [agent1(team_id = 0, agent_id = 0), agent2(team_id = 1, agent_id = 1)] - - ## added to make sure both players are given a turn before checking for winning or losing - self.turncount_player0 = 0 - self.turncount_player1 = 0 - - self.orbcount_player0 = 0 - self.orbcount_player1 = 0 - - ## Replaced X with red and O with green - ## Replaced marker with orb - self.agent_data = { - 0: {"orb": "red"}, - 1: {"orb": "green"} - } - - self.winning_team = None - - if self.show_state: - print(f"Agent {self.agents[0].agent_type_id} is red and agent {self.agents[1].agent_type_id} is green") - - def get_board_string(self): - board = self.states[-1]["board"] - row_strings = [", ".join(row) for row in board] - board_string = "\n".join(row_strings) - return board_string - - def get_observation(self, agent : Agent) -> Tuple[Observation, AvailableActions]: - board_string = self.get_board_string() - observation = Observation(text=board_string) - - ## Initialize number of rows and columns being used - rows = len(board) ## default is 11 - cols = len(board[0]) ## default is 6 - - agent_id = agent.agent_id - orb = self.agent_data[agent.agent_id]["orb"] - - available_actions = [] - for r in range(rows): - for c in range(cols): - cell_state = board[r][c] - # Check if the cell is empty or has orbs of the player's color - if cell_state == '--' or cell_state[0] == str(agent_id): - available_actions.append((r,c)) - - return observation, available_actions - - def update(self, action : Action, available_actions : AvailableActions, agent : Agent): - action = action.action_id - turncount_player0 = self.turncount_player0 - turncount_player1 = self.turncount_player1 - - orb = self.agent_data[agent.agent_id]["orb"] - board = self.states[-1]["board"] - - ## Initialize number of rows and columns being used - rows = len(board) ## default is 11 - cols = len(board[0]) ## default is 6 - - # Select a random action if no valid actions are provided - if action not in available_actions.predefined: - action = random.choice(list(available_actions.keys())) - - ## HOLD PLACE!!! Make edit to board update logic - # x, y = ast.literal_eval(action) - # board[x][y][0] = orb - - # Show the board - if self.show_state: - print("") - print(self.get_board_string()) - print("") - - # Check if player has won the game by having orbs of only their color on the board - if (turncount_player1*turncount_player2)>0: - player_won = False - - unique_colors = set() - - for r in range(rows): - for c in range(cols): - cell_state = board[r][c] - - if cell_state != '--': - unique_colors.add(cell_state[0]) - - if len(unique_colors) == 1: - player_won = True - - if player_won: - self.winning_team = agent.team_id - self.game_is_over = True - if self.show_state: - print(f"Game over: {orb} won") - - - def play(self): - player_1 = self.agents[0] - player_2 = self.agents[1] - - while True: - - # Player 1 moves - observation, available_actions = self.get_observation(player_1) - action = player_1.take_action(self.rules, observation, available_actions, show_state=self.show_state) - self.turncount_player1 += 1 - self.update(action, available_actions, player_1) - - if self.game_is_over: - break - - # Player 2 moves - observation, available_actions = self.get_observation(player_2) - action = player_2.take_action(self.rules, observation, available_actions, show_state=self.show_state) - self.turncount_player2 += 1 - self.update(action, available_actions, player_2) - - if self.game_is_over: - break - - return (float(self.winning_team == 0), float(self.winning_team == 1)) diff --git a/games/chain_reaction/function-trials-chain-reaction.ipynb b/games/chain_reaction/function-trials-chain-reaction.ipynb deleted file mode 100644 index 3753495..0000000 --- a/games/chain_reaction/function-trials-chain-reaction.ipynb +++ /dev/null @@ -1,912 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "dfeaa549", - "metadata": {}, - "source": [ - "#### Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "673e5bed", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "id": "34892df1", - "metadata": {}, - "source": [ - "#### Winning player check" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "282f9401", - "metadata": {}, - "outputs": [], - "source": [ - "def has_player_won(board_state):\n", - " \"\"\"\n", - " Check if any player has won the game by having orbs of only one color on the board.\n", - "\n", - " Parameters:\n", - " - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - "\n", - " Returns:\n", - " - bool: True if any player has won, False otherwise.\n", - " \"\"\"\n", - " rows = len(board_state)\n", - " cols = len(board_state[0])\n", - " unique_colors = set()\n", - "\n", - " for i in range(rows):\n", - " for j in range(cols):\n", - " cell_state = board_state[i][j]\n", - " if cell_state != '--':\n", - " unique_colors.add(cell_state[0])\n", - "\n", - " return len(unique_colors) == 1" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "61699c98", - "metadata": {}, - "outputs": [], - "source": [ - "current_board_state = [\n", - " ['11', '--', '--'],\n", - " ['--', '11', '--'],\n", - " ['--', '01', '01']\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "9c3746aa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The game is still ongoing.\n" - ] - } - ], - "source": [ - "if has_player_won(current_board_state):\n", - " print(\"Some player has won the game!\")\n", - "else:\n", - " print(\"The game is still ongoing.\")" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "1fb6148a", - "metadata": {}, - "outputs": [], - "source": [ - "bb = [['11','12','--','--','--','--'],\n", - " ['12','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','--'],\n", - " ['--','--','--','--','--','02'],\n", - " ['--','--','--','--','02','01']]" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "c7dae086", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2'" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb[0][1][1]" - ] - }, - { - "cell_type": "markdown", - "id": "846d7c8d", - "metadata": {}, - "source": [ - "#### Available actions" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "a7d37090", - "metadata": {}, - "outputs": [], - "source": [ - "def get_available_actions(board_state, player_id):\n", - " \"\"\"\n", - " Get the list of available actions in a Chain Reaction game.\n", - "\n", - " Parameters:\n", - " - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - " - player_id (str): The player identifier making the move.\n", - "\n", - " Returns:\n", - " - list of tuples: List of available actions, where each action is represented as a tuple (row, column).\n", - " \"\"\"\n", - " rows = len(board_state)\n", - " cols = len(board_state[0])\n", - " available_actions = []\n", - "\n", - " for r in range(rows):\n", - " for c in range(cols):\n", - " cell_state = board_state[r][c]\n", - " # Check if the cell is empty or has orbs of the player's color\n", - " if cell_state == '--' or cell_state[0] == str(player_id):\n", - " available_actions.append((r,c))\n", - " \n", - " return available_actions" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "9b879db4", - "metadata": {}, - "outputs": [], - "source": [ - "# player_id = '1' # Assume player 1's identifier is '1'" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "acba66a3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available Actions player 0: [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1), (2, 2)]\n", - "----------------------------------------\n", - "Available Actions player 1: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0)]\n" - ] - } - ], - "source": [ - "actions = get_available_actions(current_board_state, 0)\n", - "print(\"Available Actions player 0:\", actions)\n", - "print('----------------------------------------')\n", - "actions = get_available_actions(current_board_state, 1)\n", - "print(\"Available Actions player 1:\", actions)" - ] - }, - { - "cell_type": "markdown", - "id": "4b9f93a7", - "metadata": {}, - "source": [ - "#### Board update" - ] - }, - { - "cell_type": "code", - "execution_count": 183, - "id": "4708615f", - "metadata": {}, - "outputs": [], - "source": [ - "# def update_board(board_state, action, player_id):\n", - "# \"\"\"\n", - "# Update the Chain Reaction board based on a given action, including handling chain reactions.\n", - "\n", - "# Parameters:\n", - "# - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - "# - action (tuple): The action to be performed, represented as a tuple (row, column).\n", - "# - player_id (str): The player identifier making the move.\n", - "\n", - "# Returns:\n", - "# - list of lists: Updated board state after the action and chain reactions.\n", - "# \"\"\"\n", - "# def get_critical_mass(row, col):\n", - "# \"\"\"\n", - "# Get the critical mass for a given cell.\n", - "\n", - "# Parameters:\n", - "# - row (int): Row index of the cell.\n", - "# - col (int): Column index of the cell.\n", - "\n", - "# Returns:\n", - "# - int: Critical mass for the cell.\n", - "# \"\"\"\n", - "# if row == 0 or row == rows - 1:\n", - "# if col == 0 or col == cols - 1:\n", - "# return 2 # Corner cell\n", - "# return 3 # Edge cell\n", - "# if col == 0 or col == cols - 1:\n", - "# return 3 # Edge cell\n", - "# return 4 # Interior cell\n", - "\n", - "# rows = len(board_state)\n", - "# cols = len(board_state[0])\n", - "# updated_board = [row.copy() for row in board_state]\n", - "# row, col = action\n", - "\n", - "# # Check if the cell is empty or has orbs of the player's color\n", - "# if updated_board[row][col] == '--' or updated_board[row][col][0] == str(player_id):\n", - "# # Add an orb to the cell\n", - "# if updated_board[row][col] == '--':\n", - "# updated_board[row][col] = str(player_id) + '1'\n", - "# else:\n", - "# # If the cell already has orbs of the player's color, increment the number of orbs\n", - "# num_orbs = int(updated_board[row][col][1])\n", - "# updated_board[row][col] = str(player_id) + str(num_orbs + 1)\n", - "\n", - "# # Check for critical mass and trigger chain reaction if reached\n", - "# critical_mass = get_critical_mass(row, col)\n", - "# if int(updated_board[row][col][1]) >= critical_mass:\n", - "# updated_board = handle_chain_reaction(updated_board, row, col, player_id)\n", - "\n", - "# return updated_board\n", - "\n", - "# def handle_chain_reaction(board_state, row, col, player_id):\n", - "# \"\"\"\n", - "# Handle chain reaction for a given cell.\n", - "\n", - "# Parameters:\n", - "# - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - "# - row (int): Row index of the cell.\n", - "# - col (int): Column index of the cell.\n", - "# - player_id (str): The player identifier making the move.\n", - "\n", - "# Returns:\n", - "# - list of lists: Updated board state after the chain reaction.\n", - "# \"\"\"\n", - " \n", - "# def get_critical_mass(row, col):\n", - "# \"\"\"\n", - "# Get the critical mass for a given cell.\n", - "\n", - "# Parameters:\n", - "# - row (int): Row index of the cell.\n", - "# - col (int): Column index of the cell.\n", - "\n", - "# Returns:\n", - "# - int: Critical mass for the cell.\n", - "# \"\"\"\n", - "# if row == 0 or row == rows - 1:\n", - "# if col == 0 or col == cols - 1:\n", - "# return 2 # Corner cell\n", - "# return 3 # Edge cell\n", - "# if col == 0 or col == cols - 1:\n", - "# return 3 # Edge cell\n", - "# return 4 # Interior cell\n", - " \n", - "# rows = len(board_state)\n", - "# cols = len(board_state[0])\n", - "# updated_board = [row.copy() for row in board_state]\n", - "\n", - "# # Mark the current cell as empty\n", - "# updated_board[row][col] = '--'\n", - "\n", - "# # Get critical mass for the current cell\n", - "# critical_mass = get_critical_mass(row, col)\n", - "\n", - "# # Split and add one orb to every orthogonally adjacent cell\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols:\n", - "# if updated_board[new_row][new_col] == '--':\n", - "# updated_board[new_row][new_col] = str(player_id) + '1'\n", - "# else:\n", - "# num_orbs = int(updated_board[new_row][new_col][1])\n", - "# updated_board[new_row][new_col] = str(player_id) + str(num_orbs + 1)\n", - "\n", - "# # Recursively trigger chain reactions for adjacent cells\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols and updated_board[new_row][new_col][0] == str(player_id):\n", - "# if int(updated_board[new_row][new_col][1]) >= critical_mass:\n", - "# updated_board = handle_chain_reaction(updated_board, new_row, new_col, player_id)\n", - "\n", - "# return updated_board" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "ad9db328", - "metadata": {}, - "outputs": [], - "source": [ - "def update_board(board_state, action, player_id):\n", - " \n", - " def get_critical_mass(row, col):\n", - " \n", - " if row == 0 or row == rows - 1:\n", - " if col == 0 or col == cols - 1:\n", - " return 2 # Corner cell\n", - " return 3 # Edge cell\n", - " if col == 0 or col == cols - 1:\n", - " return 3 # Edge cell\n", - " return 4 # Interior cell\n", - "\n", - " rows = len(board_state)\n", - " cols = len(board_state[0])\n", - " updated_board = [row.copy() for row in board_state]\n", - " row, col = action\n", - "\n", - " # Check if the cell is empty or has orbs of the player's color\n", - " if updated_board[row][col] == '--' or updated_board[row][col][0] == str(player_id):\n", - " \n", - " # Add an orb to the cell\n", - " if updated_board[row][col] == '--':\n", - " updated_board[row][col] = str(player_id) + '1'\n", - " else:\n", - " # If the cell already has orbs of the player's color, increment the number of orbs\n", - " num_orbs = int(updated_board[row][col][1])\n", - " updated_board[row][col] = str(player_id) + str(num_orbs + 1)\n", - " \n", - " # Check for critical mass and trigger chain reaction if reached\n", - " critical_mass = get_critical_mass(row, col)\n", - " \n", - " if int(updated_board[row][col][1]) >= critical_mass:\n", - " updated_board = handle_chain_reaction(updated_board, row, col, player_id)\n", - "\n", - " return updated_board" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "56335e41", - "metadata": {}, - "outputs": [], - "source": [ - "# def handle_chain_reaction(board_state, row, col, player_id):\n", - " \n", - "# def get_critical_mass(row, col):\n", - " \n", - "# if row == 0 or row == rows - 1:\n", - "# if col == 0 or col == cols - 1:\n", - "# return 2 # Corner cell\n", - "# return 3 # Edge cell\n", - "# if col == 0 or col == cols - 1:\n", - "# return 3 # Edge cell\n", - "# return 4 # Interior cell\n", - " \n", - "# rows = len(board_state)\n", - "# cols = len(board_state[0])\n", - "# updated_board = [row.copy() for row in board_state]\n", - " \n", - "# # Split and add one orb to every orthogonally adjacent cell\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols:\n", - "# if updated_board[new_row][new_col] == '--':\n", - "# updated_board[new_row][new_col] = str(player_id) + '1'\n", - "# else:\n", - "# num_orbs = int(updated_board[new_row][new_col][1])\n", - "# updated_board[new_row][new_col] = str(player_id) + str(num_orbs + 1)\n", - "\n", - "# # Mark the current cell as empty\n", - "# updated_board[row][col] = '--'\n", - " \n", - "\n", - "# # Get critical mass for the current cell\n", - "# critical_mass = get_critical_mass(row, col)\n", - "\n", - "# # Recursively trigger chain reactions for adjacent cells\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols and updated_board[new_row][new_col][0] == str(player_id):\n", - "# if int(updated_board[new_row][new_col][1]) >= critical_mass:\n", - "# updated_board = handle_chain_reaction(updated_board, new_row, new_col, player_id)\n", - "\n", - "# return updated_board" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bfad3b97", - "metadata": {}, - "outputs": [], - "source": [ - "# def handle_chain_reaction(board_state, row, col, player_id):\n", - "# \"\"\"\n", - "# Handle chain reaction for a given cell recursively.\n", - "\n", - "# Parameters:\n", - "# - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - "# - row (int): Row index of the cell.\n", - "# - col (int): Column index of the cell.\n", - "# - player_id (str): The player identifier making the move.\n", - "\n", - "# Returns:\n", - "# - list of lists: Updated board state after the chain reaction.\n", - "# - set of tuples: Set of newly updated cells, each represented as a tuple (row, col).\n", - "# \"\"\"\n", - "# def get_critical_mass(row, col):\n", - "# \"\"\"\n", - "# Get the critical mass for a given cell.\n", - "\n", - "# Parameters:\n", - "# - row (int): Row index of the cell.\n", - "# - col (int): Column index of the cell.\n", - "\n", - "# Returns:\n", - "# - int: Critical mass for the cell.\n", - "# \"\"\"\n", - "# if row == 0 or row == rows - 1:\n", - "# if col == 0 or col == cols - 1:\n", - "# return 2 # Corner cell\n", - "# return 3 # Edge cell\n", - "# if col == 0 or col == cols - 1:\n", - "# return 3 # Edge cell\n", - "# return 4 # Interior cell\n", - "\n", - "# rows = len(board_state)\n", - "# cols = len(board_state[0])\n", - "# updated_board = [row.copy() for row in board_state]\n", - "# newly_updated_cells = set()\n", - "\n", - "# # Split and add one orb to every orthogonally adjacent cell\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols:\n", - "# if updated_board[new_row][new_col] == '--':\n", - "# updated_board[new_row][new_col] = str(player_id) + '1'\n", - "# newly_updated_cells.add((new_row, new_col))\n", - "# else:\n", - "# num_orbs = int(updated_board[new_row][new_col][1])\n", - "# updated_board[new_row][new_col] = str(player_id) + str(num_orbs + 1)\n", - "# newly_updated_cells.add((new_row, new_col))\n", - "\n", - "# # Mark the current cell as empty\n", - "# updated_board[row][col] = '--'\n", - "\n", - "# # Get critical mass for the current cell\n", - "# critical_mass = get_critical_mass(row, col)\n", - "\n", - "# # Recursively trigger chain reactions for adjacent cells\n", - "# for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - "# new_row, new_col = row + dr, col + dc\n", - "# if 0 <= new_row < rows and 0 <= new_col < cols and updated_board[new_row][new_col][0] == str(player_id):\n", - "# if int(updated_board[new_row][new_col][1]) >= critical_mass:\n", - "# updated, newly_updated = handle_chain_reaction(updated_board, new_row, new_col, player_id)\n", - "# updated_board = updated\n", - "# newly_updated_cells.update(newly_updated)\n", - "\n", - "# return updated_board, newly_updated_cells\n", - "\n", - "\n", - "\n", - "# # Call the modified recursive function\n", - "# updated_board, newly_updated_cells = handle_chain_reaction(current_board_state, row, col, player_id)\n", - "\n", - "# # Print the updated board and newly updated cells\n", - "# print(\"Updated Board:\")\n", - "# for row in updated_board:\n", - "# print(row)\n", - "\n", - "# print(\"Newly Updated Cells:\", newly_updated_cells)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 95, - "id": "50151f4a", - "metadata": {}, - "outputs": [], - "source": [ - "def update_board(board_state, action, player_id):\n", - " \"\"\"\n", - " Update the Chain Reaction board based on a given action, including handling chain reactions.\n", - "\n", - " Parameters:\n", - " - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - " - action (tuple): The action to be performed, represented as a tuple (row, column).\n", - " - player_id (str): The player identifier making the move.\n", - "\n", - " Returns:\n", - " - list of lists: Updated board state after the action and chain reactions.\n", - " - set of tuples: Set of newly updated cells, each represented as a tuple (row, col).\n", - " - str or None: Error message if cells have orbs equal to or greater than their critical mass, None otherwise.\n", - " \"\"\"\n", - " def get_critical_mass(row, col):\n", - " \"\"\"\n", - " Get the critical mass for a given cell.\n", - "\n", - " Parameters:\n", - " - row (int): Row index of the cell.\n", - " - col (int): Column index of the cell.\n", - "\n", - " Returns:\n", - " - int: Critical mass for the cell.\n", - " \"\"\"\n", - " if row == 0 or row == rows - 1:\n", - " if col == 0 or col == cols - 1:\n", - " return 2 # Corner cell\n", - " return 3 # Edge cell\n", - " if col == 0 or col == cols - 1:\n", - " return 3 # Edge cell\n", - " return 4 # Interior cell\n", - "\n", - " rows = len(board_state)\n", - " cols = len(board_state[0])\n", - " updated_board = [row.copy() for row in board_state]\n", - " row, col = action\n", - " newly_updated_cells = set()\n", - "\n", - " # Check if the cell is empty or has orbs of the player's color\n", - " if updated_board[row][col] == '--' or updated_board[row][col][0] == player_id:\n", - " # Add an orb to the cell\n", - " if updated_board[row][col] == '--':\n", - " updated_board[row][col] = player_id + 'b'\n", - " newly_updated_cells.add((row, col))\n", - " else:\n", - " # If the cell already has orbs of the player's color, increment the number of orbs\n", - " num_orbs = int(updated_board[row][col][1])\n", - " updated_board[row][col] = player_id + str(num_orbs + 1)\n", - " newly_updated_cells.add((row, col))\n", - "\n", - " # Check for critical mass and trigger chain reaction if reached\n", - " critical_mass = get_critical_mass(row, col)\n", - " if int(updated_board[row][col][1]) >= critical_mass:\n", - " _, newly_updated = handle_chain_reaction(updated_board, row, col, player_id)\n", - " newly_updated_cells.update(newly_updated)\n", - "\n", - " # Check for cells with orbs equal to or greater than their critical mass\n", - " for i in range(rows):\n", - " for j in range(cols):\n", - " if updated_board[i][j] != '--' and updated_board[i][j][0] == player_id:\n", - " cell_critical_mass = get_critical_mass(i, j)\n", - " if int(updated_board[i][j][1]) >= cell_critical_mass:\n", - " return updated_board, newly_updated_cells, f\"Error: Cell ({i}, {j}) has orbs equal to or greater than its critical mass.\"\n", - "\n", - " return updated_board, newly_updated_cells, None" - ] - }, - { - "cell_type": "code", - "execution_count": 96, - "id": "9284dc2c", - "metadata": {}, - "outputs": [], - "source": [ - "def handle_chain_reaction(board_state, row, col, player_id):\n", - " \"\"\"\n", - " Handle chain reaction for a given cell recursively.\n", - "\n", - " Parameters:\n", - " - board_state (list of lists): Represents the state of the board with player identifier and number of orbs.\n", - " - row (int): Row index of the cell.\n", - " - col (int): Column index of the cell.\n", - " - player_id (str): The player identifier making the move.\n", - "\n", - " Returns:\n", - " - list of lists: Updated board state after the chain reaction.\n", - " - set of tuples: Set of newly updated cells, each represented as a tuple (row, col).\n", - " \"\"\"\n", - " def get_critical_mass(row, col):\n", - " \"\"\"\n", - " Get the critical mass for a given cell.\n", - "\n", - " Parameters:\n", - " - row (int): Row index of the cell.\n", - " - col (int): Column index of the cell.\n", - "\n", - " Returns:\n", - " - int: Critical mass for the cell.\n", - " \"\"\"\n", - " if row == 0 or row == rows - 1:\n", - " if col == 0 or col == cols - 1:\n", - " return 2 # Corner cell\n", - " return 3 # Edge cell\n", - " if col == 0 or col == cols - 1:\n", - " return 3 # Edge cell\n", - " return 4 # Interior cell\n", - "\n", - " rows = len(board_state)\n", - " cols = len(board_state[0])\n", - " updated_board = [row.copy() for row in board_state]\n", - " newly_updated_cells = set()\n", - " \n", - " # Mark the current cell as empty\n", - " updated_board[row][col] = '--'\n", - "\n", - " # Split and add one orb to every orthogonally adjacent cell\n", - " for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:\n", - " new_row, new_col = row + dr, col + dc\n", - " \n", - " if 0 <= new_row < rows and 0 <= new_col < cols:\n", - " \n", - " if updated_board[new_row][new_col] == '--':\n", - " updated_board[new_row][new_col] = str(player_id) + '1'\n", - " newly_updated_cells.add((new_row, new_col))\n", - " else:\n", - " num_orbs = int(updated_board[new_row][new_col][1])\n", - " updated_board[new_row][new_col] = str(player_id) + str(num_orbs + 1)\n", - " newly_updated_cells.add((new_row, new_col))\n", - " \n", - " # Check for critical mass and trigger chain reaction if reached\n", - " critical_mass = get_critical_mass(new_row, new_col)\n", - " if int(updated_board[new_row][new_col][1]) >= critical_mass:\n", - " _, newly_updated = handle_chain_reaction(updated_board, new_row, new_col, player_id)\n", - " newly_updated_cells.update(newly_updated)\n", - "\n", - " return updated_board, newly_updated_cells" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "645249b9", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 97, - "id": "191588a1", - "metadata": {}, - "outputs": [], - "source": [ - "current_board_state = [\n", - " ['11', '--', '--'],\n", - " ['--', '11', '--'],\n", - " ['--', '01', '01']]" - ] - }, - { - "cell_type": "code", - "execution_count": 102, - "id": "f0973aa9", - "metadata": {}, - "outputs": [], - "source": [ - "player_id = '1' # Assume player 1's identifier is '1'\n", - "action = (0,0) # Example action to add an orb to the center cell" - ] - }, - { - "cell_type": "code", - "execution_count": 103, - "id": "ccd3bad2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Current Board:\n", - "['13', '--', '--']\n", - "['--', '11', '--']\n", - "['--', '01', '01']\n", - "-----------------\n", - "Error: Cell (0, 0) has orbs equal to or greater than its critical mass.\n", - "Updated Board:\n", - "['14', '--', '--']\n", - "['--', '11', '--']\n", - "['--', '01', '01']\n" - ] - } - ], - "source": [ - "print(\"Current Board:\")\n", - "for row in current_board_state:\n", - " print(row)\n", - "print('-----------------')\n", - "\n", - "updated_board, newly_updated_cells, error_message = update_board(current_board_state, action, player_id)\n", - "if error_message:\n", - " print(error_message)\n", - " \n", - "print(\"Updated Board:\")\n", - "for row in updated_board:\n", - " print(row)\n", - "\n", - "current_board_state = updated_board" - ] - }, - { - "cell_type": "markdown", - "id": "f1d51d73", - "metadata": {}, - "source": [ - "#### Text state to image" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "d703e1ea", - "metadata": {}, - "outputs": [], - "source": [ - "# def plot_grid(matrix):\n", - "# rows, cols = len(matrix), len(matrix[0])\n", - "\n", - "# # Create a new figure and axis\n", - "# fig, ax = plt.subplots()\n", - "\n", - "# # Set axis limits\n", - "# ax.set_xlim(0, cols)\n", - "# ax.set_ylim(0, rows)\n", - "\n", - "# # Draw grid lines\n", - "# for i in range(cols + 1):\n", - "# ax.axvline(i, color='black', linewidth=1)\n", - "\n", - "# for i in range(rows + 1):\n", - "# ax.axhline(i, color='black', linewidth=1)\n", - "\n", - "# # Set the size of balls or dots\n", - "# ball_size = 0.5\n", - "\n", - "# # Plot cells based on matrix state\n", - "# for i in range(rows):\n", - "# for j in range(cols):\n", - "# cell_state = matrix[i][j]\n", - "\n", - "# if cell_state != '--':\n", - "# player_id, num_balls = map(int, cell_state)\n", - "# color = 'red' if player_id == 0 else 'green'\n", - "\n", - "# # Plot fixed number of balls or dots in the cell\n", - "# for _ in range(num_balls):\n", - "# ax.scatter(j + 0.5, rows - i - 0.5, c=color, s=ball_size * 100, marker='o')\n", - "\n", - "# # Hide the axes\n", - "# ax.set_xticks([])\n", - "# ax.set_yticks([])\n", - "\n", - "# plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "6645a094", - "metadata": {}, - "outputs": [], - "source": [ - "def plot_grid(matrix):\n", - " rows, cols = len(matrix), len(matrix[0])\n", - "\n", - " # Create a new figure and axis\n", - " fig, ax = plt.subplots()\n", - "\n", - " # Set axis limits\n", - " ax.set_xlim(0, cols)\n", - " ax.set_ylim(0, rows)\n", - "\n", - " # Draw grid lines\n", - " for i in range(cols + 1):\n", - " ax.axvline(i, color='black', linewidth=1)\n", - "\n", - " for i in range(rows + 1):\n", - " ax.axhline(i, color='black', linewidth=1)\n", - "\n", - " # Set the size of balls or dots\n", - " ball_size = 0.5\n", - "\n", - " # Plot cells based on matrix state\n", - " for i in range(rows):\n", - " for j in range(cols):\n", - " cell_state = matrix[i][j]\n", - "\n", - " if cell_state != '--':\n", - " player_id, num_balls = map(int, cell_state)\n", - " color = 'red' if player_id == 0 else 'green'\n", - "\n", - " # Plot non-overlapping balls or dots in the cell\n", - " for k in range(num_balls):\n", - " offset = 0.2 # Adjust this value for spacing between dots\n", - " x_offset = (k % 2) * offset\n", - " y_offset = (k // 2) * offset\n", - " ax.scatter(j + 0.5 + x_offset, rows - i - 0.5 - y_offset, c=color, s=ball_size * 100, marker='o')\n", - "\n", - " # Hide the axes\n", - " ax.set_xticks([])\n", - " ax.set_yticks([])\n", - "\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "13a9d6aa", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgMAAAGFCAYAAABg2vAPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAARL0lEQVR4nO3dP2tbadrA4VuyQhAoSmuGmbRed0MyxS6y/QWmMFadZppg7GKJv8RU67CNHNJMk2YqY8zuF5DCLuwGpvN6yswScBmNQYTIR29xNplJ/MZ/Ekn+c18XGCGdB56HPEH66ZxjXBmNRqMAANKqXvQCAICLJQYAIDkxAADJiQEASE4MAEByYgAAkhMDAJBc7SyDiqKIly9fxq1bt6JSqUx6TQDAGIxGo/j111/jiy++iGr149//zxQDL1++jK+++mpsiwMApueXX36JL7/88qPHzxQDt27dioiIv/71r/H111+PZWFcXvv7+/HgwYN48uRJzM3NXfRymDD7nYv9zuWnn36KP//5z+8+xz/mTDHw9tLA119/HUtLS5+/Oi61RqMRERH37t2Lu3fvXvBqmDT7nYv9zum0S/xuIASA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIA3hoMIg4OykeARMQA9HoR7XZEoxExO1s+ttsRz55d9MoApkIMkNvWVsTSUsTubkRRlK8VRfl8cTHi8eOLXR/AFIgB8ur1ItbXI0ajiOHw/WPDYfn62pozBMC1JwbIa3MzYmbm5DEzMxGPHk1nPQAXRAyQ02AQsbNz/IzAh4bDiO1tNxUC15oYIKd+/7d7BE5TFOV4gGtKDJBTsxlRPeN//2q1HA9wTYkBcqrXI5aXI2q1k8fVahErK+V4gGtKDJDXxkbE0dHJY46OIh4+nM56AC6IGCCvhYWITieiUjl+hqBWK1/vdCJarYtZH8CUiAFyW12N6HbLSwZv7yGoVsvn3W55HOCaO+WCKSTQapU/g0H5WwPNpnsEgFTEALxVr4sAICWXCQAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJKrnWfw/v5+NBqNSa2FS2Jvb++9R643+52L/c5lf3//TOMqo9FodNqgfr8ft2/f/uxFAQDT9+rVq2g2mx89fq4zA0+ePIl79+599qK43Pb29uL+/fvx9OnTmJ+fv+jlMGH2Oxf7ncvz58/jwYMHp447VwzMzc3F3bt3P3lRXC3z8/P2OxH7nYv9zuHw8PBM49xACADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAHIaDCIODsrHqzwHjIEYAHLp9SLa7YhGI2J2tnxstyOePbtac8AYiQEgj62tiKWliN3diKIoXyuK8vniYsTjx1djDhgzMQDk0OtFrK9HjEYRw+H7x4bD8vW1tc/79j6NOWACxACQw+ZmxMzMyWNmZiIePbrcc8AEiAHg+hsMInZ2jn9b/9BwGLG9/Wk3/E1jDpgQMQBcf/3+b9fvT1MU5fjLOAdMiBgArr9mM6J6xre7arUcfxnngAkRA8D1V69HLC9H1Gonj6vVIlZWyvGXcQ6YEDEA5LCxEXF0dPKYo6OIhw8v9xwwAWIAyGFhIaLTiahUjn97r9XK1zudiFbrcs8BEyAGgDxWVyO63fJ0/tvr+9Vq+bzbLY9fhTlgzE65uAVwzbRa5c9gUN7R32yO//r9NOaAMRIDQE71+uQ/oKcxB4yBywQAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJ1c4zeH9/PxqNxqTWwiWxt7f33iPXm/3OxX7nsr+/f6ZxldFoNDptUL/fj9u3b3/2ogCA6Xv16lU0m82PHj/XmYEnT57EvXv3PntRXG57e3tx//79ePr0aczPz1/0cpgw+52L/c7l+fPn8eDBg1PHnSsG5ubm4u7du5+8KK6W+fl5+52I/c7FfudweHh4pnFuIASA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODMBbg0HEwUH5yLU3eDOIg8ODGLyZ3H5PYw4YBzEAvV5Eux3RaETMzpaP7XbEs2cXvTImoPeiF+0f29H4vhGzf5mNxveNaP/Yjmcvxrff05gDxkkMkNvWVsTSUsTubkRRlK8VRfl8cTHi8eOLXR9jtfWvrVj6YSl2f96NYlTudzEqYvfn3Vj8YTEe//vz93sac8C4iQHy6vUi1tcjRqOI4fD9Y8Nh+framjME10TvRS/W/74eoxjFsHh/v4fFMEYxirW/rX3Wt/dpzAGTIAbIa3MzYmbm5DEzMxGPHk1nPUzU5j82Y6Z68n7PVGfi0T8/fb+nMQdMghggp8EgYmfn+BmBDw2HEdvbbiq84gZvBrGzv3Ps2/qHhsUwtv+z/Uk3/E1jDpgUMUBO/f5v9wicpijK8VxZ/df9d9fvT1OMiui/Pv9+T2MOmBQxQE7NZkT1jP/9q9VyPFdW82YzqpWz7Xe1Uo3mzfPv9zTmgEkRA+RUr0csL0fUaiePq9UiVlbK8VxZ9Rv1WJ5bjlr15P2uVWux8oeVqN84/35PYw6YFDFAXhsbEUdHJ485Oop4+HA662GiNv60EUfFyft9VBzFwz9++n5PYw6YBDFAXgsLEZ1ORKVy/AxBrVa+3ulEtFoXsz7GauHOQnS+7UQlKse+vdeqtahEJTrfdqJ159P3expzwCSIAXJbXY3odstLBm/vIahWy+fdbnmca2P1m9XofteN5bnld9f3q5VqLM8tR/e7bqx+8/n7PY05YNxOuWAKCbRa5c9gUP7WQLPpHoFrrHWnFa07rRi8GUT/dT+aN5tjv34/jTlgnMQAvFWvi4BE6jfqE/+AnsYcMA4uEwBAcmIAAJITAwCQnBgAgOTEAAAkJwaAlAZvBnFweDDRvx44jTlgHMQAkErvRS/aP7aj8X0jZv8yG43vG9H+sR3PXjy7UnPAOIkBII2tf23F0g9Lsfvz7rs/N1yMitj9eTcWf1iMx/9+fCXmgHETA0AKvRe9WP/7eoxiFMNi+N6xYTGMUYxi7W9rn/XtfRpzwCSIASCFzX9sxkx15sQxM9WZePTPR5d6DpgEMQBce4M3g9jZ3zn2bf1Dw2IY2//Z/qQb/qYxB0yKGACuvf7r/rvr96cpRkX0X/cv5RwwKWIAuPaaN5vv/pzwaaqVajRvNi/lHDApYgC49uo36rE8txy16sl/qLVWrcXKH1Y+6S8NTmMOmBQxAKSw8aeNOCqOThxzVBzFwz8+vNRzwCSIASCFhTsL0fm2E5WoHPv2XqvWohKV6Hzbidad1qWeAyZBDABprH6zGt3vurE8t/zu+n61Uo3lueXofteN1W9Wr8QcMG4nX9wCuGZad1rRutOKwZtB9F/3o3mzOfbr99OYA8ZJDAAp1W/UJ/4BPY05YBxcJgCA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAILnaeQbv7+9Ho9GY1Fq4JPb29t575Hqz37nY71z29/fPNK4yGo1Gpw3q9/tx+/btz14UADB9r169imaz+dHj5zoz8OTJk7h3795nL4rLbW9vL+7fvx9Pnz6N+fn5i14OE2a/c7HfuTx//jwePHhw6rhzxcDc3FzcvXv3kxfF1TI/P2+/E7HfudjvHA4PD880zg2EAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYmBEwzeDOLg8CAGbwYXvRQAPsdgEHFwUD5yjBj4f/Re9KL9Yzsa3zdi9i+z0fi+Ee0f2/HsxbOLXhoA59HrRbTbEY1GxOxs+dhuRzzzfv57YuADW//aiqUflmL3590oRkVERBSjInZ/3o3FHxbj8b8fX/AKATiTra2IpaWI3d2Ionw/j6Iony8uRjz2fv6WGPid3oterP99PUYximExfO/YsBjGKEax9rc1ZwgALrteL2J9PWI0ihi+/34ew2H5+tqaMwT/IwZ+Z/MfmzFTnTlxzEx1Jh7989GUVgTAJ9ncjJg5+f08ZmYiHnk/jxAD7wzeDGJnf+fYGYEPDYthbP9n202FAJfVYBCxs3P8jMCHhsOI7W03FYYYeKf/uv/uHoHTFKMi+q/7E14RAJ+k3//tHoHTFEU5Pjkx8D/Nm82oVs72z1GtVKN5sznhFQHwSZrNiOoZP96q1XJ8cmLgf+o36rE8txy1au3EcbVqLVb+sBL1G/UprQyAc6nXI5aXI2onv59HrRaxslKOT04M/M7GnzbiqDg6ccxRcRQP//hwSisC4JNsbEQcnfx+HkdHEQ+9n0eIgfcs3FmIzredqETl2BmCWrUWlahE59tOtO60LmiFAJzJwkJEpxNRqRw/Q1Crla93OhEt7+cRYuCY1W9Wo/tdN5bnlt/dQ1CtVGN5bjm633Vj9ZvVC14hAGeyuhrR7ZaXDN7eQ1Ctls+73fI4ERFxygWVnFp3WtG604rBm0H0X/ejebPpHgGAq6jVKn8Gg/K3BppN9wj8P8TACeo36iIA4Dqo10XACVwmAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASE4MAEByYgAAkhMDAJCcGACA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACSEwMAkJwYAIDkxAAAJCcGACA5MQAAyYkBAEhODABAcmIAAJITAwCQnBgAgOTEAAAkJwYAIDkxAADJiQEASK52lkGj0SgiIn766adJroVLYn9/PyIinj9/HoeHhxe8GibNfudiv3N5+7n99nP8Yyqj00ZExH//+9/46quvxrIwAGC6fvnll/jyyy8/evxMMVAURbx8+TJu3boVlUplrAsEACZjNBrFr7/+Gl988UVUqx+/M+BMMQAAXF9uIASA5MQAACQnBgAgOTEAAMmJAQBITgwAQHJiAACS+z//TeAO9rRchAAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Example usage:\n", - "matrix_state = [\n", - " ['--', '01', '--', '10'],\n", - " ['--', '--', '02', '10'],\n", - " ['--', '01', '14', '--'],\n", - " ['11', '--', '--', '01']\n", - "]\n", - "\n", - "plot_grid(matrix_state)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f006f90a", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c4637c40", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/games/chain_reaction/readme.md b/games/chain_reaction/readme.md deleted file mode 100644 index 709e4ad..0000000 --- a/games/chain_reaction/readme.md +++ /dev/null @@ -1,16 +0,0 @@ -

Chain Reaction

- -

Game Summary

-The objective of the game is to take control of the board by eliminating your opponents' orbs. -Players take turns to place their orbs in a cell on the board. -When a cell has reached critical mass the orbs explode into the surrounding cells, adding an extra orb and claiming the cell for the player. -A player may only place their orbs in a blank cell or a cell that contains orbs of their own colour. -Once a player loses all their orbs they are out of the game. Last one with all orbs of their colour on the board wins. - -[Link to heuristic strategies](https://brilliant.org/wiki/chain-reaction-game/) - -

Implementation Specifics

- -- 2 player game (could be extended to more) -- Takes current state of the board as input in text form (could be modified to be read in as an image instead) -- Board dimensions: 11x6 diff --git a/games/chain_reaction/update_board.py b/games/chain_reaction/update_board.py deleted file mode 100644 index 46d1248..0000000 --- a/games/chain_reaction/update_board.py +++ /dev/null @@ -1,154 +0,0 @@ -## iterative Aish - -def update_board(board_state, action, player_id): - """Updates the board in a game of chain reaction, handling explosions, chain reactions, orb placement, and validity checks. - - Args: - board_state: A list of lists representing the current board state. - action: A tuple (row, col) indicating the cell where the player placed an orb. - player_id: The ID of the player who placed the orb. - - Returns: - The updated board state after handling explosions, chain reactions, and orb placement. - """ - - rows = len(board_state) - cols = len(board_state[0]) - - row, col = action - - available_actions = [] - - for r in range(rows): - for c in range(cols): - cell_state = board_state[r][c] - # Check if the cell is empty or has orbs of the player's color - if cell_state == '--' or cell_state[0] == str(player_id): - available_actions.append((r,c)) - - def get_critical_mass(row, col): - """ - Get the critical mass for a given cell. - - Parameters: - - row (int): Row index of the cell. - - col (int): Column index of the cell. - - Returns: - - int: Critical mass for the cell. - """ - if row == 0 or row == rows - 1: - if col == 0 or col == cols - 1: - return 2 # Corner cell - return 3 # Edge cell - if col == 0 or col == cols - 1: - return 3 # Edge cell - return 4 # Interior cell - - def explode(board_state, row, col): ## to be called only when the cell is over critical mass - """Handles the explosion of a cell and its surrounding cells iteratively, - checking only orthogonally neighboring cells for potential chain reactions - until none remain. - - Adds orbs to orthogonally adjacent cells, regardless of player ownership, - and tracks updated cells to avoid redundant processing. - """ - - player_id = board_state[row][col][0] - critical_mass = int(board_state[row][col][1]) - - rows = len(board_state) - cols = len(board_state[0]) - - # Make a list of neighnours to update - orthogonal_neighbours = [] - - if critical_mass == 2: - if row == 0: - orthogonal_neighbours.append((row+1,col)) - if col == 0: - orthogonal_neighbours.append((row,col+1)) - else: - orthogonal_neighbours.append((row,col-1)) - if row == row-1: - orthogonal_neighbours.append((row-1,col)) - if col == cols-1: - orthogonal_neighbours.append((row,col-1)) - else: - orthogonal_neighbours.append((row,col+1)) - - if critical_mass == 3: - if row == 0: - orthogonal_neighbours += [(row,col-1),(row+1,col),(row,col+1)] - elif row == rows-1: - orthogonal_neighbours += [(row,col-1),(row-1,col),(row,col+1)] - elif col == 0: - orthogonal_neighbours += [(row-1,col),(row,col+1),(row+1,col)] - else: - orthogonal_neighbours += [(row-1,col),(row,col-1),(row+1,col)] - - if critical_mass == 4: - orthogonal_neighbours += [(row-1,col),(row+1,col),(row,col-1),(row,col+1)] - - # Empty the exploded cell - print('Emptied cell:',row,col) - board_state[row][col] = "--" - - # Initialize list of cells that are unstable because of the explosion - unstable_cells = [] - - # Add 1 orb to neighbouring cells regardless of ownership - for cntr, (r,c) in enumerate(orthogonal_neighbours): - print('Updating cell:',r,c) - if board_state[r][c][1] == '-': - num_orbs = 1 - print('Just 1 orb') - print('Updated board') - else: - num_orbs = int(board_state[r][c][1]) + 1 - if num_orbs >= get_critical_mass(r, c): - unstable_cells.append((r, c)) - print('Unstable cells:',unstable_cells) - - # Update player ID and number of orbs - board_state[r][c] = f"{player_id}{num_orbs}" - print('Updated board!') - print('Unstable cells:', unstable_cells) - - print('-----------------') - for row in board_state: - print(row) - print('-----------------') - - return board_state, unstable_cells - - # Check for valid action - if not (0 <= row < len(board_state) and 0 <= col < len(board_state[0])): - action = random.choice(available_actions) - row, col = action - print("Action outside board boundaries, selecting random legal action:",action) - if board_state[row][col][0] != "-" and board_state[row][col][0] != player_id: - action = random.choice(available_actions) - row, col = action - print("Cannot place orb in opponent's cell, selecting random legal action:",action) - - if board_state[row][col][0] == player_id: # Player already has an orb in this cell - num_orbs = int(board_state[row][col][1]) + 1 - board_state[row][col] = f"{player_id}{num_orbs}" - else: # Place the first orb - board_state[row][col] = f"{player_id}1" - - critical_mass = get_critical_mass(row, col) - if int(board_state[row][col][1]) >= critical_mass: - print('Calling explosion on:',row,col) - board_state, unstable_cells = explode(board_state, row, col) # Trigger explosion and chain reactions - else: - unstable_cells = [] - - # Call the explode function on cells which are unstable - while len(unstable_cells)>0: - for cntru, (ru,cu) in enumerate(unstable_cells): - print('Calling recursive explosion on:',ru,cu) - board_state, unstable_cells = explode(board_state,ru,cu) - - return board_state diff --git a/games/codenames/card.py b/games/codenames/card.py index 59544de..2919f3e 100644 --- a/games/codenames/card.py +++ b/games/codenames/card.py @@ -8,7 +8,7 @@ class CardType(Enum): def __str__(self): return self.name - + class Card: def __init__(self, word: str, card_type: Enum): @@ -17,4 +17,4 @@ def __init__(self, word: str, card_type: Enum): def __str__(self): return self.word - + \ No newline at end of file diff --git a/games/codenames/game.py b/games/codenames/game.py index 311be37..7941656 100644 --- a/games/codenames/game.py +++ b/games/codenames/game.py @@ -24,8 +24,8 @@ class CodenamesGame(Game): operative_list : List[Agent] = field(default_factory=list) red_team_list : List[Agent] = field(default_factory=list) blue_team_list : List[Agent] = field(default_factory=list) - show_state : bool = True - game_is_over : bool = False + show_state : bool = True + game_is_over : bool = False game_board : Board = None def init_game(self, agent_1_class: Agent, agent_2_class: Agent): @@ -43,7 +43,7 @@ def init_game(self, agent_1_class: Agent, agent_2_class: Agent): def set_config(self, config: Config): self.config = config self.rules = config.codenames_rules - + def _validate_agents(self, agents): if len(agents) != 4: raise ValueError("Exactly four agents are required to start the game.") @@ -73,7 +73,7 @@ def setup_board(self): self.game_board = Board(self.config) self.game_board.current_turn = CardType.RED - + def get_agent_team(self, agent: Agent) -> CardType: if agent in self.red_team_list: return CardType.RED @@ -141,7 +141,7 @@ def _get_operative_actions(self, current_num_guesses) -> dict: actions["guess_" + str(index)] = "Guess the word: " + card.word actions["end_turn"] = "End your current turn." return actions - + def _validate_role(self, agent: Agent, role_list: List[Agent], role_name: str): if agent not in role_list: @@ -155,20 +155,24 @@ def get_observation(self, agent : Agent) -> Tuple[Observation, AvailableActions] return self.get_operative_observation(agent) else: raise ValueError("Agent is not in the game.") - + def update_spymaster(self, action : Action, available_actions : AvailableActions, agent : Agent): if agent not in self.spymaster_list: raise ValueError("Agent is not a spymaster.") if action.action_id == "submit_clue": - clue, num_guesses = action.openended_response.split(",") - num_guesses = int(num_guesses) + try: + clue, num_guesses = action.openended_response.split(",") + num_guesses = int(num_guesses) + except ValueError: + clue = "None" + num_guesses = 1 if num_guesses < 0: raise ValueError("Number of guesses must be non-negative.") self.game_board.last_hint = (clue, num_guesses) else: raise ValueError("Invalid action for spymaster.") - + if self.get_agent_team(agent) == CardType.RED: self.game_board.last_red_hint = (clue, num_guesses) else: @@ -185,13 +189,13 @@ def handle_turn(self, card, index, expected_card_type): else: self.game_is_over = True - + def update_operative(self, action : Action, available_actions : AvailableActions, agent : Agent): if agent not in self.operative_list: raise ValueError("Agent is not an operative.") if action.action_id.startswith("guess_"): index = int(action.action_id.split("_")[1]) - + guessed_word = self.game_board.cards[index].word card_type = self.game_board.cards[index].card_type if self.get_agent_team(agent) == CardType.RED: @@ -231,14 +235,14 @@ def update(self, action: Action, available_actions: AvailableActions, agent: Age self.update_operative(action, available_actions, agent) else: raise ValueError("Agent is not in the game.") - - + + def reset_last_turn_guesses(self): if self.game_board.current_turn == CardType.BLUE: self.game_board.last_blue_guesses = [] else: self.game_board.last_red_guesses = [] - + def _process_spymaster_turn(self, spymaster: Agent): if self.game_board.last_hint[0] is None: self.reset_last_turn_guesses() @@ -253,7 +257,7 @@ def _process_spymaster_turn(self, spymaster: Agent): def _check_turn_end(self) -> bool: return self.game_board.is_turn_over() - + def _process_operative_turn(self, operative: Agent): observation, actions = self.get_operative_observation(operative) action = operative.take_action(self.rules, observation, actions, show_state=self.show_state) @@ -272,7 +276,7 @@ def _determine_scores(self) -> Tuple[float, float]: red_points = sum(1 for i, card in enumerate(self.game_board.cards) if card.card_type == CardType.RED and self.game_board.revealed[i]) blue_points = sum(1 for i, card in enumerate(self.game_board.cards) if card.card_type == CardType.BLUE and self.game_board.revealed[i]) assassin_revealed = any(card.card_type == CardType.ASSASSIN and self.game_board.revealed[i] for i, card in enumerate(self.game_board.cards)) - + if assassin_revealed: if winner == CardType.RED: blue_points = 0 @@ -283,12 +287,12 @@ def _determine_scores(self) -> Tuple[float, float]: red_points += 3 elif winner == CardType.BLUE: blue_points += 3 - + total_points = red_points + blue_points - + if total_points == 0: return 0.5, 0.5 - + return red_points / total_points, blue_points / total_points def _get_current_team(self) -> Tuple[Agent, Agent]: @@ -296,7 +300,7 @@ def _get_current_team(self) -> Tuple[Agent, Agent]: return self.spymaster_1, self.operative_1 else: return self.spymaster_2, self.operative_2 - + def play(self) -> Tuple[float, float]: while not self.game_is_over: diff --git a/games/codenames/test.py b/games/codenames/test.py index 620b1c4..9490e43 100644 --- a/games/codenames/test.py +++ b/games/codenames/test.py @@ -22,4 +22,3 @@ for i in range(1): game = Game(config=config, agents=agents) print(game.play()) - diff --git a/games/hive/board.py b/games/hive/board.py new file mode 100644 index 0000000..4658c7f --- /dev/null +++ b/games/hive/board.py @@ -0,0 +1,379 @@ +from .pieces import HivePiece, Grasshopper, Spider +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import numpy as np +from matplotlib.animation import FuncAnimation +from multiprocessing import Process +from PIL import Image +import io +import os + +class HiveBoardVisualizer: + def __init__(self, board, piece_images=None): + self.board = board + self.counter = 0 + self.piece_images = piece_images if piece_images else {} + if not os.path.exists('images'): + os.makedirs('images') + + def draw_hexagon(self, ax, center, size=1, fill_color='white', edge_color='black'): + """Draw a hexagon given a center, size.""" + hexagon = patches.RegularPolygon(center, numVertices=6, radius=size, orientation=0, + facecolor=fill_color, edgecolor=edge_color, linewidth=1.5) + ax.add_patch(hexagon) + return hexagon + + def draw_piece(self, ax, center, coords, piece, label_color='black'): + """Draw a piece on the hexagon.""" + ax.text(center[0], center[1], piece.type + "\n" + "(" + str(coords[0]) + ", " + str(coords[1]) + ")", + ha='center', va='center', fontsize=6.5, color=label_color) + + def draw_fake_piece(self, ax, center, coords, label_color='black'): + """Draw a fake piece on the hexagon.""" + ax.text(center[0], center[1], "(" + str(coords[0]) + ", " + str(coords[1]) + ")", + ha='center', va='center', fontsize=6.5, color=label_color) + + def draw_board(self, interactive=False): + """Draw and display the Hive board.""" + fig, ax = plt.subplots(figsize=(5.12, 5.12), dpi=100) + ax.set_aspect('equal') + ax.axis('off') # Hide the axes + + # Find board limits + min_x, max_x, min_y, max_y = self.find_board_limits() + ax.set_xlim(min_x - 1, max_x + 1) + ax.set_ylim(min_y - 1, max_y + 1) + seen_set = set() + # Draw hexagons and pieces + for hex, piece in self.board.items(): + x, y = self.hex_to_pixel(hex) + fill_color = 'lightgreen' if piece.owner == 1 else 'lightblue' + self.draw_hexagon(ax, (x, y), fill_color=fill_color) + self.draw_piece(ax, (x, y), (hex.x, hex.y), piece) + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex not in self.board and neighbor_hex not in seen_set: + nx, ny = self.hex_to_pixel(neighbor_hex) + self.draw_hexagon(ax, (nx, ny), fill_color='white', edge_color='black') + self.draw_fake_piece(ax, (nx, ny), (neighbor_hex.x, neighbor_hex.y), label_color='red') + seen_set.add(neighbor_hex) + + + if interactive: + plt.show() + + # return as PIL image + self.counter += 1 + plt.savefig('images/board_' + str(self.counter) + '.png', format='png') + plt.close() + return Image.open('images/board_' + str(self.counter) + '.png') + + def find_board_limits(self): + """Calculate the limits of the board to set the display size.""" + all_hexes = set(list(self.board.keys())) + # add all neighbors to the list + for hex in self.board.keys(): + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex not in self.board: + all_hexes.add(neighbor_hex) + + x_coords = [self.hex_to_pixel(hex)[0] for hex in all_hexes] + y_coords = [self.hex_to_pixel(hex)[1] for hex in all_hexes] + if not x_coords or not y_coords: + return (0, 0, 0, 0) + return min(x_coords), max(x_coords), min(y_coords), max(y_coords) + + def hex_to_pixel(self, hex, size=1): + """Convert hex coordinates to pixel coordinates for pointy-topped hexagons with odd-r vertical layout.""" + x = size * np.sqrt(3) * (hex.x + 0.5 * (hex.y & 1)) + y = size * 3/2 * hex.y + return (x, y) + +class Hex: + EVEN_R_DIRECTIONS = [(1, 0), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1)] + ODD_R_DIRECTIONS = [(1, 0), (1, -1), (0, -1), (-1, 0), (0, 1), (1, 1)] + + def __init__(self, x, y): + self.x = x + self.y = y + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + def __hash__(self): + return hash((self.x, self.y)) + + def neighbor(self, direction): + """ Returns the neighboring hex in the given direction """ + directions = Hex.EVEN_R_DIRECTIONS if self.y % 2 == 0 else Hex.ODD_R_DIRECTIONS + dq, dr = directions[direction] + return Hex(self.x + dq, self.y + dr) + + def __str__(self): + return f"({self.x},{self.y})" + +class HiveBoard: + def __init__(self): + self.board = {} # Dictionary to store pieces keyed by their Hex coordinates + self.queen_bee_placed = [False, False] + self.visualizer = HiveBoardVisualizer(self.board) + + + def add_piece(self, piece, hex): + if hex in self.board: + raise ValueError("There is already a piece at this position") + self.board[hex] = piece + + def get_queen_bee(self, team_id): + for hex, piece in self.board.items(): + if piece.type == "Queen" and piece.owner == team_id: + return hex + return None + + def get_surrounding_pieces(self, team_id, hex): + """ + Get the pieces surrounding the given hex. + """ + surrounding_pieces = [] + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex in self.board: + surrounding_pieces.append(self.board[neighbor_hex]) + return surrounding_pieces + + def create_text_board(self, team_id): + """ + Print the board with the pieces. Do this in a way that it is easy to see the pieces and their positions on the board. Start by finding the current hex. + """ + + # Find the current hex + if not self.board: + return "Empty Board" + min_x = min(hex.x for hex in self.board) + max_x = max(hex.x for hex in self.board) + min_y = min(hex.y for hex in self.board) + max_y = max(hex.y for hex in self.board) + + for y in range(max_y, min_y - 1, -1): + row = " " * (max_y - y) + for x in range(min_x, max_x + 1): + hex = Hex(x, y) + if hex in self.board: + piece = self.board[hex] + row += f" {piece.type}({x},{y}) " + else: + row += f" .({x},{y}) " + print(row) + print() + + def generate_text_board(self): + return self.visualizer.text_representation() + + def display_board(self, interactive=False): + """ + Display the board with the pieces and their positions. Produce a visual representation of the board such that an image can be exported. + """ + return self.visualizer.draw_board(interactive) + + def is_queen_surrounded(self, owner): + """ + Check if the queen bee is surrounded by enemy pieces. + """ + queen_hex = next((hex for hex, piece in self.board.items() if piece.owner == owner and piece.type == "Queen"), None) + if queen_hex is None: + return False + + for direction in range(6): + neighbor_hex = queen_hex.neighbor(direction) + if neighbor_hex not in self.board: + return False + return True + + + def is_adjacent_to_enemy_piece(self, hex, owner): + """ + Check if the given hex is adjacent to a piece owned by the enemy player. + """ + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex in self.board and self.board[neighbor_hex].owner != owner: + return True + return False + + def is_one_hive_if_added(self, hex): + """ + Check if the hive remains connected if a piece is temporarily added. + """ + if hex in self.board: + return self.is_one_hive() + else: + temporary_piece = Grasshopper(0) + self.board[hex] = temporary_piece + is_connected = self.is_one_hive() + del self.board[hex] + return is_connected + + def can_place_piece(self, piece, hex): + """ + Check if a piece can be placed at the given hex according to Hive rules. + + It must be placed adjacent to another piece and not break the One-Hive Rule. + + It cannot be adjacent to an enemy piece unless it is the first move. + + It cannot be placed on top of another piece. + """ + if hex in self.board: + return False + + if len(self.board) >= 2 and self.is_adjacent_to_enemy_piece(hex, piece.owner): + return False + + if not self.is_one_hive_if_added(hex): + return False + return True + + def can_move_piece(self, from_hex, to_hex): + """ + Check if a piece can move from from_hex to to_hex according to Hive rules. + """ + + if self.get_piece_at(to_hex) is not None: + return False + + if self.get_piece_at(from_hex) is None: + return False + + if not self.has_freedom_to_move(from_hex, to_hex): + return False + + if not self.is_one_hive_if_removed(from_hex): + return False + + # Temporarily move the piece to check One-Hive Rule + piece = self.board[from_hex] # Remove the top piece temporarily + del self.board[from_hex] + self.board[to_hex] = piece # Move the piece to the destination + + one_hive = self.is_one_hive() + + self.board[from_hex] = piece + + del self.board[to_hex] + + return one_hive + + def is_in_direct_contact(self, piece, hex): + """ + Check if a piece is in direct contact with a given hex. + """ + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if self.get_piece_at(neighbor_hex) is piece: + return True + return False + + def is_one_hive_if_removed(self, hex_to_remove): + """ + Check if the hive remains connected if a piece is temporarily removed. + """ + temp_piece = self.board.get(hex_to_remove) + del self.board[hex_to_remove] + is_connected = self.is_one_hive() + self.board[hex_to_remove] = temp_piece # Put the piece back + return is_connected + + def is_almost_completely_surrounded(self, hex): + """ + Check if a hex is almost completely surrounded by other pieces. + A hex is almost completely surrounded if there is one or no empty spaces adjacent to it. + """ + adjacent_hexes = self.get_adjacent_hexes(hex) + empty_adjacent_hexes = [h for h in adjacent_hexes if h not in self.board] + + # If there is one or no empty adjacent hexes, the hex is almost completely surrounded + return len(empty_adjacent_hexes) <= 1 + + def get_adjacent_hexes(self, hex): + """ + Returns a list of hexes adjacent to the given hex. + """ + return [hex.neighbor(direction) for direction in range(6)] + + def has_freedom_to_move(self, from_hex, to_hex): + """ + Check the Freedom to Move Rule between from_hex and to_hex. + """ + # Grasshopper exception to freedom to move rule + if from_hex in self.board and self.board[from_hex].type == "Hopper": + return True + # Check if the destination hex is empty and not almost completely surrounded + if to_hex in self.board or self.is_almost_completely_surrounded(to_hex): + return False + + # Check if the starting hex is almost completely surrounded + if self.is_almost_completely_surrounded(from_hex): + return False + + # If 'from_hex' and 'to_hex' are adjacent, we check if the move is directly possible + if to_hex in self.get_adjacent_hexes(from_hex): + return True + + return False # If none of the conditions are met, the move is not allowed. + + def is_one_hive(self): + """ + Check if the board still forms one connected group (One-Hive Rule). + This can be implemented using a depth-first search from any piece on the board. + """ + if not self.board: + return True + + start = next(iter(self.board)) + visited = set() + stack = [start] + + while stack: + current = stack.pop() + if current in visited: + continue + visited.add(current) + for direction in range(6): + neighbor = current.neighbor(direction) + if neighbor in self.board: + stack.append(neighbor) + + return len(visited) == len(self.board) + + def move_piece(self, from_hex, to_hex): + """ + Move a piece from from_hex to to_hex. + """ + if from_hex not in self.board or not self.board[from_hex]: + raise ValueError("There is no piece at the starting position") + + moving_piece = self.board[from_hex] + if not self.board.get(to_hex): + self.board[to_hex] = moving_piece + + del self.board[from_hex] + + + def get_adjacent_pieces(self, hex): + """ + Returns a list of pieces adjacent to the given hex. + """ + adjacent_pieces = [] + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex in self.board: + adjacent_pieces.append(self.board[neighbor_hex]) + return adjacent_pieces + + def get_piece_at(self, hex): + return self.board.get(hex, None) + + def is_adjacent_empty(self, hex): + """ Check if there are empty adjacent spaces around the hex """ + return any(self.get_piece_at(hex.neighbor(direction)) is None for direction in range(6)) diff --git a/games/hive/config.py b/games/hive/config.py new file mode 100644 index 0000000..716ec65 --- /dev/null +++ b/games/hive/config.py @@ -0,0 +1,21 @@ +from api.classes import Rules + +class GameConfig: + NUM_BEETLE_CARDS = 0 + NUM_SPIDER_CARDS = 2 + NUM_GRASSHOPPER_CARDS = 3 + NUM_SOLDIERANT_CARDS = 3 + NUM_QUEENBEE_CARDS = 1 + MAX_TURNS = 250 + rules = Rules( + title="Hive", summary="Hive is a bug-themed abstract strategy game. The object of Hive is to capture the opponent's queen bee by allowing it to become completely surrounded by pieces belonging to either player, while avoiding the capture of one's own queen. Tiles can be moved to other positions after being placed according to various rules, much like chess pieces.", + additional_details={"Placing the Queen Bee": "Players must place their Queen Bee by their fourth turn. Until then, they cannot move any placed pieces.", + "Queen Bee Movement": "The Queen Bee can only move one space at a time around the hive.", + "Spider Movement": "The Spider can move exactly three spaces.", + "Ant Movement": "Able to move to any empty space around the hive as long as other movement rules are not violated.", + "Grasshopper Movement": "The Grasshopper can jump over over adjacent pieces, landing on the first empty space.", + "One Hive Rule": "The tiles must always be connected; you cannot move a piece if it would break the hive into separate groups.", + "Freedom to Move": "A piece can only move if it can physically slide to its new position without disturbing other tiles.", + "Max Turns": "The game ends after " + str(MAX_TURNS) + " turns." + "If no Queen Bee is surrounded by the end of the game, the game is a draw." + } + ) \ No newline at end of file diff --git a/games/hive/game.py b/games/hive/game.py new file mode 100644 index 0000000..bbfe7aa --- /dev/null +++ b/games/hive/game.py @@ -0,0 +1,362 @@ +# TODO: handle beetle movement +from api.classes import Agent, Action, Observation, AvailableActions, Rules +from .pieces import HivePiece, QueenBee, Grasshopper, Spider, SoldierAnt +from .config import GameConfig as Config +from api.classes import Game +from .board import HiveBoard, Hex +from dataclasses import dataclass, field +import random + +default_config = Config() + + +@dataclass +class HiveGame(Game): + + id : str = "hive" + config : Config = default_config + board : HiveBoard = None + rules : Rules = default_config.rules#None + image_mode : bool = True + interactive_mode : bool = False + + def export_state(self): + """ + Export the current game state. + """ + return { + "board": self.board.board, + "players": self.players, + "current_player_index": self.current_player_index, + "turn_count": self.turn_count, + "pieces_remaining": self.pieces_remaining, + "queen_bee_placed": self.board.queen_bee_placed + } + + def init_game(self, agent_1_class: Agent, agent_2_class : Agent): + """ + Initialize the game. + """ + self.board = HiveBoard() + self.players = [agent_1_class(team_id=0, agent_id=0, **self.agent_1_kwargs), agent_2_class(team_id=1, agent_id=1, **self.agent_2_kwargs)] + self.current_player_index = 0 + self.turn_count = [0, 0] + self.pieces_remaining = [] + self.rules = self.config.rules + + self.add_starting_pieces(0) + self.add_starting_pieces(1) + + + def add_starting_pieces(self, team_id): + """ + Add the starting pieces to the board. + Per the official rules without expansions this includes the following for each team: + - 1 Queen Bee + - 2 Spiders + - 3 Grasshoppers + - 3 Soldier Ants + """ + for _ in range(self.config.NUM_QUEENBEE_CARDS): + self.pieces_remaining.append(QueenBee(team_id)) + + for _ in range(self.config.NUM_SPIDER_CARDS): + self.pieces_remaining.append(Spider(team_id)) + + for _ in range(self.config.NUM_GRASSHOPPER_CARDS): + self.pieces_remaining.append(Grasshopper(team_id)) + + for _ in range(self.config.NUM_SOLDIERANT_CARDS): + self.pieces_remaining.append(SoldierAnt(team_id)) + + def get_observation(self, agent): + """ + Get the observation and available actions for the given agent. + """ + if agent not in self.players: + raise ValueError("Agent is not in the game.") + + # Define the observation based on the current state of the board + observation = self.generate_observation(agent) + + # Define available actions (place a piece or move a piece) + actions = self.get_available_actions(agent) + + if not actions: + actions = {"pass": "Pass the turn."} + + return Observation(observation), AvailableActions(instructions="Choose a move:", predefined=actions, openended={}) + + def generate_observation(self, agent): + """ + Generate the current game state observation for the agent. + """ + image = None + if self.image_mode: + image = self.board.display_board(interactive=self.interactive_mode) + + remaining_turns = self.config.MAX_TURNS - max(self.turn_count) + text = "{current_team} to move. Surround the enemy Queen. You have {remaining_turns} left.".format(current_team="Green" if agent.team_id == 1 else "Blue", remaining_turns=remaining_turns) + if not self.image_mode: + text += "\n\nBoard:\n\n" + self.board.generate_text_board() + return Observation(text, image=image) + + def get_available_actions(self, agent): + """ + Get the available actions for the agent based on the game rules and board state. + """ + # List actions such as placing or moving pieces + # The actions should be tailored based on the game's rules and the current turn + return self.list_actionable_pieces(agent.team_id) + + def list_possible_moves_for_placed_piece(self, piece, hex): + """ + List all possible moves for a piece that is already placed on the board. + """ + possible_moves = {} + for possible_move in piece.valid_moves(self.board): + possible_moves["move_" + str(hex) + "_" + str(possible_move)] = "Move the piece to " + str(possible_move) + return possible_moves + + def list_possible_moves_for_unplaced_piece(self, piece, player_index): + """ + List all possible moves for a piece that has not been placed on the board yet. + + If 3 moves have passed without the Queen Bee being placed, then it can be the only possible move. + """ + if self.turn_count[player_index] == 3 and not self.board.queen_bee_placed[player_index]: + if piece.type != "Queen": + return [] + + current_hexes = [hex for hex in self.board.board if self.board.board[hex]] + possible_places = [] + for hex in current_hexes: + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex not in self.board.board: + possible_places.append(neighbor_hex) + if not self.board.board: + possible_places.append(Hex(0, 0)) + + actions = {} + for hex in possible_places: + if self.board.can_place_piece(piece, hex): + actions["place_" + str(hex) + "_" + str(piece.type)] = "" + return actions + + + + def list_actionable_pieces(self, player_index): + """ + List all pieces that can be moved or placed by the player. + """ + + possible_actions = {} + + possible_pieces_to_place = [piece for piece in self.pieces_remaining if piece.owner == player_index] + seen_pieces = set() + + for possible_piece in possible_pieces_to_place: + if possible_piece.type in seen_pieces: + continue + seen_pieces.add(possible_piece.type) + possible_moves = self.list_possible_moves_for_unplaced_piece(possible_piece, player_index) + if len(possible_moves) > 0: + possible_actions["list_place_" + str(possible_piece.type)] = "Place the " + str(possible_piece.type) + " piece." + + if not self.board.queen_bee_placed[player_index]: + return possible_actions + + for hex in list(self.board.board.keys()): + if self.board.board[hex].owner == player_index: + possible_moves = self.list_possible_moves_for_placed_piece(self.board.board[hex], hex) + possible_piece = self.board.board[hex] + if len(possible_moves) > 0: + possible_actions["list_move_" + str(possible_piece.type) + "_" + str(hex)] = "Move the " + str(possible_piece.type) + " piece." + + return possible_actions + + def process_piece_place_action(self, action, agent): + """ + Process the action of placing a piece on the board. + """ + hex_coords = action.split("_")[1].split(",") + hex_x = int(hex_coords[0][1:]) + hex_y = int(hex_coords[1][0: -1]) + hex = Hex(hex_x, hex_y) + piece_type = action.split("_")[2] + for piece in self.pieces_remaining: + if piece.type == piece_type and piece.owner == agent.team_id: + break + else: + raise ValueError("Invalid action") + + if self.board.can_place_piece(piece, hex): + self.board.add_piece(piece, hex) + self.pieces_remaining.remove(piece) + if piece.type == "Queen": + self.board.queen_bee_placed[agent.team_id] = True + + def process_piece_move_action(self, action, agent): + """ + Process the action of moving a piece on the board. + """ + from_hex = action.split("_")[1][1:-1].split(",") + to_hex = action.split("_")[2][1:-1].split(",") + + from_hex = Hex(int(from_hex[0]), int(from_hex[1])) + to_hex = Hex(int(to_hex[0]), int(to_hex[1])) + + if from_hex not in self.board.board or to_hex in self.board.board: + raise ValueError("Invalid action") + piece = self.board.board[from_hex] + if piece.owner != agent.team_id: + raise ValueError("Invalid action") + if to_hex in piece.valid_moves(self.board): + self.board.move_piece(from_hex, to_hex) + + def process_list_placement_action(self, action, agent): + """ + Process the action of listing possible placements for a piece. + """ + piece_type = action.split("_")[2] + for piece in self.pieces_remaining: + if piece.type == piece_type and piece.owner == agent.team_id: + return self.list_possible_moves_for_unplaced_piece(piece, agent.team_id) + + + def process_list_moves_action(self, action, agent): + """ + Process the action of listing possible moves for a piece. + """ + hex_coords = action.split("_")[3].split(",") + hex_x = int(hex_coords[0][1:]) + hex_y = int(hex_coords[1][0:-1]) + hex = Hex(hex_x, hex_y) + if hex not in self.board.board: + raise ValueError("Invalid action") + piece = self.board.board[hex] + return self.list_possible_moves_for_placed_piece(piece, hex) + + def update(self, action, agent): + """ + Update the game state based on the agent's action. + """ + action_id = action.action_id + if action_id.startswith("place"): + self.process_piece_place_action(action_id, agent) + elif action_id.startswith("move"): + self.process_piece_move_action(action_id, agent) + elif action_id.startswith("list_place"): + return self.process_list_placement_action(action_id, agent) + elif action_id.startswith("list_move"): + return self.process_list_moves_action(action_id, agent) + else: + raise ValueError("Invalid action") + + def play_turn(self): + """ + Play a turn for the current player. + """ + agent = self.players[self.current_player_index] + observation, actions = self.get_observation(agent) + + if self.show_state: + print(self.board.board) + + if not actions or not actions.predefined: + self.next_player() + return + + piece_actions = actions.predefined + action_id = agent.take_action(self.rules, observation, actions, show_state=self.show_state).action_id + + if self.show_state: + print("Action ID: ", action_id) + + if action_id not in piece_actions: + if self.show_state: + print("Invalid action: ", action_id) + action_id = random.choice(list(piece_actions.keys())) + if action_id == "pass": + self.next_player() + return + + specific_move_actions = self.update(Action(action_id=action_id), agent) + new_actions = AvailableActions(instructions="Choose a move:", predefined=specific_move_actions, openended={}) + if specific_move_actions: + action_id = agent.take_action(self.rules, observation, new_actions, show_state=self.interactive_mode).action_id + + if self.show_state: + print("Action ID: ", action_id) + + if action_id not in specific_move_actions: + print("Invalid action: ", action_id) + action_id = random.choice(list(specific_move_actions.keys())) + else: + self.next_player() + return + if action_id == "pass": + self.next_player() + return + self.update(Action(action_id=action_id), agent) + self.next_player() + + def next_player(self): + """ + Switch to the next player. + """ + self.turn_count[self.current_player_index] += 1 + self.current_player_index = (self.current_player_index + 1) % len(self.players) + + def get_intermediate_score(self): + """ + Intermediate scoring function based on how surrounded each Queen Bee is. The less surrounded Queen Bee wins. This is a heuristic if we run out of turns. + """ + num_pieces_1 = len(self.board.get_surrounding_pieces(self.players[0].team_id, self.board.get_queen_bee(self.players[0].team_id))) + num_pieces_2 = len(self.board.get_surrounding_pieces(self.players[1].team_id, self.board.get_queen_bee(self.players[1].team_id))) + if num_pieces_1 < num_pieces_2: + return [1, 0] + elif num_pieces_2 < num_pieces_1: + return [0, 1] + else: + return [0, 0] + + + def is_game_over(self): + """ + Check if the game is over by checking if either player's Queen Bee is surrounded. + + """ + if max(self.turn_count) >= self.config.MAX_TURNS: + return True + for player in self.players: + if self.board.is_queen_surrounded(player.team_id): + return True + return False + + def play(self): + """ + Run the game loop. + """ + while not self.is_game_over(): + self.play_turn() + if self.image_mode: + image = self.board.display_board(interactive=self.interactive_mode) + queen_1_surrounded = self.board.is_queen_surrounded(self.players[0].team_id) + queen_2_surrounded = self.board.is_queen_surrounded(self.players[1].team_id) + + if queen_1_surrounded and queen_2_surrounded: + return [0, 0] + elif queen_1_surrounded: + return [0, 1] + elif queen_2_surrounded: + return [1, 0] + else: + return [0.5, 0.5] + #return self.get_intermediate_score() + + + + +# Agent class and other relevant classes/methods not shown for brevity diff --git a/games/hive/pieces.py b/games/hive/pieces.py new file mode 100644 index 0000000..b652b8d --- /dev/null +++ b/games/hive/pieces.py @@ -0,0 +1,348 @@ +from abc import ABC, abstractmethod +class HivePiece(ABC): + def __init__(self, type, owner): + self.type = type + self.owner = owner + + @abstractmethod + def valid_moves(self, board): + pass + + def find_current_hex(self, board): + for hex, piece in board.board.items(): + if piece is self: + return hex + return None + + def __hash__(self) -> int: + # hash based on the type and owner + return hash((self.type, self.owner)) + +class QueenBee(HivePiece): + def __init__(self, owner): + super().__init__("Queen", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + valid_moves = [] + for direction in range(6): + neighbor_hex = current_hex.neighbor(direction) + if board.can_move_piece(current_hex, neighbor_hex): + valid_moves.append(neighbor_hex) + + return valid_moves + + + +class Spider(HivePiece): + def __init__(self, owner): + super().__init__("Spider", owner) + + def valid_moves(self, board): + start_hex = self.find_current_hex(board) + if start_hex is None: + return [] + + visited = set([start_hex]) # Initialize the visited set with the starting hex + valid_moves = self.find_moves(start_hex, start_hex, board, 3, visited) + return valid_moves + + def has_common_neighboring_piece(self, hex1, hex2, board): + neighbors1 = self.get_neighbors_with_pieces(hex1, board) + neighbors2 = self.get_neighbors_with_pieces(hex2, board) + return any(neighbor in neighbors2 for neighbor in neighbors1) + + def get_neighbors_with_pieces(self, hex, board): + neighbors_with_pieces = set() + for direction in range(6): + neighbor_hex = hex.neighbor(direction) + if neighbor_hex in board.board: + neighbors_with_pieces.add(neighbor_hex) + return neighbors_with_pieces + + def find_moves(self, start_hex, current_hex, board, steps_remaining, visited): + if steps_remaining == 0: + # If the spider has moved 3 steps, return the current hex as a possible move, + # but only if it is not the starting hex (which is already in visited) + return [current_hex] if current_hex not in visited else [] + new_visited = visited.union({current_hex}) + moves = [] + for direction in range(6): + neighbor_hex = current_hex.neighbor(direction) + + is_current_hex_real = current_hex in board.board + + if not is_current_hex_real: + temp_spider = Spider(0) + board.board[current_hex] = temp_spider + backup_old_spider = board.board[start_hex] + del board.board[start_hex] + + if board.can_move_piece(current_hex, neighbor_hex) and neighbor_hex not in visited and self.has_common_neighboring_piece(current_hex, neighbor_hex, board): + # Add the neighbor to the visited set to prevent backtracking in this move sequence + if not is_current_hex_real: + is_current_hex_real = True + del board.board[current_hex] + board.board[start_hex] = backup_old_spider + further_moves = self.find_moves(start_hex, neighbor_hex, board, steps_remaining - 1, new_visited) + moves.extend(further_moves) + + if not is_current_hex_real: + del board.board[current_hex] + board.board[start_hex] = backup_old_spider + + return list(set(moves)) # Removing duplicates + + def find_current_hex(self, board): + for hex, piece in board.board.items(): + if piece is self: + return hex + return None + + def find_current_hex(self, board): + for hex, piece in board.board.items(): + if piece is self: + return hex + return None + +class Grasshopper(HivePiece): + def __init__(self, owner): + super().__init__("Hopper", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + valid_moves = [] + for direction in range(6): + jump_hex = self.jump_over_pieces(board, current_hex, direction) + if jump_hex is not None: + if board.can_move_piece(current_hex, jump_hex): + valid_moves.append(jump_hex) + return valid_moves + + def jump_over_pieces(self, board, start_hex, direction): + """ + Jump over the pieces in the given direction and return the landing hex. + At least one piece must be jumped over. Check if the hex is within the bounds of the board. + """ + current_hex = start_hex.neighbor(direction) + jumped_over_piece = False + + # Continue jumping over pieces until an empty hex is found + while current_hex and board.get_piece_at(current_hex) is not None: + current_hex = current_hex.neighbor(direction) + jumped_over_piece = True + + # Ensure that we have jumped at least one piece and the landing hex is not the same as the starting hex + return current_hex if current_hex and current_hex != start_hex and jumped_over_piece else None + + +class SoldierAnt(HivePiece): + def __init__(self, owner): + super().__init__("Ant", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + return self.find_moves(current_hex, board, set(), current_hex) + + def find_moves(self, current_hex, board, visited, start_hex): + """ + Recursively find all valid moves for the Soldier Ant. + """ + if current_hex in visited: + return [] + + if start_hex is None: + start_hex = current_hex + + visited.add(current_hex) + moves = [] + + + for direction in range(6): + neighbor_hex = current_hex.neighbor(direction) + + temp_ant_created = False + + if current_hex not in board.board: + temp_ant = SoldierAnt(0) + board.board[current_hex] = temp_ant + backup_old_ant = board.board[start_hex] + temp_ant_created = True + del board.board[start_hex] + + if board.can_move_piece(current_hex, neighbor_hex) and neighbor_hex != start_hex: + moves.append(neighbor_hex) + if temp_ant_created: + del board.board[current_hex] + board.board[start_hex] = backup_old_ant + temp_ant_created = False + moves.extend(self.find_moves(neighbor_hex, board, visited, start_hex)) + + if temp_ant_created: + del board.board[current_hex] + board.board[start_hex] = backup_old_ant + temp_ant_created = False + + + return list(set(moves)) + +# expansion and not necessarily part of the core gmae. not using this for now +""" +class Mosquito(HivePiece): + def __init__(self, owner): + super().__init__("Mosquito", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + valid_moves = [] + for adjacent_piece in board.get_adjacent_pieces(current_hex): + valid_moves.extend(adjacent_piece.valid_moves(board)) + + return list(set(valid_moves)) +""" + +# expansion and not necessarily part of the core gmae. not using this for now +""" +class Pillbug(HivePiece): + def __init__(self, owner): + super().__init__("Pillbug", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + valid_moves = [] + for direction in range(6): + adjacent_hex = current_hex.neighbor(direction) + piece = board.get_piece_at(adjacent_hex) + if piece and not self.is_covered(board, adjacent_hex): + for target_direction in range(6): + target_hex = current_hex.neighbor(target_direction) + if self.can_move_other_piece(board, adjacent_hex, target_hex): + valid_moves.append((adjacent_hex, target_hex)) + + return valid_moves + + def can_move_other_piece(self, board, from_hex, to_hex): + if from_hex == to_hex or board.get_piece_at(to_hex) is not None: + return False + + if not board.is_adjacent(from_hex, to_hex) or self.is_covered(board, to_hex): + return False + + if not board.is_one_hive_if_removed(from_hex): + return False + + if board.has_recently_moved(from_hex) or self.has_recently_moved(board, self.find_current_hex(board)): + return False + + if board.is_beetle_gate_present(from_hex, to_hex): + return False + + return True + + def is_covered(self, board, hex): + return board.is_piece_covered(hex) + + + def find_current_hex(self, board): + for hex, piece in board.board.items(): + if piece is self: + return hex + return None +""" +# expansion and not necessarily part of the core gmae. not using this for now +""" +class Ladybug(HivePiece): + def __init__(self, owner): + super().__init__("Ladybug", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + return self.find_moves(current_hex, board, 3) + + def find_moves(self, current_hex, board, steps_remaining): + Recursively find all valid moves for the Ladybug. + if steps_remaining == 0: + return [current_hex] if board.get_piece_at(current_hex) is None else [] + + moves = [] + for direction in range(6): + neighbor_hex = current_hex.neighbor(direction) + if steps_remaining > 1 or board.get_piece_at(neighbor_hex) is None: + if board.can_move_piece(current_hex, neighbor_hex): # Check for climb up/down + further_moves = self.find_moves(neighbor_hex, board, steps_remaining - 1) + moves.extend(further_moves) + + return list(set(moves)) + +""" + +# this is currently not being used to simplify the implementation +""" +class Beetle(HivePiece): + def __init__(self, owner): + super().__init__("Beetle", owner) + + def valid_moves(self, board): + current_hex = self.find_current_hex(board) + if current_hex is None: + return [] + + valid_moves = [] + for direction in range(6): + neighbor_hex = current_hex.neighbor(direction) + if self.can_climb(board, current_hex, neighbor_hex) or board.can_move_piece(current_hex, neighbor_hex): + valid_moves.append(neighbor_hex) + + return valid_moves + + def can_climb(self, board, current_hex, target_hex): + Check if the Beetle can climb to the target hex. + # If the target hex is empty or has a stack, the Beetle can climb onto it + if board.get_piece_at(target_hex) is not None: + return True + + # Check for the Beetle's special movement restriction: + # It cannot move through a gap between two higher stacks + current_height = self.stack_height(board, current_hex) + target_height = self.stack_height(board, target_hex) + + for direction in range(6): + adjacent_hex = current_hex.neighbor(direction) + adjacent_height = self.stack_height(board, adjacent_hex) + + # Skip the target hex and the hex directly opposite to it + if adjacent_hex == target_hex or adjacent_hex == current_hex.neighbor((direction + 3) % 6): + continue + + if adjacent_height > current_height and adjacent_height > target_height: + # Found a gap that the Beetle cannot move through + return False + + def stack_height(self, board, hex): + Returns the height of the stack at the given hex. + piece = board.get_piece_at(hex) + height = 0 + while piece: + height += 1 + # Assuming we have a way to find the piece below in the stack + piece = piece.below + return height +""" diff --git a/games/hive/test.py b/games/hive/test.py new file mode 100644 index 0000000..7e73503 --- /dev/null +++ b/games/hive/test.py @@ -0,0 +1,20 @@ +from .game import HiveGame as Game +from build.lib.agents.human_agent import HumanAgent +#from agents.gpt import OpenAITextAgent +from api.classes import Agent +from .config import GameConfig + +# create a game config +config = GameConfig() + +# create a list of agents +#agents = [HumanAgent(agent_id=1, team_id=1, agent_type_id="agent"), HumanAgent(team_id=1, agent_id=2, agent_type_id="agent"), HumanAgent(team_id=2, agent_id=3, agent_type_id="agent"), HumanAgent(team_id=2, agent_id=4, agent_type_id="agent")] +#agents = [OpenAITextAgent(openai_model="gpt-4-1106-preview",agent_id=1, team_id=1, agent_type_id="agent"), HumanAgent(team_id=1, agent_id=2, agent_type_id="agent"), OpenAITextAgent(openai_model="gpt-4-1106-preview",agent_id=3, team_id=2, agent_type_id="agent"), OpenAITextAgent(openai_model="gpt-4-1106-preview",agent_id=4,team_id=2, agent_type_id="agent")] +agents = [HumanAgent(agent_id=1, team_id=1, agent_type_id="agent"), HumanAgent(team_id=1, agent_id=2, agent_type_id="agent"), HumanAgent(team_id=2, agent_id=3, agent_type_id="agent"), HumanAgent(team_id=2, agent_id=4, agent_type_id="agent")] + +# play the game + +for i in range(1): + game = Game(id="test",rules=None, agents=agents) + game.init_game(HumanAgent, HumanAgent) + print(game.play()) \ No newline at end of file diff --git a/games/pit.py b/games/pit.py deleted file mode 100644 index 021eefb..0000000 --- a/games/pit.py +++ /dev/null @@ -1,124 +0,0 @@ -from dataclasses import dataclass -from typing import List, Tuple -import random -from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules - - -# Define a simple Commodity class for representing commodities in the Pit game -@dataclass -class Commodity: - name: str - - -# Define the PitGame class implementing the Game interface -@dataclass -class PitGame(Game): - rules: Rules = Rules( - title="Pit", - summary="""Pit is a commodity trading game where players engage in trading to accumulate points and emerge as the winner. - The game involves commodity cards representing various goods, with each card holding a specific point value. - Players shout out their trade offers, attempting to negotiate deals with others to acquire valuable commodities. - Additionally, Bull and Bear cards periodically influence the market conditions, either boosting or decreasing commodity values. - The game continues with trading phases, market fluctuations, and scoring until a player or team reaches the agreed-upon point total, - declaring them the victor in the spirited world of commodity trading.""", - additional_details=None, - ) - id: str = "pit" - - def __post_init__( - self, - id: str = None, - rules: Rules = Rules, - agents: List[Agent] = [], - show_state: bool = False, - game_is_over: bool = False, - ): - #super().__init__(id, rules, agents, show_state, game_is_over) - self.commodities = [ - Commodity("Wheat"), - Commodity("Corn"), - Commodity("Barley"), - Commodity("Oats"), - ] - self.stock_pile = { - commodity.name: random.randint(1, 10) for commodity in self.commodities - } - self.scores = [] # Initialize scores as an empty list""" - - def init_game( - self, - agent_1_cls: Agent, - agent_2_cls: Agent, - ): - agent_1 = agent_1_cls( - team_id=0, - agent_id=1, - agent_type_id=agent_1_cls.agent_type_id, - **self.agent_1_kwargs, - ) - agent_2 = agent_2_cls( - team_id=0, - agent_id=2, - agent_type_id=agent_2_cls.agent_type_id, - **self.agent_2_kwargs, - ) - self.agents = [agent_1, agent_2] - self.scores = [0.0] * len( - self.agents - ) # Initialize scores with the correct length - - def get_observation(self, agent: Agent) -> Tuple[Observation, AvailableActions]: - observation_text = ( - f"{agent.agent_id}, it's your turn. Stock Pile: {self.stock_pile}" - ) - available_actions = AvailableActions( - instructions="Choose a commodity to trade", - predefined={ - commodity.name: f"Trade {commodity.name}" - for commodity in self.commodities - }, - openended={}, - ) - return Observation(text=observation_text), available_actions - - def update(self, action: Action, available_actions: AvailableActions, agent: Agent): - chosen_commodity = action.action_id - if chosen_commodity in available_actions.predefined: - if self.stock_pile[chosen_commodity] > 0: - self.stock_pile[chosen_commodity] -= 1 - # Increment agent's score by 1 - self.scores[self.agents.index(agent)] += 1 - if self.show_state: - print(f"{agent.agent_id} traded {chosen_commodity}") - else: - if self.show_state: - print(f"No more {chosen_commodity} in stock pile.") - else: - if self.show_state: - print("Invalid action. Choosing a random action instead.") - chosen_commodity = random.choice(list(available_actions.predefined.keys())) - if self.stock_pile[chosen_commodity] > 0: - self.stock_pile[chosen_commodity] -= 1 - # Increment agent's score by 1 - self.scores[self.agents.index(agent)] += 1 - if self.show_state: - print(f"{agent.agent_id} traded {chosen_commodity}") - else: - if self.show_state: - print(f"No more {chosen_commodity} in stock pile.") - - def play(self) -> Tuple[float, float]: - while not self.game_is_over: - for agent in self.agents: - observation, available_actions = self.get_observation(agent) - action = agent.take_action(self.rules, observation, available_actions, show_state=self.show_state) - self.update(action, available_actions, agent) - - if all(value == 0 for value in self.stock_pile.values()): - self.game_is_over = True - - # Normalize scores to sum up to 1 - total_score = sum(self.scores) - normalized_scores = [score / total_score for score in self.scores] - - return tuple(normalized_scores) diff --git a/games/pit/pit.py b/games/pit/pit.py new file mode 100644 index 0000000..2bfe42f --- /dev/null +++ b/games/pit/pit.py @@ -0,0 +1,365 @@ +from dataclasses import dataclass, field +from typing import List, Tuple, Dict +import random +import uuid +from api.classes import Observation, Action, Agent, AvailableActions, Game, Rules + + +@dataclass +class Commodity: + name: str + value: float + + +@dataclass +class TradeProposal: + proposer_id: int + offered_commodities: Dict[str, int] + status: str = "pending" + trade_id: str = field(default_factory=lambda: str(uuid.uuid4())) + + +@dataclass +class PitGame(Game): + rules: Rules = Rules( + title="Pit", + summary="""Pit is a commodity trading game where players engage in trading to accumulate points and emerge as the winner. + The game involves commodity cards representing various goods, with each card holding a specific point value. + Players shout out their trade offers, attempting to negotiate deals with others to acquire valuable commodities. + Additionally, Bull and Bear cards periodically influence the market conditions, either boosting or decreasing commodity values. + The game continues with trading phases, market fluctuations, and scoring until a player or team reaches the agreed-upon point total, + declaring them the victor in the spirited world of commodity trading.""", + additional_details=None, + ) + id: str = "pit" + agent_virtual_players: Dict[int, List[int]] = field(default_factory=dict) + virtual_player_hands: Dict[int, Dict[str, int]] = field(default_factory=dict) + virtual_player_scores: Dict[int, float] = field(default_factory=dict) + agents: List[Agent] = field(default_factory=list) + pending_trades: List[TradeProposal] = field(default_factory=list) + last_trade_outcome: str = "" + + def __post_init__(self): + self.commodities = [ + Commodity("Barley", 85.0), + Commodity("Corn", 75.0), + Commodity("Soyabeans", 55.0), + Commodity("Wheat", 100.0), + Commodity("Bull", 0.0), + Commodity("Bear", 0.0), + ] + self.scores = [] + self.round_number = 0 + self.pending_trades = [] + self.max_possible = 6 # actual value: 9 + self.winning_score = 500 # actual value: 500 + + def setup_virtual_players(self): + virtual_player_id = 0 + for agent in self.agents: + self.agent_virtual_players[agent.agent_id] = [ + virtual_player_id, + virtual_player_id + 1, + ] + for vp_id in self.agent_virtual_players[agent.agent_id]: + self.virtual_player_scores[vp_id] = 0.0 + self.virtual_player_hands[vp_id] = { + commodity.name: 0 for commodity in self.commodities + } + virtual_player_id += 2 + + def shuffle_cards(self): + deck = [ + commodity.name for commodity in self.commodities[:-2] for _ in range(9) + ] + ["Bull", "Bear"] + + random.shuffle(deck) + + for vp_id in self.virtual_player_hands.keys(): + self.virtual_player_hands[vp_id] = { + commodity.name: 0 for commodity in self.commodities + } + + vp_ids = list(self.virtual_player_hands.keys()) + for card in deck: + vp_id = vp_ids.pop(0) + self.virtual_player_hands[vp_id][card] += 1 + vp_ids.append(vp_id) + + def init_game( + self, + agent_1_cls: Agent, + agent_2_cls: Agent, + ): + agent_1 = agent_1_cls( + team_id=0, + agent_id=1, + agent_type_id=agent_1_cls.agent_type_id, + **self.agent_1_kwargs, + ) + agent_2 = agent_2_cls( + team_id=1, + agent_id=2, + agent_type_id=agent_2_cls.agent_type_id, + **self.agent_2_kwargs, + ) + self.agents = [agent_1, agent_2] + self.scores = [0.0] * len(self.agents) + self.setup_virtual_players() + + def propose_trade(self, proposer_id, offered_commodities): + proposer_hand = self.virtual_player_hands[ + self.agent_virtual_players[proposer_id][0] + ] + + if all( + proposer_hand[commodity] >= quantity + for commodity, quantity in offered_commodities.items() + ): + if self.show_state: + print( + f"Proposer (Agent {proposer_id}) Hand before trade:", proposer_hand + ) + self.pending_trades = [ + proposal + for proposal in self.pending_trades + if proposal.proposer_id != proposer_id + ] + proposal = TradeProposal(proposer_id, offered_commodities) + self.pending_trades.append(proposal) + trade_details = " + ".join( + f"{quantity} {commodity}" + for commodity, quantity in offered_commodities.items() + ) + if self.show_state: + print(f"Agent {proposer_id} proposed a trade offering {trade_details}.") + else: + if self.show_state: + print(f"Agent {proposer_id} does not have enough commodities to trade.") + + def respond_to_trade(self, responder_id, trade_id, accept, response_commodities): + proposal = next( + (p for p in self.pending_trades if p.trade_id == trade_id), None + ) + + if proposal and accept: + responder_hand = self.virtual_player_hands[ + self.agent_virtual_players[responder_id][0] + ] + + has_enough_of_each_commodity = all( + responder_hand.get(commodity, 0) >= quantity + for commodity, quantity in response_commodities.items() + ) + + if has_enough_of_each_commodity: + proposal.status = "accepted" + self.execute_trade(proposal, responder_id, response_commodities) + else: + if self.show_state: + print( + f"Agent {responder_id} does not have enough commodities to respond to the trade." + ) + + self.pending_trades = [ + p for p in self.pending_trades if p.trade_id != trade_id + ] + elif proposal: + if self.show_state: + print(f"Agent {responder_id} rejects trade proposal {trade_id}.") + self.pending_trades = [ + p for p in self.pending_trades if p.trade_id != trade_id + ] + + def execute_trade(self, proposal, responding_agent_id, response_commodities): + if proposal.status == "accepted": + proposer_vp_ids = self.agent_virtual_players[proposal.proposer_id] + responder_vp_ids = self.agent_virtual_players[responding_agent_id] + + proposer_trade_details = " + ".join( + f"{quantity} {commodity}" + for commodity, quantity in proposal.offered_commodities.items() + ) + + for vp_id in proposer_vp_ids: + for commodity, quantity in proposal.offered_commodities.items(): + self.virtual_player_hands[vp_id][commodity] -= quantity + + for vp_id in responder_vp_ids: + for commodity, quantity in proposal.offered_commodities.items(): + self.virtual_player_hands[vp_id][commodity] += quantity + + responder_trade_details = " + ".join( + f"{quantity} {commodity}" + for commodity, quantity in response_commodities.items() + ) + + for commodity, quantity in response_commodities.items(): + for vp_id in responder_vp_ids: + self.virtual_player_hands[vp_id][commodity] -= quantity + for vp_id in proposer_vp_ids: + self.virtual_player_hands[vp_id][commodity] += quantity + + if self.show_state: + print( + f"Trade completed: Agent {proposal.proposer_id} offered {proposer_trade_details} and Agent {responding_agent_id} offered back {responder_trade_details}." + ) + + self.last_trade_outcome = f"Trade completed between Agent {proposal.proposer_id} (offered {proposer_trade_details}) and Agent {responding_agent_id} (offered back {responder_trade_details})." + self.check_corners_update_score() + + def check_corners_update_score(self): + for vp_id, hand in self.virtual_player_hands.items(): + bull_card = hand.get("Bull", 0) + bear_card = hand.get("Bear", 0) + for commodity, count in hand.items(): + is_corner = count == self.max_possible or ( + count == self.max_possible - 1 and bull_card + ) + if is_corner: + commodity_obj = next( + (c for c in self.commodities if c.name == commodity), None + ) + if commodity_obj: + score = commodity_obj.value + if bull_card and count == self.max_possible - 1: + if self.show_state: + print( + f"Virtual Player {vp_id} has a Bull Corner on {commodity}." + ) + if bull_card and count == self.max_possible: + score *= 2 # Double Bull Corner + if self.show_state: + print( + f"Virtual Player {vp_id} has a Double Bull Corner on {commodity}." + ) + if bear_card: + score -= 20 * bear_card # Penalty for holding Bear card + + self.virtual_player_scores[vp_id] += score + if self.show_state: + print( + f"Virtual Player {vp_id} has cornered the market on {commodity}. Score updated." + ) + + for agent_id, vp_ids in self.agent_virtual_players.items(): + if vp_id in vp_ids: + self.scores[ + self.agents.index( + next( + agent + for agent in self.agents + if agent.agent_id == agent_id + ) + ) + ] = sum(self.virtual_player_scores[vp] for vp in vp_ids) + break + self.shuffle_cards() + + def get_observation(self, agent: Agent) -> Tuple[Observation, AvailableActions]: + agent_hand = self.virtual_player_hands[ + self.agent_virtual_players[agent.agent_id][0] + ] + hand_description = ", ".join( + f"{count} x {commodity}" + for commodity, count in agent_hand.items() + if count > 0 + ) + + available_actions = AvailableActions( + instructions="Choose commodities and quantities to trade, respond to pending trades, or indicate which commodity you're willing to trade.", + predefined={}, + openended={}, + ) + + for commodity in self.commodities: + for quantity in range(1, 5): + action_id = f"Offer_{commodity.name}_{quantity}" + action_description = f"Offer {quantity} {commodity.name}" + available_actions.predefined[action_id] = action_description + + pending_trade_descriptions = [] + for i, proposal in enumerate(self.pending_trades, 1): + if agent.agent_id != proposal.proposer_id and proposal.status == "pending": + offered_description = ", ".join( + f"{quantity} {commodity}" + for commodity, quantity in proposal.offered_commodities.items() + ) + trade_description = f"Trade {i} offers {offered_description}" + pending_trade_descriptions.append(trade_description) + + for commodity, quantity in agent_hand.items(): + if quantity > 0: + for offer_quantity in range(1, quantity + 1): + accept_action_id = ( + f"Accept_{i}_{commodity}_{offer_quantity}" + ) + available_actions.predefined[accept_action_id] = ( + f"Accept trade {i}, offering {offer_quantity} {commodity}" + ) + + observation_text = f"Agent {agent.agent_id}, it's your turn. Your hand: {hand_description}. {' '.join(pending_trade_descriptions)}" + return Observation(text=observation_text), available_actions + + def update(self, action: Action, available_actions: AvailableActions, agent: Agent): + if action.action_id not in available_actions.predefined.keys(): + action_id = random.choice(list(available_actions.predefined.keys())) + action = Action(action_id) + + action_parts = action.action_id.split("_") + if "Accept" in action.action_id or "Reject" in action.action_id: + trade_index = int(action_parts[1]) - 1 + proposal = self.pending_trades[trade_index] + + if "Accept" in action.action_id and agent.agent_id != proposal.proposer_id: + if len(action_parts) >= 4: + responding_commodity = action_parts[2] + quantity = int(action_parts[3]) + response_commodities = {responding_commodity: quantity} + self.respond_to_trade( + agent.agent_id, proposal.trade_id, True, response_commodities + ) + else: + if self.show_state: + print( + f"Action ID {action.action_id} does not contain enough information for trade acceptance." + ) + elif "Reject" in action.action_id: + self.respond_to_trade(agent.agent_id, proposal.trade_id, False) + + elif len(action_parts) == 3 and action_parts[0] == "Offer": + offered_commodity = action_parts[1] + quantity = int(action_parts[2]) + self.propose_trade(agent.agent_id, {offered_commodity: quantity}) + + else: + if self.show_state: + print("Invalid action received.") + + def play(self) -> Tuple[float, float]: + self.shuffle_cards() + + while not self.game_is_over: + for current_agent in self.agents: + self.round_number += 1 + + observation, available_actions = self.get_observation(current_agent) + action = current_agent.take_action( + self.rules, observation, available_actions, show_state=True + ) + + self.update(action, available_actions, current_agent) + self.check_corners_update_score() + + if self.show_state: + print(f"End of round {self.round_number}. Scores: {self.scores}") + + if any(score >= self.winning_score for score in self.scores): + self.game_is_over = True + break + + winning_agent_id = self.agents[self.scores.index(max(self.scores))].agent_id + print(f"Agent {winning_agent_id} won with a score of {max(self.scores)}.") + total_score = sum(self.scores) + normalized_scores = [score / total_score for score in self.scores] + return tuple(normalized_scores) \ No newline at end of file diff --git a/games/pit/test.py b/games/pit/test.py new file mode 100644 index 0000000..6093954 --- /dev/null +++ b/games/pit/test.py @@ -0,0 +1,54 @@ +# Small script to test game in VSCode + +import random + +# import time +from pit import PitGame +from gpt import OpenAITextAgent +from dataclasses import dataclass, field +from classes import Action, Agent, AvailableActions, Observation, Rules + + +@dataclass +class RandomAgent(Agent): + agent_type_id: str = "random" + + def take_action( + self, + rules: Rules, + observation: Observation, + available_actions: AvailableActions, + show_state: bool, + ): + actions = list(available_actions.predefined.keys()) + try: + action_id = random.choice(actions) + except IndexError: + raise Exception("Agent was given zero available actions.") + # time.sleep(1.5) + return Action(action_id=action_id) + + +agents = [ + OpenAITextAgent( + openai_model="gpt-4-1106-preview", agent_id=1, team_id=0, agent_type_id="agent" + ), + OpenAITextAgent( + openai_model="gpt-4-1106-preview", agent_id=2, team_id=1, agent_type_id="agent" + ), + # RandomAgent(agent_id=1, team_id=0, agent_type_id="agent"), + # RandomAgent(agent_id=2, team_id=1, agent_type_id="agent"), +] + + +if __name__ == "__main__": + pit_game = PitGame() + + pit_game.init_game( + agent_1_cls=agents[0], + agent_2_cls=agents[1], + ) + + scores = pit_game.play() + + print("Final scores:", scores) diff --git a/games/santorini/readme.md b/games/santorini/readme.md new file mode 100644 index 0000000..a35e5c7 --- /dev/null +++ b/games/santorini/readme.md @@ -0,0 +1,25 @@ +# Santorini + +Santorini is a strategy game in which two players take turns moving one of their two pawns on a five by five grid and building blocks on the grid. The game ends when one of the players moves a pawn to a square that has been built three blocks high or when one of the players cannot make a move. Read the full ruleset in [this PDF](./rulebook.pdf). + +Note that in this implementation, the pawn that each player moves and builds with alternates on each of the player's turns. The player doesn't choose which pawn to use. + +By default, the terminal output board is colored to more easily visualize the pawns and the towers. Player 1's pawns are blue and cyan, and Player 2's pawns are red and magenta. To turn off coloring, set `colored_output` to `False`. + +## Board representation + +The board is presented to the agents as text in the following format: + +``` +0. 0. 1. 3. 1. +4. 4. 4. 1Y 2. +2. 4. 3. 4. 3. +2. 3B 4. 1. 0X +4. 4. 2A 4. 0. +``` + +Each cell of the board is represented by a pair of characters, the first representing the level of the cell (0 to 4) and the second representing any pawns on the cell (`A`, `B`, `X`, or `Y`). If there is no pawn on a cell, the second character is a period. + +This representation should be tokenized correctly by GPT-based agents: + +![](./tokenization.png) diff --git a/games/santorini/rulebook.pdf b/games/santorini/rulebook.pdf new file mode 100644 index 0000000..c6f1158 Binary files /dev/null and b/games/santorini/rulebook.pdf differ diff --git a/games/santorini/santorini.py b/games/santorini/santorini.py new file mode 100644 index 0000000..b03a7d0 --- /dev/null +++ b/games/santorini/santorini.py @@ -0,0 +1,358 @@ +import ast +import random +from dataclasses import dataclass +from typing import Dict, List, Tuple + +from colorama import Back, Fore, Style +from santorinai.board import Board, Pawn + +from api.classes import Action, Agent, AvailableActions, Game, Observation, Rules + + +@dataclass +class Play: + move: Tuple[int, int] + build: Tuple[int, int] + + +@dataclass +class BoardSquare: + """Used to represent a square on the board when converting the board into a string.""" + + level: int + pawn_letter: str + + +@dataclass +class RelativePosition: + name: str + position: Tuple[int, int] + + +@dataclass +class Santorini(Game): + rules: Rules = Rules( + title="Santorini", + summary='Win by moving one of your pawns to the third level of the board or forcing your opponent to be unable to finish their turn. The game is played on a five by five grid of squares, and each player controls two pawns. Play alternates between the players, starting with player 1. The pawn that a player plays with alternates during each of their turns: for example, player 1 plays pawn A on their first turn, pawn B on their next turn, then pawn A, and so on. Blocks can be placed on squares on the board up to four blocks high, creating four possible height levels.\n\nThe board begins with no blocks placed, so every square begins at level 0. Before the game starts, each of the players takes turns placing each of their pawns on the board. A square is occupied if a pawn is on it.\n\nEach turn consists of two stages: the "move" stage and the "build" stage. During the move stage, the player moves their pawn by one square (horizontally, vertically, or diagonally). They cannot move their pawn onto a square that is occupied by another pawn, more than one level higher than the pawn, or at level 4. They can move a pawn any number of levels down, to the same level, or one level higher, but not more than one level higher and not to level 4.\n\nDuring the build stage, the player must select an unoccupied square adjacent to the pawn they moved during the move stage and place a block on it. They can place a block onto an unoccupied square at any level less than 4. Once a square has been built to level 4, it is "complete", meaning pawns cannot move to it and blocks cannot be placed on it. The player instantly wins if they move their pawn onto a square at level 3 or if they force their opponent to not be able to finish their turn.', + additional_details=None, + ) + id: str = "santorini" + agents: List[Agent] = None + show_state: bool = False + game_is_over: bool = False + board: Board = None + colored_output: bool = True + DIRECTION_NAME_MATRIX = [ + ["northwest", "north", "northeast"], + ["west", None, "east"], + ["southwest", "south", "southeast"], + ] + + def init_game(self, agent_1: Agent, agent_2: Agent): + self.agents = [ + agent_1(team_id=1, agent_id=0, **self.agent_1_kwargs), + agent_2(team_id=2, agent_id=1, **self.agent_2_kwargs), + ] + self.board = Board(2) + + def pawn_letter(self, pawn: Pawn) -> str: + letter_mapping = { + 1: "A", + 2: "X", + 3: "B", + 4: "Y", + } + return letter_mapping[pawn.number] + + def get_pawns(self, agent: Agent) -> List[Pawn]: + return [ + pawn for pawn in self.board.pawns if pawn.player_number == agent.team_id + ] + + def get_opponent_pawns(self, agent: Agent) -> List[Pawn]: + return [ + pawn for pawn in self.board.pawns if pawn.player_number != agent.team_id + ] + + def get_board_matrix(self) -> List[List[BoardSquare]]: + """Return a matrix representation of the board, where each square is represented as a list of two elements: the first element is the level of the square, and the second element is the letter of the pawn that is on the square, or "." if the square is not occupied.""" + + original_board = self.board.copy() + board_matrix: List[List[BoardSquare]] = [ + [ + BoardSquare(level=original_board.board[i][j], pawn_letter=".") + for j in range(5) + ] + for i in range(5) + ] + + for pawn in original_board.pawns: + position = pawn.pos + if position is None or position[0] is None or position[1] is None: + continue + letter = self.pawn_letter(pawn) + board_matrix[position[0]][position[1]].pawn_letter = letter + + return board_matrix + + def board_string_for_agent(self) -> str: + """Return a string representation of the board to be displayed to an agent.""" + + board_matrix = self.get_board_matrix() + board_string = "" + + for i in range(len(board_matrix)): + for square in board_matrix[i]: + board_string += str(square.level) + square.pawn_letter + " " + board_string += "\n" if i != len(board_matrix) - 1 else "" + + return board_string + + def board_string_for_user(self) -> str: + """Return a string representation of the board to be displayed to a human user in the terminal.""" + # TODO: replace board_string_for_user with a print board for user function + level_color_mapping = { + 0: Back.BLACK, + 1: Back.BLUE, + 2: Back.GREEN, + 3: Back.CYAN, + 4: Back.WHITE, + } + pawn_color_mapping = { + "A": Fore.BLACK, + "B": Fore.BLACK, + "X": Fore.WHITE, + "Y": Fore.WHITE, + " ": "", + } + + board_matrix = self.get_board_matrix() + board_string = "" + + for i in range(len(board_matrix)): + for square in board_matrix[i]: + pawn_letter = square.pawn_letter + if pawn_letter == ".": + pawn_letter = " " + if self.colored_output: + board_string += ( + level_color_mapping[square.level] + + pawn_color_mapping[pawn_letter] + + pawn_letter + + Style.RESET_ALL + ) + else: + board_string += pawn_letter + board_string += "\n" if i != len(board_matrix) - 1 else "" + + board_string += "\n" + + return board_string + + def get_general_observation(self, agent: Agent) -> Observation: + board_string = self.board_string_for_agent() + pawns = self.get_pawns(agent) + pawn_letters = [self.pawn_letter(pawn) for pawn in pawns] + opponent_pawns = self.get_opponent_pawns(agent) + opponent_pawn_letters = [self.pawn_letter(pawn) for pawn in opponent_pawns] + + observation_text = f"Player {agent.team_id}, it is your turn. You control two pawns, represented as the letters {pawn_letters[0]} and {pawn_letters[1]}, and your opponent controls pawns {opponent_pawn_letters[0]} and {opponent_pawn_letters[1]}. Each non-occupied square is represented as a digit corresponding to what level it is, from 0 to 4. Here is the board:\n\n{board_string}" + return Observation(text=observation_text) + + def relative_direction_name( + self, first_position: Tuple[int, int], second_position: Tuple[int, int] + ) -> str: + """Given a pawn and a position adjacent to the pawn, return the relative direction name. For example, if a pawn is at (0, 0), the position (0, 1) returns 'east' and (1, 1), returns 'southeast'.""" + relative_position = ( + second_position[0] - first_position[0], + second_position[1] - first_position[1], + ) + # matrix_lookup = map(lambda x: x + 1, relative_position) + matrix_lookup = (relative_position[0] + 1, relative_position[1] + 1) + return self.DIRECTION_NAME_MATRIX[matrix_lookup[0]][matrix_lookup[1]] + + def relative_position(self, pawn: Pawn, direction: str) -> Tuple[int, int]: + """Given a pawn and a direction name (e.g. "north"), return the position adjacent to the pawn in that direction.""" + # try: + # assert direction in self.DIRECTION_NAME_MAPPING + # except AssertionError: + # raise Exception(f"Invalid direction: {direction}") + # x_offset, y_offset = self.DIRECTION_NAME_MAPPING[direction] + # pawn_position = pawn.pos + # return (pawn_position[0] + x_offset, pawn_position[1] + y_offset) + + matrix_position = (1, 1) + for i, row in enumerate(self.DIRECTION_NAME_MATRIX): + if direction in row: + matrix_position = (i, row.index(direction)) + break + relative_position = map(lambda x: x - 1, matrix_position) + + if relative_position == (0, 0): + raise ValueError(f"Direction '{direction}' not found in matrix.") + + return relative_position + + def get_move_build_observation( + self, agent: Agent + ) -> Tuple[Observation, AvailableActions, Dict[str, Play]]: + observation = self.get_general_observation(agent) + + # TODO: double check that tokenization makes sense (ints are separate tokens) + + pawn = self.board.get_playing_pawn() + + if pawn.player_number != agent.team_id: + self.display_message( + "The agent's team ID is out of sync with the current pawn's player number, which means that the agent ran out of options and lost." + ) + return {}, {}, {} + + pawn_letter = self.pawn_letter(pawn) + + available_actions = AvailableActions( + instructions=f"For this turn, you are playing with pawn {pawn_letter}. Pick which direction you want to move pawn {pawn_letter} and where you want to build a block relative to it after it has moved.", + predefined={}, + openended={}, + ) + possible_plays: List[Tuple[Tuple[int, int], Tuple[int, int]]] = ( + self.board.get_possible_movement_and_building_positions(pawn) + ) + action_name_mapping: Dict[str, Play] = {} + for play in possible_plays: + (move, build) = play + move_direction = self.relative_direction_name(pawn.pos, move) + build_direction = self.relative_direction_name(move, build) + action_id = f"Move {move_direction}, build {build_direction}" + action_name_mapping[action_id] = play + action_description = f"Move pawn {pawn_letter} from {pawn.pos} to {play} and then build a block on {build}." + available_actions.predefined[action_id] = action_description + + return observation, available_actions, action_name_mapping + + def get_pawn_placement_observation( + self, agent: Agent + ) -> Tuple[Observation, AvailableActions]: + observation = self.get_general_observation(agent) + + pawn = self.board.get_playing_pawn() + pawn_letter = self.pawn_letter(pawn) + + available_actions = AvailableActions( + instructions=f'You are in the "initial pawn placement" phase of the game. Decide where you want to place pawn {pawn_letter}.', + predefined={}, + openended={}, + ) + + possible_placement_positions = self.board.get_possible_movement_positions(pawn) + for position in possible_placement_positions: + action_id = f"{position}" + action_description = f"Place pawn {pawn_letter} at {position}." + available_actions.predefined[action_id] = action_description + + return observation, available_actions + + def display_message(self, message: str): + """Prints a message to the console.""" + if self.show_state: + print(message) + + def place_pawn(self, action: Action, available_actions: AvailableActions): + # If the agent chose an invalid action, replace it with a random action instead. + if action.action_id not in available_actions.predefined.keys(): + action = random.choice(list(available_actions.predefined.keys())) + + move: Tuple = ast.literal_eval(action.action_id) + + pawn_letter = self.pawn_letter(self.board.get_playing_pawn()) + player_number = self.board.get_playing_pawn().player_number + + pawn_placed, pawn_placement_error = self.board.place_pawn(move) + if not pawn_placed: + raise Exception(pawn_placement_error) + + self.display_message( + f"Player {player_number} placed pawn {pawn_letter} at {move}." + ) + self.display_message(self.board_string_for_user()) + + def play_turn( + self, + action: Action, + available_actions: AvailableActions, + action_name_mapping: Dict[str, Play], + ): + # If the agent chose an invalid action, replace it with a random action instead. + if action.action_id not in available_actions.predefined.keys(): + action_id = random.choice(list(available_actions.predefined.keys())) + action = Action(action_id) + + play = action_name_mapping[action.action_id] + move, build = play + pawn_letter = self.pawn_letter(self.board.get_playing_pawn()) + player_number = self.board.get_playing_pawn().player_number + + self.board.play_move(move, build) + + self.display_message( + f"Player {player_number} moved pawn {pawn_letter} to {move} and built at {build}." + ) + self.display_message(self.board_string_for_user()) + + def play(self) -> Tuple[float, float]: + """Return the scores for agent_1 and agent_2 after the game is finished.""" + # Returns 1 for the winning team and 0 for the losing team. + # There are no draws in Santorini. + + self.display_message("Placing the pawns.\n") + for i in range(2): + for agent in self.agents: + observation, available_actions = self.get_pawn_placement_observation( + agent + ) + action = agent.take_action( + self.rules, + observation, + available_actions, + show_state=self.show_state, + ) + self.place_pawn(action, available_actions) + + self.display_message("Playing the game.\n") + while not self.board.is_game_over(): + for agent in self.agents: + ( + observation, + available_actions, + action_name_mapping, + ) = self.get_move_build_observation(agent) + if available_actions == {} or available_actions.predefined == {}: + self.display_message( + f"Player {agent.team_id} ran out of options and loses." + ) + return (0.0, 1.0) if agent.team_id == 1 else (1.0, 0.0) + current_pawn = self.board.get_playing_pawn() + try: + assert current_pawn.player_number == agent.team_id + except AssertionError: + raise Exception( + f"Agent {agent.team_id} is trying to take a turn when it is not their turn." + ) + + action = agent.take_action( + self.rules, + observation, + available_actions, + show_state=self.show_state, + ) + + self.play_turn(action, available_actions, action_name_mapping) + + self.display_message("Game over.") + + winner_number = self.board.winner_player_number + if winner_number is None: + raise Exception("The game is over, but there is no winner.") + else: + self.display_message(f"Player {winner_number} wins!") + return (1.0, 0.0) if winner_number == 1 else (0.0, 1.0) diff --git a/games/santorini/tokenization.png b/games/santorini/tokenization.png new file mode 100644 index 0000000..d08d5e2 Binary files /dev/null and b/games/santorini/tokenization.png differ diff --git a/games/two_rooms_and_a_boom/__pycache__/two_rooms.cpython-310.pyc b/games/two_rooms_and_a_boom/__pycache__/two_rooms.cpython-310.pyc index cdf6ae3..b971156 100644 Binary files a/games/two_rooms_and_a_boom/__pycache__/two_rooms.cpython-310.pyc and b/games/two_rooms_and_a_boom/__pycache__/two_rooms.cpython-310.pyc differ diff --git a/generate_all_results.py b/generate_all_results.py new file mode 100644 index 0000000..5779a58 --- /dev/null +++ b/generate_all_results.py @@ -0,0 +1,330 @@ +from api.util import load_json +from collections import defaultdict +import random +import seaborn as sns +import functools + +random.seed(0) + +import choix +import matplotlib.pyplot as plt +import numpy as np +from rating import * + +import pandas as pd + +complete_matches = { + g: get_matches(g) for g in games +} | {"all": get_matches()} +complete_bootstrapped_params = { + g: bootstrap_params(m) for g, m in complete_matches.items() +} | {"all": bootstrap_params(get_matches())} + +################################################################################ +################################################################################ +################################################################################ + + + +sns.set(style='whitegrid') +sns.set_context("paper", font_scale=1.5) + +bootstrapped_params = complete_bootstrapped_params["all"] +ratings = bootstrapped_params.mean(0) + +sorted_indices = np.argsort(ratings) +sorted_data = bootstrapped_params[:, sorted_indices] +sorted_labels = [players[i] for i in sorted_indices] + +fig, ax = plt.subplots(constrained_layout=True, dpi=300) +sns.boxplot(data=sorted_data, whis=(5, 95), fliersize=0, ax=ax) +ax.set_ylabel("Rating") +ax.set_xticks(ticks=range(len(players)), labels=sorted_labels) +#ax.tick_params(axis='x', rotation=30) + +plt.savefig("figures/overall_rating.png") + +################################################################################ + +fig, ax = plt.subplots(constrained_layout=True, dpi=300) + +matrix = np.zeros((n_players, n_players)) +for i in range(n_players): + for j in range(n_players): + matrix[i, j] = choix.probabilities([i, j], ratings)[0] + +sns.heatmap(matrix, ax=ax, annot=True, xticklabels=players, yticklabels=players, fmt=".2f") +#ax.set_title("Win probabilities") +ax.set_ylabel("Probability this agent...") +ax.set_xlabel("... beats this agent") +ax.tick_params(axis='x', rotation=30) +ax.tick_params(axis='y', rotation=0) +ax.invert_yaxis() + +plt.savefig("figures/overall_probabilities.png") + +################################################################################ + +fig, ax = plt.subplots(constrained_layout=True, dpi=300) + +n_games = defaultdict(int) +counts = np.array([0] * n_players) +for match in complete_matches["all"]: + agents = list(match.keys())[1:] + counts[players.index(agents[0])] += 1 + counts[players.index(agents[1])] += 1 + +sorted_indices = np.argsort(counts) +sorted_players = [players[i] for i in sorted_indices] +sorted_counts = counts[sorted_indices] +sns.barplot(x=sorted_players, y=sorted_counts, ax=ax) +#ax.tick_params(axis='x', rotation=30) +#ax.set_ylabel("Agent") +#ax.set_xlabel("Number of matches collected") + +plt.savefig("figures/num_matches_per_agent.png") + +################################################################################ + +fig, ax = plt.subplots(constrained_layout=True, dpi=300) + +n_games = defaultdict(int) +for match in complete_matches["all"]: + game = match["game"] + n_games[game] += 1 + +games, counts = zip(*list(n_games.items())) +counts = np.array(counts) +sorted_indices = np.argsort(counts) +sorted_games = [games[i] for i in sorted_indices] +sorted_counts = counts[sorted_indices] +sns.barplot(x=[shorter_names[g] for g in sorted_games], y=sorted_counts, ax=ax) +labels = ax.get_xticklabels() +#plt.setp(labels, rotation=45, ha="right", rotation_mode="anchor") +#ax.tick_params(axis='x', rotation=30) +#ax.set_ylabel("Game") +#ax.set_xlabel("Number of matches collected") + +plt.savefig("figures/num_matches_per_game.png") + +################################################################################ +sns.set_context("paper", font_scale=2) +categories = games +N = len(categories) + +all_ratings = { + c: complete_bootstrapped_params[c].mean(0) for c in categories +} +for game, ratings in all_ratings.items(): + expd = np.exp(ratings) + maxd = np.max(expd) + all_ratings[game] = expd / maxd + +x, y, hue = [], [], [] +for category in categories: + ratings = all_ratings[category] + for agent_idx, score in enumerate(ratings): + x.append(shorter_names[category]) + y.append(score) + hue.append(players[agent_idx]) + +x = np.array(x) +y = np.array(y) +hue = np.array(hue) + +fig, ax = plt.subplots(figsize=(10, 7), constrained_layout=True, dpi=300) +sns.scatterplot(x=x, y=y, hue=hue, style=hue, palette='bright', s=300, ax=ax) +#ax.set_ylim(-2.5, 3) +ax.set_ylabel("Proportional rating") +#plt.xticks(rotation=45, ha="right", rotation_mode="anchor") +plt.savefig("figures/rating_scatter.png") + +################################################################################ + +ratings_df = pd.DataFrame(index=players, columns=["Overall"] + [shorter_names[g] for g in games]) +scores_df = pd.DataFrame(index=players, columns=["Overall"] + [shorter_names[g] for g in games]) + +header = "\\begin{tabular}{lcccccccccc}\n\\toprule\nAgent & & \\multicolumn{9}{c}{Score} \\\\\n\\cmidrule(lr){2-11}\n & Overall & ALS & ARC & AYT & CN & HV & PT & SN & TRB & SB \\\\\n" + +def create_bold_underline_formatter(column): + sorted_values = column.sort_values(ascending=False).unique() + if len(sorted_values) >= 2: + max_value = sorted_values[0] + second_max_value = sorted_values[1] + else: + max_value = sorted_values[0] + second_max_value = None + + def formatter(x): + formatted_x = f"{x:.2f}" + if x == max_value: + return f"\\textbf{{{formatted_x}}}" + elif x == second_max_value: + return f"\\underline{{{formatted_x}}}" + return formatted_x + + return formatter + +def latex(name, df): + formatters = {col: create_bold_underline_formatter(df[col]) for col in df.columns} + l = df.to_latex(formatters=formatters) + l = header + "\n".join(l.split("\n")[3:]) + with open(f"figures/{name}.tex", "w") as f: + f.write(l) + +for game in games: + matches = complete_matches[game] + params = complete_bootstrapped_params[game] + game = shorter_names[game] + + n_matches = defaultdict(int) + scores = defaultdict(int) + for m in matches: + agents = list(m.keys())[1:] + n_matches[agents[0]] += 1 + n_matches[agents[1]] += 1 + scores[agents[0]] += m[agents[0]] + scores[agents[1]] += m[agents[1]] + + + ratings = params.mean(0) + ratings_df.loc[:, game] = ratings + for agent in players: + scores_df.loc[agent, game] = scores[agent] / n_matches[agent] if n_matches[agent] > 0 else float("nan") + +matches = complete_matches["all"] +n_matches = defaultdict(int) +scores = defaultdict(int) +for m in matches: + agents = list(m.keys())[1:] + n_matches[agents[0]] += 1 + n_matches[agents[1]] += 1 + scores[agents[0]] += m[agents[0]] + scores[agents[1]] += m[agents[1]] + +params = complete_bootstrapped_params["all"] +ratings = params.mean(0) +ratings_df.loc[:, "Overall"] = ratings +for agent in players: + scores_df.loc[agent, "Overall"] = scores[agent] / n_matches[agent] if n_matches[agent] > 0 else float("nan") + +latex("ratings_table", ratings_df) +latex("scores_table", scores_df) + +################################################################################ + +from api.util import load_json +from collections import defaultdict +import random + +import choix +import matplotlib.pyplot as plt +import numpy as np +import functools +import seaborn as sns +from rating import * + +sns.set(style='whitegrid') +sns.set_context("paper", font_scale=1.5) + +def make_figures(game, matches): + + fig, axs = plt.subplots(2, 2, figsize=(9.5, 7.8), constrained_layout=True, dpi=300) + ax_nmatches = axs[0, 0] + ax_score = axs[0, 1] + ax_prob = axs[1, 0] + ax_rating = axs[1, 1] + fig.suptitle(better_names[game]) + + ################################################################################ + + n_matches = defaultdict(int) + + for match in matches: + agents = list(match.keys())[1:] + n_matches[agents[0], agents[1]] += 1 + n_matches[agents[1], agents[0]] += 1 + + matrix = np.zeros((n_players, n_players), dtype="int") + for i, player1 in enumerate(players): + for j, player2 in enumerate(players): + if player1 == player2: + continue + + matrix[i][j] = n_matches[player1, player2] + + sns.heatmap(matrix, ax=ax_nmatches, annot=True, xticklabels=players, yticklabels=players) + ax_nmatches.tick_params(axis='x', rotation=30) + ax_nmatches.tick_params(axis='y', rotation=0) + ax_nmatches.invert_yaxis() + #plt.xticks(rotation=30) + + ax_nmatches.set_title("Number of matches") + + ################################################################################ + + + wins = defaultdict(int) + for match in matches: + agents = list(match.keys())[1:] + wins[agents[0], agents[1]] += match[agents[0]] + wins[agents[1], agents[0]] += match[agents[1]] + + for (agent1, agent2), score in wins.items(): + wins[agent1, agent2] = score + + matrix = np.empty((len(players), len(players))) + matrix.fill(np.nan) + for i, player1 in enumerate(players): + for j, player2 in enumerate(players): + if player1 == player2: + continue + + matrix[i, j] = wins[player1, player2] + + sns.heatmap(matrix, ax=ax_score, annot=True, xticklabels=players, yticklabels=players) + + ax_score.set_title("Total score") + ax_score.set_ylabel("Total points this agent scored...") + ax_score.set_xlabel("... against this agent") + ax_score.tick_params(axis='x', rotation=30) + ax_score.tick_params(axis='y', rotation=0) + ax_score.invert_yaxis() + + ################################################################################ + + bootstrapped_params = complete_bootstrapped_params[game] + ratings = bootstrapped_params.mean(0) + + sorted_indices = np.argsort(ratings) + sorted_data = bootstrapped_params[:, sorted_indices] + sorted_labels = [players[i] for i in sorted_indices] + + sns.boxplot(data=sorted_data, whis=(5, 95), fliersize=0, ax=ax_rating) + ax_rating.set_ylabel("Rating") + ax_rating.set_xticks(ticks=range(len(players)), labels=sorted_labels) + ax_rating.tick_params(axis='x', rotation=30) + + ################################################################################ + + matrix = np.zeros((n_players, n_players)) + for i in range(n_players): + for j in range(n_players): + matrix[i, j] = choix.probabilities([i, j], ratings)[0] + + sns.heatmap(matrix, ax=ax_prob, annot=True, xticklabels=players, yticklabels=players, fmt=".2f") + ax_prob.set_title("Win probabilities") + ax_prob.set_ylabel("Probability this agent...") + ax_prob.set_xlabel("... beats this agent") + ax_prob.tick_params(axis='x', rotation=30) + ax_prob.tick_params(axis='y', rotation=0) + ax_prob.invert_yaxis() + + + ################################################################################ + + plt.savefig(f"figures/{game}.png") + plt.close() + +for game in games: + make_figures(game, complete_matches[game]) \ No newline at end of file diff --git a/matches.json b/matches.json index d16823b..fbaf754 100644 --- a/matches.json +++ b/matches.json @@ -1,242 +1,1389 @@ [ { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "air_land_sea", + "gpt-3": 0.8421052631578947, + "random": 0.15789473684210525 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "air_land_sea", + "gpt-3": 0.75, + "random": 0.25 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "air_land_sea", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "air_land_sea", + "gpt-3": 0.3333333333333333, + "random": 0.6666666666666666 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "air_land_sea", + "gpt-3": 0.2, + "random": 0.8 + }, + { + "game": "santorini", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3": 0.5, + "random": 0.5 + }, + { + "game": "santorini", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3": 0.5, + "random": 0.5 + }, + { + "game": "hive", + "gpt-3": 0.5, + "random": 0.5 + }, + { + "game": "santorini", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3": 0.5, + "random": 0.5 + }, + { + "game": "santorini", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-3": 0.5, + "random": 0.5 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-4": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4": 0.4, + "random": 0.6 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-4": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-4": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4": 0.7272727272727273, + "random": 0.2727272727272727 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-4": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-4": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3-cot": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3-cot": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-3-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-3-cot": 0.42857142857142855, + "random": 0.5714285714285714 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3-cot": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-3-cot": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-3-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-3-cot": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-3-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-3-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-4-cot": 0.0, + "random": 1.0 + }, + { + "game": "hive", + "gpt-4-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-4-cot": 0.0, + "random": 1.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4-cot": 0, + "random": 1 + }, + { + "game": "sea_battle", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-4-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 0, + "random": 1 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-4-cot": 0, + "random": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-4-cot": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "hive", + "gpt-4-cot": 0.5, + "random": 0.5 + }, + { + "game": "air_land_sea", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "codenames", + "gpt-3": 0.0, + "random": 1.0 + }, + { + "game": "codenames", + "gpt-3-cot": 0.0, + "random": 1.0 + }, + { + "game": "codenames", + "gpt-4-cot": 0.631578947368421, + "random": 0.3684210526315789 + }, + { + "game": "codenames", + "gpt-3": 1.0, + "random": 0.0 + }, + { + "game": "codenames", + "gpt-3-cot": 1.0, + "random": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.5, - "gpt-4": 0.5 + "game": "codenames", + "gpt-4-cot": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-3": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "codenames", + "gpt-3-cot": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "codenames", + "gpt-4-cot": 1.0, + "random": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-3": 1.0, + "random": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-3-cot": 1.0, + "random": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4-cot": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "codenames", + "gpt-3": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-3-cot": 0.3888888888888889, + "random": 0.6111111111111112 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4-cot": 0.6111111111111112, + "random": 0.3888888888888889 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4": 1.0, + "random": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4": 0.6470588235294118, + "random": 0.35294117647058826 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-4": 0.0, + "random": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 }, { - "game": "tic_tac_toe", - "random": 0.5, - "gpt-4": 0.5 + "game": "arctic_scavengers", + "gpt-3-cot": 0, + "random": 1 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "arctic_scavengers", + "gpt-4": 0, + "random": 1 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "arctic_scavengers", + "gpt-4-cot": 1, + "random": 0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "arctic_scavengers", + "gpt-3": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-3-cot": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-4-cot": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-4": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-3-cot": 0, + "random": 1 + }, + { + "game": "arctic_scavengers", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-3-cot": 1, + "random": 0 + }, + { + "game": "arctic_scavengers", + "gpt-4-cot": 1, + "random": 0 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, + "gpt-3-cot": 0.0 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, + "gpt-3-cot": 0.0 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, + "gpt-3-cot": 0.0 + }, + { + "game": "codenames", + "gpt-3": 0.0, + "gpt-3-cot": 1.0 + }, + { + "game": "codenames", + "gpt-3": 0.3125, + "gpt-3-cot": 0.6875 + }, + { + "game": "codenames", + "gpt-3": 0.42105263157894735, + "gpt-3-cot": 0.5789473684210527 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "gpt-3-cot": 0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "gpt-3-cot": 0 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "gpt-3-cot": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-3-cot": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-3-cot": 1 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-3-cot": 1 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "sea_battle", + "gpt-3": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "sea_battle", + "gpt-3": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "codenames", + "gpt-3": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, + "game": "codenames", + "gpt-3": 0.0, "gpt-4": 1.0 }, { - "game": "tic_tac_toe", - "random": 0.0, + "game": "codenames", + "gpt-3": 0.0, "gpt-4": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "two_rooms_and_a_boom", + "gpt-3": 1, + "gpt-4": 0 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "gpt-4": 1 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "gpt-4": 1 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-4": 1 }, { - "game": "tic_tac_toe", - "random": 1.0, - "gpt-4": 0.0 + "game": "are_you_the_traitor", + "gpt-3": 1, + "gpt-4": 0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-4": 1 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "sea_battle", + "gpt-3": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "sea_battle", + "gpt-3": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-3": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "codenames", + "gpt-3": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "codenames", + "gpt-3": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "codenames", + "gpt-3": 0.7857142857142857, + "gpt-4-cot": 0.21428571428571427 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "gpt-4-cot": 1 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "gpt-4-cot": 1 + }, + { + "game": "two_rooms_and_a_boom", + "gpt-3": 0, + "gpt-4-cot": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-4-cot": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 1, + "gpt-4-cot": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3": 0, + "gpt-4-cot": 1 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "sea_battle", + "gpt-3-cot": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "sea_battle", + "gpt-3-cot": 1.0, + "gpt-4": 0.0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 1.0, + "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, + "game": "codenames", + "gpt-3-cot": 0.0, "gpt-4": 1.0 }, { - "game": "tic_tac_toe", - "random": 1.0, + "game": "codenames", + "gpt-3-cot": 1.0, "gpt-4": 0.0 }, { - "game": "tic_tac_toe", - "random": 0.0, - "gpt-4": 1.0 + "game": "codenames", + "gpt-3-cot": 1.0, + "gpt-4": 0.0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 1, - "gpt-3.5": 0 + "gpt-3-cot": 1, + "gpt-4": 0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-3-cot": 1, + "gpt-4": 0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 1, - "gpt-3.5": 0 + "gpt-3-cot": 0, + "gpt-4": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "gpt-4": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "gpt-4": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "gpt-4": 0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "sea_battle", + "gpt-3-cot": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "codenames", + "gpt-3-cot": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "codenames", + "gpt-3-cot": 0.631578947368421, + "gpt-4-cot": 0.3684210526315789 + }, + { + "game": "codenames", + "gpt-3-cot": 0.3125, + "gpt-4-cot": 0.6875 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-3-cot": 1, + "gpt-4-cot": 0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-3-cot": 1, + "gpt-4-cot": 0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-3-cot": 1, + "gpt-4-cot": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 0, + "gpt-4-cot": 1 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "gpt-4-cot": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-3-cot": 1, + "gpt-4-cot": 0 + }, + { + "game": "sea_battle", + "gpt-4": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "arctic_scavengers", + "gpt-3": 0, + "gpt-3-cot": 1 + }, + { + "game": "arctic_scavengers", + "gpt-3": 1, + "gpt-3-cot": 0 + }, + { + "game": "pit", + "gpt-3": 0.45595854922279794, + "random": 0.5440414507772021 + }, + { + "game": "pit", + "gpt-3": 0.09401709401709402, + "random": 0.905982905982906 + }, + { + "game": "pit", + "gpt-3": 0.5524861878453039, + "random": 0.44751381215469616 + }, + { + "game": "pit", + "gpt-4": 0.08661417322834646, + "random": 0.9133858267716536 + }, + { + "game": "pit", + "gpt-4": 0.08943089430894309, + "random": 0.9105691056910569 + }, + { + "game": "pit", + "gpt-4": 0.41040462427745666, + "random": 0.5895953757225434 + }, + { + "game": "pit", + "gpt-4-cot": 0.2054794520547945, + "random": 0.7945205479452054 + }, + { + "game": "pit", + "gpt-4-cot": 0.6325301204819277, + "random": 0.3674698795180723 + }, + { + "game": "pit", + "gpt-4-cot": 1.0, + "random": 0.0 + }, + { + "game": "pit", + "gpt-3": 0.12195121951219512, + "random": 0.8780487804878049 + }, + { + "game": "pit", + "gpt-3": 0.2907801418439716, + "random": 0.7092198581560284 + }, + { + "game": "pit", + "gpt-3": 0.8780487804878049, + "random": 0.12195121951219512 + }, + { + "game": "pit", + "gpt-3-cot": 0.5153061224489796, + "random": 0.4846938775510204 + }, + { + "game": "pit", + "gpt-3-cot": 0.6666666666666666, + "random": 0.3333333333333333 + }, + { + "game": "pit", + "gpt-3-cot": 0.4627659574468085, + "random": 0.5372340425531915 + }, + { + "game": "arctic_scavengers", + "gpt-4-rap": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-rap": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-rap": 1, + "random": 0 + }, + { + "game": "are_you_the_traitor", + "gpt-4-rap": 1, + "random": 0 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-4-rap": 0, + "random": 1 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 1, - "gpt-3.5": 0 + "gpt-4-rap": 0, + "random": 1 }, { "game": "two_rooms_and_a_boom", - "gpt-4": 0, - "gpt-3.5": 1 + "gpt-4-rap": 1, + "random": 0 + }, + { + "game": "codenames", + "gpt-4-rap": 0.0, + "random": 1.0 + }, + { + "game": "codenames", + "gpt-4-rap": 0.75, + "random": 0.25 + }, + { + "game": "codenames", + "gpt-4-rap": 1.0, + "random": 0.0 + }, + { + "game": "santorini", + "gpt-4-rap": 0.0, + "random": 1.0 + }, + { + "game": "santorini", + "gpt-4-rap": 1.0, + "random": 0.0 + }, + { + "game": "pit", + "gpt-4-rap": 0.2625, + "random": 0.7375 + }, + + + { + "game": "codenames", + "human": 1.0, + "gpt-4-cot": 0.0 + }, + { + "game": "codenames", + "human": 0.29411764705882354, + "gpt-4-cot": 0.7058823529411765 + }, + { + "game": "codenames", + "human": 0.0, + "gpt-4-cot": 1.0 + }, + { + "game": "hive", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "hive", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "hive", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "pit", + "human": 0.7832167832167832, + "gpt-4-cot": 0.21678321678321677 + }, + { + "game": "santorini", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "santorini", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "santorini", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "sea_battle", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "sea_battle", + "human": 1.0, + "gpt-4": 0.0 + }, + { + "game": "sea_battle", + "human": 1.0, + "gpt-4": 0.0 } ] \ No newline at end of file diff --git a/rating.py b/rating.py new file mode 100644 index 0000000..981260a --- /dev/null +++ b/rating.py @@ -0,0 +1,109 @@ +from api.util import load_json +from collections import defaultdict +import random +import functools +import choix +import numpy as np + +################################################################################ +################################################################################ +################################################################################ +# The code contained in this block is copied from choix. It has been modified +# from the original to fit our data better. The original license is included. +# The MIT License (MIT) +# +# Copyright (c) 2015 Lucas Maystre +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +def lsr_pairwise(n_items, data, alpha=0.0, initial_params=None): + weights, chain = choix.lsr._init_lsr(n_items, alpha, initial_params) + for p1, p2, p1score, p2score in data: + chain[p1, p2] += float(p2score) / (weights[p1] + weights[p2]) + chain[p2, p1] += float(p1score) / (weights[p1] + weights[p2]) + chain -= np.diag(chain.sum(axis=1)) + return choix.utils.log_transform(choix.utils.statdist(chain)) + + +def ilsr_pairwise( + n_items, data, alpha=0.0, initial_params=None, max_iter=100, tol=1e-8 +): + fun = functools.partial(lsr_pairwise, n_items=n_items, data=data, alpha=alpha) + return choix.lsr._ilsr(fun, initial_params, max_iter, tol) + +################################################################################ +################################################################################ +################################################################################ + +players = ["random", "human", "gpt-3", "gpt-3-cot", "gpt-4", "gpt-4-cot", "gpt-4-rap"] +n_players = len(players) + +def get_matches(game=None): + if game: + return [m for m in load_json("matches.json") if m["game"] == game] + return load_json("matches.json") + +def get_params(matches): + wins = [] + for match in matches: + agents = list(match.keys())[1:] + if match[agents[0]] == match[agents[1]]: + continue + + i = players.index(agents[0]) + j = players.index(agents[1]) + + wins.append((i, j, match[agents[0]], match[agents[1]])) + + params = ilsr_pairwise(len(players), wins, alpha=0.001) + return params + + +def bootstrap_params(matches): + weights = defaultdict(int) + for match in matches: + weights[match["game"]] += 1 + + bootstrapped_params = np.array( + [ + get_params( + random.choices( + matches, k=len(matches), weights=[1 / weights[m["game"]] for m in matches] + ) + ) + for _ in range(1000) + ] + ) + + return bootstrapped_params + +games = ["sea_battle", "two_rooms_and_a_boom", "are_you_the_traitor", "air_land_sea", "santorini", "hive", "pit", "arctic_scavengers", "codenames"] +games.sort() + +better_names = { + "sea_battle": "Sea Battle", "two_rooms_and_a_boom": "Two Rooms and a Boom", + "are_you_the_traitor": "Are You the Traitor?", "air_land_sea": "Air, Land, and Sea", + "santorini": "Santorini", "hive": "Hive", "pit": "Pit", "arctic_scavengers": "Arctic Scavengers", + "codenames": "Codenames" +} + +shorter_names = { + "sea_battle": "SB", "two_rooms_and_a_boom": "TRB", "are_you_the_traitor": "AYT", + "air_land_sea": "ALS", "santorini": "SN", "hive": "HV", "pit": "PT", + "arctic_scavengers": "AS", "codenames": "CN" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4a6c1ac..171da7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ fire Pillow -openai \ No newline at end of file +openai +matplotlib +santorinai +colorama +choix +seaborn +jinja2 diff --git a/scripts/run_arctic_scavengers.sh b/scripts/run_arctic_scavengers.sh index 4cd450b..3a424ca 100644 --- a/scripts/run_arctic_scavengers.sh +++ b/scripts/run_arctic_scavengers.sh @@ -1,6 +1,6 @@ python3 api/play_game.py \ - --agent_1_path agents.gpt.GPT4Text \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_1_path agents.gpt.GPT4 \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.arctic_scavengers.arctic_scavengers.ArcticScavengers \ --save_results False \ --show_state True \ diff --git a/scripts/run_aytt.sh b/scripts/run_aytt.sh index 832489c..a1274cd 100755 --- a/scripts/run_aytt.sh +++ b/scripts/run_aytt.sh @@ -1,8 +1,8 @@ python api/play_game.py \ - --agent_1_path agents.gpt.ChatGPTText \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_1_path agents.gpt.GPT3 \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.are_you_the_traitor.aytt.AreYouTheTraitor \ --save_results False \ --show_state True \ --num_matches 10 -#GPT4Text +#GPT4Text diff --git a/scripts/run_two_rooms.sh b/scripts/run_two_rooms.sh index 5264530..0fae4ac 100755 --- a/scripts/run_two_rooms.sh +++ b/scripts/run_two_rooms.sh @@ -1,8 +1,8 @@ python api/play_game.py \ - --agent_1_path agents.gpt.ChatGPTText \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_1_path agents.gpt.GPT3 \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.two_rooms_and_a_boom.two_rooms.TwoRoomsAndaBoom \ --save_results False \ --show-state \ --num_matches 10 -#GPT4Text +#GPT4Text diff --git a/scripts/test_air_land_sea.sh b/scripts/test_air_land_sea.sh new file mode 100755 index 0000000..ac33c79 --- /dev/null +++ b/scripts/test_air_land_sea.sh @@ -0,0 +1,6 @@ +python api/play_game.py \ + --agent_1_path agents.gpt.GPT4 \ + --agent_2_path agents.gpt.GPT4 \ + --game_path games.air_land_sea.game.AirLandSea \ + --show_state \ + --num_matches 1 \ No newline at end of file diff --git a/scripts/test_air_land_sea_human.sh b/scripts/test_air_land_sea_human.sh new file mode 100755 index 0000000..90dfe9f --- /dev/null +++ b/scripts/test_air_land_sea_human.sh @@ -0,0 +1,6 @@ +python api/play_game.py \ + --agent_1_path agents.human_agent.HumanAgent \ + --agent_2_path agents.human_agent.HumanAgent \ + --game_path games.air_land_sea.game.AirLandSea \ + --show_state \ + --num_matches 1 \ No newline at end of file diff --git a/scripts/test_codenames.sh b/scripts/test_codenames.sh index 7d685c9..b18c8a1 100644 --- a/scripts/test_codenames.sh +++ b/scripts/test_codenames.sh @@ -1,6 +1,6 @@ python api/play_game.py \ - --agent_1_path agents.gpt.GPT4Text \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_1_path agents.gpt.GPT4 \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.codenames.game.CodenamesGame \ --show_state \ --num_matches 50 \ No newline at end of file diff --git a/scripts/test_hive.sh b/scripts/test_hive.sh new file mode 100755 index 0000000..e546ab9 --- /dev/null +++ b/scripts/test_hive.sh @@ -0,0 +1,6 @@ +python3.10 api/play_game.py \ + --agent_1_path agents.gpt.GPT4 \ + --agent_2_path agents.random_agent.RandomAgent \ + --game_path games.hive.game.HiveGame \ + --show_state \ + --num_matches 250 \ No newline at end of file diff --git a/scripts/test_new_agents.sh b/scripts/test_new_agents.sh index 35ce4b3..c3ee3dd 100644 --- a/scripts/test_new_agents.sh +++ b/scripts/test_new_agents.sh @@ -1,6 +1,6 @@ python api/play_game.py \ - --agent_1_path agents.gpt.ChainOfThought \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_1_path agents.gpt.GPT3CoT \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.tic_tac_toe.TicTacToe \ --show_state \ --num_matches 50 \ diff --git a/scripts/test_santorini.py b/scripts/test_santorini.py new file mode 100644 index 0000000..badf400 --- /dev/null +++ b/scripts/test_santorini.py @@ -0,0 +1,11 @@ +# This test script is easier to debug in VS Code than the original Bash script. + +from api.play_game import play_game + +play_game( + agent_1_path="agents.random_agent.RandomAgent", + agent_2_path="agents.gpt.GPT4", + game_path="games.santorini.santorini.Santorini", + show_state=True, + num_matches=3, +) diff --git a/scripts/test_santorini.sh b/scripts/test_santorini.sh new file mode 100755 index 0000000..b8e947c --- /dev/null +++ b/scripts/test_santorini.sh @@ -0,0 +1,6 @@ +python api/play_game.py \ + --agent_1_path agents.random_agent.RandomAgent \ + --agent_2_path agents.gpt.GPT4 \ + --game_path games.santorini.santorini.Santorini \ + --show_state \ + --num_matches 3 \ No newline at end of file diff --git a/scripts/test_text_agent.sh b/scripts/test_text_agent.sh index e454769..d6cc9b1 100644 --- a/scripts/test_text_agent.sh +++ b/scripts/test_text_agent.sh @@ -1,6 +1,6 @@ python api/play_game.py \ --agent_1_path agents.random_agent.RandomAgent \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.tic_tac_toe.TicTacToe \ --show_state \ --num_matches 5 \ No newline at end of file diff --git a/scripts/test_text_agent_pit.sh b/scripts/test_text_agent_pit.sh index 96e474c..1232281 100644 --- a/scripts/test_text_agent_pit.sh +++ b/scripts/test_text_agent_pit.sh @@ -1,6 +1,6 @@ python3 api/play_game.py \ --agent_1_path agents.random_agent.RandomAgent \ - --agent_2_path agents.gpt.GPT4Text \ + --agent_2_path agents.gpt.GPT4 \ --game_path games.pit.PitGame \ --show_state \ --num_matches 50 \ No newline at end of file diff --git a/total_table.tex b/total_table.tex new file mode 100644 index 0000000..d1e80dd --- /dev/null +++ b/total_table.tex @@ -0,0 +1,13 @@ +\begin{tabular}{llllllllllllllllllllllllllllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr} +\toprule +game & \multicolumn{3}{r}{sea_battle} & \multicolumn{3}{r}{two_rooms_and_a_boom} & \multicolumn{3}{r}{are_you_the_traitor} & \multicolumn{3}{r}{air_land_sea} & \multicolumn{3}{r}{santorini} & \multicolumn{3}{r}{hive} & \multicolumn{3}{r}{pit} & \multicolumn{3}{r}{arctic_scavengers} & \multicolumn{3}{r}{codenames} & gpt4 & gpt4-cot & random & gpt3 & gpt3-cot & gpt3 & random & gpt4 & gpt3-cot & gpt4-cot & rap & gpt3 & random & gpt4 & gpt3-cot & gpt4-cot & rap & gpt3 & random & gpt4 & gpt3-cot & gpt4-cot & gpt3 & random & gpt4 & gpt3-cot & gpt4-cot & rap & gpt3 & random & gpt4 & gpt3-cot & gpt4-cot & gpt3 & random & gpt4 & gpt4-cot & gpt3-cot & rap & gpt3 & random & gpt3-cot & gpt4 & gpt4-cot & rap & gpt3 & random & gpt3-cot & gpt4-cot & gpt4 & rap \\ +metric & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & # matches & P(win) & rating & ('sea_battle', '# matches') & ('sea_battle', '# matches') & ('sea_battle', '# matches') & ('sea_battle', '# matches') & ('sea_battle', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('two_rooms_and_a_boom', '# matches') & ('are_you_the_traitor', '# matches') & ('are_you_the_traitor', '# matches') & ('are_you_the_traitor', '# matches') & ('are_you_the_traitor', '# matches') & ('are_you_the_traitor', '# matches') & ('are_you_the_traitor', '# matches') & ('air_land_sea', '# matches') & ('air_land_sea', '# matches') & ('air_land_sea', '# matches') & ('air_land_sea', '# matches') & ('air_land_sea', '# matches') & ('santorini', '# matches') & ('santorini', '# matches') & ('santorini', '# matches') & ('santorini', '# matches') & ('santorini', '# matches') & ('santorini', '# matches') & ('hive', '# matches') & ('hive', '# matches') & ('hive', '# matches') & ('hive', '# matches') & ('hive', '# matches') & ('pit', '# matches') & ('pit', '# matches') & ('pit', '# matches') & ('pit', '# matches') & ('pit', '# matches') & ('pit', '# matches') & ('arctic_scavengers', '# matches') & ('arctic_scavengers', '# matches') & ('arctic_scavengers', '# matches') & ('arctic_scavengers', '# matches') & ('arctic_scavengers', '# matches') & ('arctic_scavengers', '# matches') & ('codenames', '# matches') & ('codenames', '# matches') & ('codenames', '# matches') & ('codenames', '# matches') & ('codenames', '# matches') & ('codenames', '# matches') \\ +\midrule +random & NaN & 0.176086 & 1.649150 & NaN & 0.233609 & 0.362798 & NaN & 0.001342 & -2.027480 & NaN & 0.000453 & -2.930012 & NaN & 0.102551 & -0.299480 & NaN & 0.166667 & 0.000000 & NaN & 0.196763 & 0.363366 & NaN & 0.002919 & -0.918040 & NaN & 0.184672 & 0.177621 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +gpt3 & NaN & 0.218267 & 1.863896 & NaN & 0.138223 & -0.161983 & NaN & 0.002320 & -1.480240 & NaN & 0.000758 & -2.415343 & NaN & 0.441656 & 1.160695 & NaN & 0.166667 & 0.000000 & NaN & 0.130748 & -0.045360 & NaN & 0.000836 & -2.168174 & NaN & 0.126982 & -0.196915 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +gpt3-cot & NaN & 0.082575 & 0.891879 & NaN & 0.174814 & 0.072871 & NaN & 0.038395 & 1.326045 & NaN & 0.003553 & -0.870179 & NaN & 0.156933 & 0.125980 & NaN & 0.166667 & 0.000000 & NaN & 0.239953 & 0.561812 & NaN & 0.000937 & -2.054731 & NaN & 0.214074 & 0.325360 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +gpt4 & NaN & 0.000008 & -8.361116 & NaN & 0.135981 & -0.178332 & NaN & 0.001284 & -2.072191 & NaN & 0.002218 & -1.341625 & NaN & 0.102559 & -0.299395 & NaN & 0.166667 & 0.000000 & NaN & 0.048227 & -1.042721 & NaN & 0.007509 & 0.026857 & NaN & 0.124711 & -0.214965 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +gpt4-cot & NaN & 0.353913 & 2.347226 & NaN & 0.192870 & 0.171164 & NaN & 0.007708 & -0.279596 & NaN & 0.826306 & 4.578925 & NaN & 0.101455 & -0.310225 & NaN & 0.166667 & 0.000000 & NaN & 0.314193 & 0.831374 & NaN & 0.009082 & 0.217097 & NaN & 0.081384 & -0.641785 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +rap & NaN & 0.169151 & 1.608964 & NaN & 0.124504 & -0.266517 & NaN & 0.948950 & 4.533463 & NaN & 0.166713 & 2.978233 & NaN & 0.094847 & -0.377575 & NaN & 0.166667 & 0.000000 & NaN & 0.070117 & -0.668472 & NaN & 0.978717 & 4.896990 & NaN & 0.268177 & 0.550684 & 23 & 16 & 25 & 14 & 14 & 14 & 25 & 12 & 14 & 12 & 3 & 14 & 24 & 12 & 14 & 11 & 3 & 5 & 18 & 5 & 5 & 3 & 5 & 22 & 6 & 5 & 4 & 2 & 5 & 18 & 5 & 5 & 3 & 6 & 16 & 3 & 3 & 3 & 1 & 11 & 25 & 6 & 7 & 4 & 1 & 14 & 23 & 14 & 11 & 11 & 3 \\ +\bottomrule +\end{tabular}