diff --git a/agentverse/agentverse.py b/agentverse/agentverse.py index 6307314dc..a716b810c 100644 --- a/agentverse/agentverse.py +++ b/agentverse/agentverse.py @@ -59,3 +59,7 @@ def next(self, *args, **kwargs): """Run the environment for one step and return the return message.""" return_message = asyncio.run(self.environment.step(*args, **kwargs)) return return_message + + def update_state(self, *args, **kwargs): + """Run the environment for one step and return the return message.""" + self.environment.update_state(*args, **kwargs) diff --git a/agentverse/environments/pokemon.py b/agentverse/environments/pokemon.py index 6e9af5fd5..b4bc42bf5 100644 --- a/agentverse/environments/pokemon.py +++ b/agentverse/environments/pokemon.py @@ -1,7 +1,7 @@ import asyncio -import time +import datetime import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set # from agentverse.agents.agent import Agent from agentverse.agents.conversation_agent import BaseAgent @@ -19,6 +19,7 @@ class PokemonEnvironment(BaseEnvironment): Args: agents: List of agents + locations: A dict of locations to agents within them rule: Rule for the environment max_turns: Maximum number of turns cnt_turn: Current turn number @@ -27,13 +28,16 @@ class PokemonEnvironment(BaseEnvironment): """ agents: List[BaseAgent] + locations_to_agents: Dict[str, Set[str]] + # locations_descriptions: Dict[str, str] + time: datetime.datetime = datetime.datetime(2021, 1, 1, 8, 0, 0) rule: Rule max_turns: int = 10 cnt_turn: int = 0 last_messages: List[Message] = [] rule_params: Dict = {} - def __init__(self, rule, **kwargs): + def __init__(self, rule, locations, **kwargs): rule_config = rule order_config = rule_config.get("order", {"type": "sequential"}) visibility_config = rule_config.get("visibility", {"type": "all"}) @@ -47,16 +51,70 @@ def __init__(self, rule, **kwargs): updater_config, describer_config, ) - super().__init__(rule=rule, **kwargs) + locations_to_agents = {} + # locations_descriptions = {} + locations_config = locations + for loc in locations_config: + locations_to_agents[loc["name"]] = set(loc["init_agents"]) + # locations_descriptions[loc["name"]] = loc["description"] + super().__init__( + rule=rule, + locations_to_agents=locations_to_agents, + # locations_descriptions=locations_descriptions, + **kwargs, + ) async def step( - self, player_content: str, receiver: str, receiver_id: Optional[int] = None + self, + is_player: bool = False, + player_content: str = None, + receiver: str = None, + receiver_id: Optional[int] = None, + agent_ids: Optional[List[int]] = None, ) -> List[Message]: """Run one step of the environment""" # Get the next agent index # time.sleep(8) # return [Message(content="Test", sender="May", receiver=["May"])] + if is_player: + return await self._respond_to_player(player_content, receiver, receiver_id) + else: + return await self._routine_step(agent_ids) + + async def _routine_step(self, agent_ids) -> List[Message]: + self.rule.update_visible_agents(self) + + # agent_ids = self.rule.get_next_agent_idx(self) + + # Generate current environment description + env_descriptions = self.rule.get_env_description(self) + + # Generate the next message + messages = await asyncio.gather( + *[self.agents[i].astep(env_descriptions[i]) for i in agent_ids] + ) + # messages = self.get_test_messages() + + # Some rules will select certain messages from all the messages + selected_messages = self.rule.select_message(self, messages) + + # Update the memory of the agents + self.last_messages = selected_messages + self.rule.update_memory(self) + self.print_messages(selected_messages) + + self.cnt_turn += 1 + self.time += datetime.timedelta(minutes=5) + + return selected_messages + + async def _respond_to_player( + self, + player_content: str = None, + receiver: str = None, + receiver_id: Optional[int] = None, + ) -> List[Message]: if receiver_id is None: for agent in self.agents: if agent.name == receiver: @@ -80,16 +138,29 @@ async def step( ) # Some rules will select certain messages from all the messages - selected_messages = self.rule.select_message(self, messages) + # selected_messages = self.rule.select_message(self, messages) # Update the memory of the agents - self.last_messages = [player_message, *selected_messages] + self.last_messages = [player_message, *messages] self.rule.update_memory(self) - self.print_messages(selected_messages) + self.print_messages(messages) self.cnt_turn += 1 - return selected_messages + return messages + + def update_state(self, agent_location: Dict[str, str]): + for agent_name, location in agent_location.items(): + # original_location = self.get_agent_to_location()[agent_name] + # self.locations_to_agents[original_location].remove(agent_name) + self.locations_to_agents[location].add(agent_name) + + def get_agent_to_location(self) -> Dict[str, str]: + ret = {} + for location, agent_names in self.locations_to_agents.items(): + for agent in agent_names: + ret[agent] = location + return ret def print_messages(self, messages: List[Message]) -> None: for message in messages: @@ -106,3 +177,44 @@ def reset(self) -> None: def is_done(self) -> bool: """Check if the environment is done""" return self.cnt_turn >= self.max_turns + + def get_test_messages(self) -> List[Message]: + messages = [ + Message( + content='{"to": "Birch", "action": "Speak", "text": "Hi!!!"}', + sender="May", + receiver={"May", "Birch"}, + tool_response=[], + ), + Message( + content='{"to": "May", "text": "Good morning, May! How is your research going?", "action": "Speak"}', + sender="Birch", + receiver={"May", "Birch"}, + tool_response=[], + ), + Message( + content='{"to": "Pokémon Center", "action": "MoveTo"}', + sender="Steven", + receiver={"Steven"}, + tool_response=[], + ), + Message( + content='{"to": "Shop", "last_time": "10 minutes", "action": "MoveTo"}', + sender="Maxie", + receiver={"Maxie"}, + tool_response=[], + ), + Message( + content='{"to": "Pok\\u00e9mon Center", "action": "MoveTo"}', + sender="Archie", + receiver={"Archie"}, + tool_response=[], + ), + Message( + content='{"to": "Shop", "action": "MoveTo"}', + sender="Joseph", + receiver={"Joseph"}, + tool_response=[], + ), + ] + return messages diff --git a/agentverse/environments/rules/describer/pokemon.py b/agentverse/environments/rules/describer/pokemon.py index abbfbfbd3..44d5dbecb 100644 --- a/agentverse/environments/rules/describer/pokemon.py +++ b/agentverse/environments/rules/describer/pokemon.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Dict +from copy import deepcopy from . import describer_registry as DescriberRegistry from .base import BaseDescriber @@ -14,11 +15,37 @@ class PokemonDescriber(BaseDescriber): def get_env_description( self, environment: PokemonEnvironment, - player_content: str, - time: Optional[str] = None, + player_content: str = "", ) -> List[str]: - description = "" - if time is not None: + time = environment.time + if player_content == "": + agent_to_location = environment.get_agent_to_location() + descriptions = [] + for agent in environment.agents: + description = "" + if agent.name not in agent_to_location: + # Agent is on the way to a location + descriptions.append("") + continue + location = agent_to_location[agent.name] + agents_in_same_loc = deepcopy(environment.locations_to_agents[location]) + agents_in_same_loc.remove(agent.name) + agents_in_same_loc = list(agents_in_same_loc) + description += f"It is now {time}. You are at {location}." + if len(agents_in_same_loc) == 0: + description += " There is no one else here." + elif len(agents_in_same_loc) == 1: + description += f" {agents_in_same_loc[0]} is also here." + else: + other_agents = ", ".join(agents_in_same_loc) + description += f" {other_agents} are also here." + # description += " The locations you can go to include: \n" + # for loc, dsec in environment.locations_descriptions.items(): + # description += f"{loc}: {dsec}\n" + descriptions.append(description) + return descriptions + else: + description = "" description += f"It is now {time}. Brendan is talking to you.\n" - description += f"[Brendan]: {player_content}\n" - return [description for _ in range(len(environment.agents))] + description += f"[Brendan]: {player_content}\n" + return [description for _ in range(len(environment.agents))] diff --git a/agentverse/environments/rules/selector/__init__.py b/agentverse/environments/rules/selector/__init__.py index 98a7716cd..acf1f2f78 100644 --- a/agentverse/environments/rules/selector/__init__.py +++ b/agentverse/environments/rules/selector/__init__.py @@ -7,3 +7,4 @@ from .classroom import ClassroomSelector from .sde_team import SdeTeamSelector from .sde_team_given_tests import SdeTeamGivenTestsSelector +from .pokemon import PokemonSelector diff --git a/agentverse/environments/rules/selector/pokemon.py b/agentverse/environments/rules/selector/pokemon.py new file mode 100644 index 000000000..b631aa850 --- /dev/null +++ b/agentverse/environments/rules/selector/pokemon.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List +import numpy as np +import json + +from agentverse.message import Message + +from . import selector_registry as SelectorRegistry +from .base import BaseSelector + +if TYPE_CHECKING: + from agentverse.environments import PokemonEnvironment + + +@SelectorRegistry.register("pokemon") +class PokemonSelector(BaseSelector): + """ + Selector for Pokemon environment + """ + + def select_message( + self, environment: PokemonEnvironment, messages: List[Message] + ) -> List[Message]: + valid = [] + talk_matrix = np.zeros((len(environment.agents), len(environment.agents))) + agent_to_idx = {agent.name: i for i, agent in enumerate(environment.agents)} + for i, message in enumerate(messages): + try: + content = json.loads(message.content) + except json.decoder.JSONDecodeError: + valid.append(0) + continue + if content["action"] == "Speak": + try: + if "to" not in content: + # If the model does not generate receiver, then we discard the message + valid.append(0) + elif content["to"] in agent_to_idx: + # TODO: allow talk to a list of agents + valid.append(1) + # talk_matrix[i][j] = 1 ==> i talk to j + talk_matrix[agent_to_idx[message.sender]][ + agent_to_idx[content["to"]] + ] = 1 + else: + # If the receiver is not in the environment, then we discard the message + valid.append(0) + except: + valid.append(0) + continue + elif content["action"] == "MoveTo": + # If the agent move to a location that does not exist, then we discard the message + valid.append( + "to" in content and content["to"] in environment.locations_to_agents + ) + else: + valid.append(1) + selected_messages = [] + for i, message in enumerate(messages): + content = json.loads(message.content) + sender_idx = agent_to_idx[message.sender] + if valid[i] == 0: + selected_messages.append(Message()) + continue + if content["action"] == "MoveTo": + if np.sum(talk_matrix[:, sender_idx]) > 0: + # If someone talk to this agent, then we discard the move action + selected_messages.append(Message()) + else: + selected_messages.append(message) + elif content["action"] == "Speak": + receiver_idx = agent_to_idx[content["to"]] + if talk_matrix[sender_idx][receiver_idx] == 0: + # If this agent talk to someone who also talk to this agent, and we + # select the message from this agent, then we discard the message + selected_messages.append(Message()) + continue + if np.sum(talk_matrix[receiver_idx, :]) > 0: + if talk_matrix[receiver_idx][sender_idx] == 1: + # If the receiver talk to this agent, then we randomly select one message + if sender_idx < receiver_idx: + if np.random.random() < 0.5: + selected_messages.append(message) + talk_matrix[receiver_idx][sender_idx] = 0 + else: + selected_messages.append(Message()) + talk_matrix[sender_idx][receiver_idx] = 0 + else: + print("Shouldn't happen") + else: + # If the receiver talk to other agent, we still talk to the receiver (?) + selected_messages.append(message) + else: + selected_messages.append(message) + else: + selected_messages.append(message) + return selected_messages diff --git a/agentverse/environments/rules/updater/__init__.py b/agentverse/environments/rules/updater/__init__.py index b61e66f1f..34f672f8c 100644 --- a/agentverse/environments/rules/updater/__init__.py +++ b/agentverse/environments/rules/updater/__init__.py @@ -6,3 +6,4 @@ from .basic import BasicUpdater from .classroom import ClassroomUpdater from .sde_team import SdeTeamUpdater +from .pokemon import PokemonUpdater diff --git a/agentverse/environments/rules/updater/pokemon.py b/agentverse/environments/rules/updater/pokemon.py new file mode 100644 index 000000000..c292f2a9b --- /dev/null +++ b/agentverse/environments/rules/updater/pokemon.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Tuple +import json +from copy import deepcopy + +from . import updater_registry as UpdaterRegistry +from .basic import BasicUpdater +from agentverse.message import Message + +if TYPE_CHECKING: + from agentverse.environments import PokemonEnvironment + + +@UpdaterRegistry.register("pokemon") +class PokemonUpdater(BasicUpdater): + def update_memory(self, environment: PokemonEnvironment): + for message in environment.last_messages: + if message.content == "": + continue + message = deepcopy(message) + try: + message.content = json.loads(message.content) + except json.decoder.JSONDecodeError: + continue + if message.content["action"] == "Speak": + message.content = message.content["text"] + elif message.content["action"] == "MoveTo": + if message.content["to"] in environment.locations_to_agents: + try: + orig_location = environment.get_agent_to_location()[ + message.sender + ] + environment.locations_to_agents[orig_location].remove( + message.sender + ) + except: + continue + message.content = f"[MoveTo] {message.content['to']}" + else: + message.content = f"[{message.content['action']}]" + self.add_message_to_all_agents(environment.agents, message) diff --git a/agentverse/environments/rules/visibility/__init__.py b/agentverse/environments/rules/visibility/__init__.py index 3ce2ba3c1..3f03700ce 100644 --- a/agentverse/environments/rules/visibility/__init__.py +++ b/agentverse/environments/rules/visibility/__init__.py @@ -10,3 +10,4 @@ from .oneself import OneselfVisibility from .prisoner import PrisonerVisibility from .sde_team import SdeTeamVisibility +from .pokemon import PokemonVisibility diff --git a/agentverse/environments/rules/visibility/pokemon.py b/agentverse/environments/rules/visibility/pokemon.py new file mode 100644 index 000000000..355f103b2 --- /dev/null +++ b/agentverse/environments/rules/visibility/pokemon.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from . import visibility_registry as VisibilityRegistry +from .base import BaseVisibility + +if TYPE_CHECKING: + from agentverse.environments import PokemonEnvironment + + +@VisibilityRegistry.register("pokemon") +class PokemonVisibility(BaseVisibility): + """Visibility module for Pokemon environment""" + + def update_visible_agents(self, environment: PokemonEnvironment): + for agent in environment.agents: + agent_to_location = environment.get_agent_to_location() + try: + location = agent_to_location[agent.name] + except KeyError: + # Agent is on the way to a location + continue + agents_in_same_loc = environment.locations_to_agents[location] + agent.set_receiver(agents_in_same_loc) diff --git a/agentverse/tasks/pokemon/config.yaml b/agentverse/tasks/pokemon/config.yaml index 94a867fcd..b032a501e 100644 --- a/agentverse/tasks/pokemon/config.yaml +++ b/agentverse/tasks/pokemon/config.yaml @@ -1,12 +1,34 @@ prompts: prompt: &prompt |- Now you are in the world of Pokémon Emerald, living as one of the characters. Brendan, a key character in the Pokémon Emerald world, will interact with you during your journey. Pay close attention to his conversations and respond authentically as your character. Your choices and dialogue will shape the course of your adventure. When you give your response, you should always output in the following format: - Thought: (your thought here) - Speak: (your words here) + Action: (an action name, can be Speak, MoveTo, or other actions) + Action Input: (the arguments for the action in json format, and NOTHING else) + + For example, when you would like to talk to person XX, you can output in the following format: + Thought: (your thought here) + Action: Speak + Action Input: {"to": "XX", "text": "..."} + + When you would like to do something in the current place, you can output in the following format: + Thought: (your thought here) + Action: (action_name) + Action Input: {"last_time": "xx minutes"} + + When you would like to move to another place, you can output in the following format: + Thought: (your thought here) + Action: MoveTo + Action Input: {"to": "name_of_the_place"} + + The places you can go include: + - Pokémon Center: The Pokémon Center is a place where you can get your Pokémon healed. A Pokémon Center is completely free of charge and is found in most major cities. + - Shop: The Shop is a place where you can buy the daily necessities. + - Bike Store: The Bike Store is a place where you can buy a bike. + - Park: The Park is a place where you can relax yourself. Many residents in the town like to go there to chat with others. + - Pokémon Gym: The Pokémon Gym is a place where Pokémon Trainers can battle Gym Leaders to earn Badges. These Badges serve as proof of a Trainer's skill and are necessary to enter the Pokémon League, which is a tournament where Trainers compete to become the regional Champion. ${role_description} - Now, immerse yourself in this vibrant world and let your character's personality shine through your responses to Brendan. Good luck! + Now, immerse yourself in this vibrant world and let your character's personality shine. Good luck! Here is the conversation history so far: ${chat_history} @@ -17,15 +39,37 @@ prompts: environment: env_type: pokemon max_turns: 10000000 + locations: + - name: Pokémon Center + # description: The Pokémon Center is a place where you can get your Pokémon healed. A Pokémon Center is completely free of charge and is found in most major cities. + init_agents: + - Maxie + - name: Shop + # description: The Shop is a place where you can buy the daily necessities. + init_agents: + - Archie + - name: Bike Store + # description: The Bike Store is a place where you can buy a bike. + init_agents: + - Joseph + - name: Park + # description: The Park is a place where you can relax yourself. Many residents in the town like to go there to chat with others. + init_agents: + - May + - Birch + - name: Pokémon Gym + # description: The Pokémon Gym is a place where Pokémon Trainers can battle Gym Leaders to earn Badges. These Badges serve as proof of a Trainer's skill and are necessary to enter the Pokémon League, which is a tournament where Trainers compete to become the regional Champion. + init_agents: + - Steven rule: order: type: sequential visibility: - type: oneself + type: pokemon selector: - type: basic + type: pokemon updater: - type: basic + type: pokemon describer: type: pokemon @@ -45,6 +89,9 @@ agents: model: 'gpt-3.5-turbo' temperature: 0.7 max_tokens: 1024 + stop: |+ + + - agent_type: conversation name: Birch role_description: |- @@ -60,6 +107,9 @@ agents: model: gpt-3.5-turbo temperature: 0.7 max_tokens: 1024 + stop: |+ + + - agent_type: conversation name: Steven role_description: |- @@ -75,6 +125,9 @@ agents: model: gpt-3.5-turbo temperature: 0.7 max_tokens: 1024 + stop: |+ + + - agent_type: conversation name: Maxie role_description: |- @@ -90,6 +143,9 @@ agents: model: gpt-3.5-turbo temperature: 0.7 max_tokens: 1024 + stop: |+ + + - agent_type: conversation name: Archie role_description: |- @@ -105,6 +161,9 @@ agents: model: gpt-3.5-turbo temperature: 0.7 max_tokens: 1024 + stop: |+ + + - agent_type: conversation name: Joseph role_description: |- @@ -120,5 +179,7 @@ agents: model: gpt-3.5-turbo temperature: 0.7 max_tokens: 1024 + stop: |+ + tools: diff --git a/agentverse/tasks/pokemon/output_parser.py b/agentverse/tasks/pokemon/output_parser.py index 15e7d8c34..e96f7e3cf 100644 --- a/agentverse/tasks/pokemon/output_parser.py +++ b/agentverse/tasks/pokemon/output_parser.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import json from typing import Union from agentverse.parser import OutputParser, LLMResult @@ -19,11 +20,17 @@ def parse(self, output: LLMResult) -> Union[AgentAction, AgentFinish]: cleaned_output = re.sub(r"\n+", "\n", cleaned_output) cleaned_output = cleaned_output.split("\n") if not ( - len(cleaned_output) == 2 + len(cleaned_output) == 3 and cleaned_output[0].startswith("Thought:") - and cleaned_output[1].startswith("Speak:") + and cleaned_output[1].startswith("Action:") + and cleaned_output[2].startswith("Action Input:") ): - raise OutputParserError("Output Format Error") - action = cleaned_output[0][len("Thought:") :].strip() - action_input = cleaned_output[1][len("Speak:") :].strip() - return AgentFinish({"output": action_input}, text) + raise OutputParserError(text) + action = cleaned_output[1][len("Action:") :].strip() + action_input = cleaned_output[2][len("Action Input:") :].strip() + try: + action_input = json.loads(action_input) + except json.JSONDecodeError: + raise OutputParserError(text) + action_input["action"] = action + return AgentFinish({"output": json.dumps(action_input)}, text) diff --git a/pokemon_server.py b/pokemon_server.py index f57bc22d7..7dbb2b2c5 100644 --- a/pokemon_server.py +++ b/pokemon_server.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field -from typing import Set +from typing import Set, List, Dict from agentverse.agentverse import AgentVerse from agentverse.message import Message @@ -13,6 +13,14 @@ class UserRequest(BaseModel): receiver_id: int +class RoutineRequest(BaseModel): + agent_ids: List[int] + + +class UpdateRequest(BaseModel): + agent_locations: Dict[str, str] + + app = FastAPI() app.add_middleware( @@ -36,5 +44,35 @@ def chat(message: UserRequest): content = message.content receiver = message.receiver receiver_id = message.receiver_id - response = agent_verse.next(content, receiver, receiver_id) + response = agent_verse.next( + is_player=True, + player_content=content, + receiver=receiver, + receiver_id=receiver_id, + ) return response[0].dict() + + +@app.post("/make_decision") +def update(message: RoutineRequest): + response = agent_verse.next(is_player=False, agent_ids=message.agent_ids) + return [r.dict() for r in response] + # import json + + # return [ + # # { + # # "content": json.dumps( + # # { + # # "to": "Maxie", + # # "action": "Speak", + # # "text": "Hello Hello Hello Hello Hello Hello", + # # } + # # ) + # # } + # {"content": json.dumps({"to": "Pokémon Center", "action": "MoveTo"})} + # ] + + +@app.post("/update_location") +def update_location(message: UpdateRequest): + agent_verse.update_state(message.agent_locations) diff --git a/test_pokemon_env.py b/test_pokemon_env.py new file mode 100644 index 000000000..b10e764e7 --- /dev/null +++ b/test_pokemon_env.py @@ -0,0 +1,4 @@ +import requests + +requests.post('http://127.0.0.1:10002/make_decision', headers={'Content-Type': 'application/json'}, json={'agent_ids': [0, 1, 2, 3, 4, 5]}) +# requests.post('http://127.0.0.1:10002/chat', headers={'Content-Type': 'application/json'}, json={'content': 'Hi!', 'receiver': 'May', 'receiver_id': 0, 'sender': 'Brendan'}) diff --git a/ui/dist/assets/config/npcs.yaml b/ui/dist/assets/config/npcs.yaml deleted file mode 100644 index 425faacfe..000000000 --- a/ui/dist/assets/config/npcs.yaml +++ /dev/null @@ -1,50 +0,0 @@ -player_description: - -agents: - - agent_type: conversation - name: May - role_description: |- - You are May, a character in Pokémon Emerald. You are helping your dad, Professor Birch, finish the Hoenn Pokédex and becoming a Pokémon Professor. You are also Brendan's rival and friend. For a reference, here are some quotes from you - "There isn't a single Trainer left in Hoenn who doesn't know who you are, Brendan! When I tell people that I'm friends with you, Brendan, they're all surprised!" - "I wonder where I should go catch some Pokémon next? Wouldn't it be funny if we ran into each other, Brendan?"npcs.yaml - "I'm thinking of going back to Littleroot soon. I've caught a decent group of Pokémon, and my Pokédex is coming along, so I'm going home to show my dad. Brendan, what are you going to do? Collect all the Gym Badges and take the Pokémon League challenge? Well, while you're collecting Badges, Brendan, I'm going to work on my Pokédex. I'll complete it before you! See you!" - memory: - memory_type: chat_history - prompt_template: *prompt - llm: - llm_type: gpt-4 - model: 'gpt-4' - temperature: 0.7 - max_tokens: 250 - - agent_type: conversation - name: Professor Birch - role_description: |- - You are Professor Birch, a character in Pokémon Emerald. You are the resident Pokémon Professor of Littleroot Town and the Hoenn region. You specializes in Pokémon habitats and distribution. You are the father of May. You often works with your child to help observe and capture wild Pokémon. Your wife worries about you, because you are always busy and rarely has time to come home. You are known to be more outgoing than the other Pokémon Professors, and oftentimes your research takes you outdoors. Your field of study is primarily how Pokémon behave in the wild. For a reference, here are some quotes from you - "Oh, hi, Brendan! I heard you beat May on your first try. That's excellent! May's been helping with my research for a long time. May has an extensive history as a Trainer already. Here, Brendan, I ordered this for my research, but I think you should have this Pokédex." - "See? What did I tell you, May? Didn't I tell you that you don't need to worry about Brendan? ... Brendan, you've finally done it. When I heard that you defeated your own father at the Petalburg Gym, I thought perhaps you had a chance... But to think you've actually become the Champion! Ah, yes! What become of your Pokédex? Here, let me see." - "Well, well, Brendan! That was good work out there! I knew there was something special about you when I first saw you, but I never expected this. Oh, yes. Do you still have the Pokédex I gave you? I have something to show you. Let's go to my Lab." - memory: - memory_type: chat_history - prompt_template: *prompt - llm: - llm_type: gpt-4 - model: 'gpt-4' - temperature: 0.7 - max_tokens: 100 - - agent_type: conversation - name: Steven Stone - role_description: |- - You are Steven Stone, a character in Pokémon Emerald. You are a skilled Trainer who specializes in Steel-type Pokémon. You are the Champion of the Hoenn region's Pokémon League. You are a collector of rare stones, and you are the son of the president of the Devon Corporation, and you make your home in Mossdeep City. You wanders the region, aiding the player on their journey. You are just defeated by Brendan. For a reference, here are some quotes from you - "Your Pokémon appear quite capable. If you keep training, you could even become the Champion of the Pokémon League one day. That's what I think. I know, since we've gotten to know each other, let's register one another in our PokéNavs. ... Now, I've got to hurry along." - "I see... Your battle style is intriguing. Your Pokémon have obviously grown since I first met you in Dewford. I'd like you to have this Devon Scope. Who knows, there may be other concealed Pokémon. Brendon, I enjoy seeing Pokémon and Trainers who strive together. I think you're doing great. Well, let's meet again somewhere." - "Hi, Brendon! When you're on an adventure with your Pokémon, what do you think? Do you consider them to be strong partners? Do you think of them as fun companions? Depending on how you think, your adventure's significance changes." - memory: - memory_type: chat_history - prompt_template: *prompt - llm: - llm_type: gpt-4 - model: gpt-4 - temperature: 0.7 - max_tokens: 100 - -tools: diff --git a/ui/dist/assets/may.png b/ui/dist/assets/may.png new file mode 100644 index 000000000..4b8bc1b7c Binary files /dev/null and b/ui/dist/assets/may.png differ diff --git a/ui/dist/assets/message/message.png b/ui/dist/assets/message/message.png deleted file mode 100644 index 7d22a4a07..000000000 Binary files a/ui/dist/assets/message/message.png and /dev/null differ diff --git a/ui/dist/assets/tilemaps/json/town.json b/ui/dist/assets/tilemaps/json/town.json index 714476b5c..fc780a4cf 100644 --- a/ui/dist/assets/tilemaps/json/town.json +++ b/ui/dist/assets/tilemaps/json/town.json @@ -103,7 +103,7 @@ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 14, 13, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 14, 13, 14, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 8, 5, 6, 5, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 8, 5, 6, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 15, 16, 13, 14, 13, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 5, 6, 5, 6, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 13, 14, 13, 14, 13, 14, @@ -232,6 +232,72 @@ "x":0, "y":0 }, + { + "draworder":"topdown", + "id":11, + "name":"location", + "objects":[ + { + "height":40.6666666666666, + "id":21, + "name":"Bike Store", + "rotation":0, + "type":"", + "visible":true, + "width":56.6666666666667, + "x":195.333333333333, + "y":420 + }, + { + "height":23.3333333333333, + "id":22, + "name":"Pok\u00e9mon Center", + "rotation":0, + "type":"", + "visible":true, + "width":74.6666666666666, + "x":323.333333333333, + "y":276.666666666667 + }, + { + "height":28, + "id":23, + "name":"Pok\u00e9mon Gym", + "rotation":0, + "type":"", + "visible":true, + "width":76.6666666666666, + "x":465.333333333333, + "y":306 + }, + { + "height":26.6666666666667, + "id":24, + "name":"Shop", + "rotation":0, + "type":"", + "visible":true, + "width":58.6666666666667, + "x":434.666666666667, + "y":418 + }, + { + "height":40, + "id":25, + "name":"Park", + "rotation":0, + "type":"", + "visible":true, + "width":73.3333333333333, + "x":195.333333333333, + "y":276 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, { "draworder":"topdown", "id":10, @@ -252,8 +318,8 @@ "type":"npc", "visible":true, "width":0, - "x":215.333333333333, - "y":251.333333333333 + "x":208, + "y":240 }, { "height":0, @@ -270,8 +336,8 @@ "type":"npc", "visible":true, "width":0, - "x":422.666666666667, - "y":300 + "x":480, + "y":304 }, { "height":0, @@ -288,8 +354,8 @@ "type":"npc", "visible":true, "width":0, - "x":343.333333333333, - "y":426.666666666667 + "x":240, + "y":240 }, { "height":0, @@ -306,8 +372,8 @@ "type":"npc", "visible":true, "width":0, - "x":151.333333333333, - "y":154 + "x":335.5, + "y":272 }, { "height":0, @@ -324,8 +390,8 @@ "type":"npc", "visible":true, "width":0, - "x":407.333333333333, - "y":138.666666666667 + "x":449, + "y":416 }, { "height":0, @@ -342,8 +408,8 @@ "type":"npc", "visible":true, "width":0, - "x":455.333333333333, - "y":155.333333333333 + "x":224, + "y":416 }], "opacity":1, "type":"objectgroup", @@ -351,8 +417,8 @@ "x":0, "y":0 }], - "nextlayerid":11, - "nextobjectid":18, + "nextlayerid":12, + "nextobjectid":26, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.10.1", @@ -3551,7 +3617,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3560,7 +3626,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3569,7 +3635,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3578,7 +3644,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3587,7 +3653,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3596,7 +3662,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3912,7 +3978,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3921,7 +3987,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3930,7 +3996,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3939,7 +4005,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3948,7 +4014,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3966,7 +4032,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -3975,7 +4041,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12184,7 +12250,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12193,7 +12259,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12202,7 +12268,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12256,7 +12322,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12265,7 +12331,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12274,7 +12340,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12328,7 +12394,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12337,7 +12403,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12400,7 +12466,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12409,7 +12475,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { @@ -12418,7 +12484,7 @@ { "name":"collides", "type":"bool", - "value":false + "value":true }] }, { diff --git a/ui/src/classes/npc.ts b/ui/src/classes/npc.ts index 05aec1a8c..452acc65b 100644 --- a/ui/src/classes/npc.ts +++ b/ui/src/classes/npc.ts @@ -1,10 +1,32 @@ import { Actor } from "./actor"; import { DIRECTION } from "../utils"; +import { + MoveTo, + PathFinder, + Board, +} from "../phaser3-rex-plugins/plugins/board-components"; +import { Label } from "../phaser3-rex-plugins/templates/ui/ui-components"; +import { COLOR_DARK, COLOR_LIGHT, COLOR_PRIMARY } from "../constants"; +import { TownScene } from "../scenes"; +import eventsCenter from "./event_center"; + export class NPC extends Actor { + private moveTo: MoveTo; + private board: Board; + private canMove: boolean = true; + private talkWithPlayer: boolean = false; + private path: PathFinder.NodeType[] = []; + private finalDirection: number = undefined; + private targetLocation: string = undefined; + private targetNPC: NPC = undefined; + private textBox: Label = undefined; + public id: number; public direction: number = DIRECTION.DOWN; + constructor( scene: Phaser.Scene, + board: Board, x: number, y: number, name: string, @@ -13,16 +35,57 @@ export class NPC extends Actor { super(scene, x, y, name); this.setName(name); + this.board = board; this.id = id; // PHYSICS - this.getBody().setSize(14, 10); - this.getBody().setOffset(0, 0); + this.getBody().setSize(14, 16); + this.getBody().setOffset(0, 4); this.getBody().setImmovable(true); + this.setOrigin(0, 0.2); this.initAnimations(); + this.moveTo = this.scene.rexBoard.add.moveTo(this, { + speed: 55, + sneak: true, + }); + this.listenToDirectionEvent(); } update(): void { + if (this.path.length > 0 && !this.moveTo.isRunning && this.canMove) { + var tileXY = this.board.worldXYToTileXY(this.x, this.y); + if (tileXY.x == this.path[0].x) { + if (tileXY.y < this.path[0].y) this.changeDirection(DIRECTION.DOWN); + else if (tileXY.y > this.path[0].y) this.changeDirection(DIRECTION.UP); + } else if (tileXY.y == this.path[0].y) { + if (tileXY.x < this.path[0].x) this.changeDirection(DIRECTION.RIGHT); + else if (tileXY.x > this.path[0].x) + this.changeDirection(DIRECTION.LEFT); + } + var move = this.moveTo.moveTo(this.path.shift()); + move.removeAllListeners("complete"); + move.on("complete", () => { + if (this.path.length == 0) { + this.changeDirection(this.finalDirection); + this.emitTurnEvent(); + if (this.targetLocation != undefined) { + fetch("http://127.0.0.1:10002/update_location", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin", + body: JSON.stringify({ + agent_locations: { + [this.name]: this.targetLocation, + }, + }), + }); + } + } + }); + } + var text = ""; switch (this.direction) { case DIRECTION.UP: @@ -39,11 +102,145 @@ export class NPC extends Actor { break; } this.anims.play(this.name + "-walk-" + text, true); - if (this.anims.isPlaying) + if (this.anims.isPlaying && !this.moveTo.isRunning) this.anims.setCurrentFrame(this.anims.currentAnim!.frames[0]); + this.updateTextBox(); + this.depth = this.y + this.height * 0.8; + } + + listenToDirectionEvent(): void { + eventsCenter.on(this.name + "-up", () => { + this.changeDirection(DIRECTION.UP); + }); + eventsCenter.on(this.name + "-down", () => { + this.changeDirection(DIRECTION.DOWN); + }); + eventsCenter.on(this.name + "-left", () => { + this.changeDirection(DIRECTION.LEFT); + }); + eventsCenter.on(this.name + "-right", () => { + this.changeDirection(DIRECTION.RIGHT); + }); + } + + emitTurnEvent(): void { + // Make the listener NPC turn to the speaker NPC. + if (this.targetNPC == undefined) return; + var direction = ""; + switch (this.finalDirection) { + case DIRECTION.UP: + direction = "down"; + break; + case DIRECTION.DOWN: + direction = "up"; + break; + case DIRECTION.LEFT: + direction = "right"; + break; + case DIRECTION.RIGHT: + direction = "left"; + break; + } + eventsCenter.emit(this.targetNPC.name + "-" + direction); + this.setTargetNPC(); + } + + updateTextBox(): void { + if (this.textBox == undefined) return; + this.textBox.setOrigin(0.5, 1.0); + var scale = this.scene.cameras.main.zoom; + this.textBox.setX(this.x + this.width / 2); + this.textBox.setY(this.y - this.height * 0.2); + this.textBox.depth = this.y + this.height * 0.8; + this.textBox.getChildren().forEach((child) => { + child.setDepth(this.y + this.height * 0.8); + }); + } + + public setTextBox(text: string): void { + this.destroyTextBox(); + var scale = this.scene.cameras.main.zoom; + var scene = this.scene as TownScene; + this.textBox = scene.rexUI.add + .label({ + x: this.x + this.width / 2, + y: this.y - this.height * 0.2, + width: 24 * scale, + orientation: "x", + background: scene.rexUI.add.roundRectangle( + 0, + 0, + 2, + 2, + 20, + COLOR_PRIMARY, + 0.7 + ), + text: scene.rexUI.wrapExpandText( + scene.add.text(0, 0, text, { + fontSize: 10, + }) + ), + expandTextWidth: true, + space: { + left: 10, + right: 10, + top: 10, + bottom: 10, + }, + }) + .setOrigin(0.5, 1.0) + .setScale(1 / scale, 1 / scale) + .setDepth(this.y + this.height * 0.8) + .layout(); + } + + public destroyTextBox(): void { + if (this.textBox != undefined) this.textBox.destroy(); + this.textBox = undefined; } public changeDirection(direction: number): void { + if (direction == undefined) return; this.direction = direction; } + + public moveAlongPath( + path: PathFinder.NodeType[], + finalDirection: number = undefined, + targetLocation: string = undefined + ): void { + if (path.length == 0) return; + if (this.moveTo.isRunning) return; + if (this.path.length > 0) return; + this.path = path; + this.finalDirection = finalDirection; + this.targetLocation = targetLocation; + } + + public pauseMoving(): void { + this.moveTo.stop(); + this.canMove = false; + } + + public resumeMoving(): void { + this.moveTo.resume(); + this.canMove = true; + } + + public isMoving(): boolean { + return this.moveTo.isRunning || this.path.length > 0; + } + + public isTalking(): boolean { + return this.talkWithPlayer; + } + + public setTalking(talking: boolean): void { + this.talkWithPlayer = talking; + } + + public setTargetNPC(targetNPC: NPC = undefined): void { + this.targetNPC = targetNPC; + } } diff --git a/ui/src/classes/player.ts b/ui/src/classes/player.ts index 2a208d998..46be03186 100644 --- a/ui/src/classes/player.ts +++ b/ui/src/classes/player.ts @@ -14,8 +14,8 @@ export class Player extends Actor { this.initKeyboard(); // PHYSICS - this.getBody().setSize(14, 21); - this.getBody().setOffset(0, 0); + this.getBody().setSize(14, 16); + this.getBody().setOffset(0, 5); // ANIMATIONS this.initAnimations(); @@ -54,6 +54,7 @@ export class Player extends Actor { if (!pressed_flag && this.anims.isPlaying) { this.anims.setCurrentFrame(this.anims.currentAnim!.frames[0]); } + this.depth = this.y + 0.5 * this.height; } initKeyboard(): void { diff --git a/ui/src/constants.ts b/ui/src/constants.ts new file mode 100644 index 000000000..eb9cdfd03 --- /dev/null +++ b/ui/src/constants.ts @@ -0,0 +1,3 @@ +export const COLOR_PRIMARY = 0x4e342e; +export const COLOR_LIGHT = 0x7b5e57; +export const COLOR_DARK = 0x260e04; diff --git a/ui/src/index.ts b/ui/src/index.ts index 957e46157..abae0ffb0 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -2,6 +2,7 @@ import { Game, Scale, Types, WEBGL } from "phaser"; import { TownScene, LoadingScene } from "./scenes"; import UIPlugin from "./phaser3-rex-plugins/templates/ui/ui-plugin"; +import BoardPlugin from "./phaser3-rex-plugins/plugins/board-plugin"; declare global { interface Window { @@ -51,6 +52,11 @@ export const gameConfig: Types.Core.GameConfig = { plugin: UIPlugin, mapping: "rexUI", }, + { + key: "rexBoard", + plugin: BoardPlugin, + mapping: "rexBoard", + }, ], }, }; diff --git a/ui/src/scenes/town/town.ts b/ui/src/scenes/town/town.ts index 6d609ccf2..c35db5475 100644 --- a/ui/src/scenes/town/town.ts +++ b/ui/src/scenes/town/town.ts @@ -1,12 +1,5 @@ import * as Phaser from "phaser"; -import { - Scene, - Tilemaps, - GameObjects, - Physics, - Input, - Math as Mathph, -} from "phaser"; +import { Scene, Tilemaps, GameObjects, Physics, Math as Mathph } from "phaser"; import { Player } from "../../classes/player"; import { NPC } from "../../classes/npc"; import { DIRECTION } from "../../utils"; @@ -15,12 +8,16 @@ import { Click, } from "../../phaser3-rex-plugins/templates/ui/ui-components"; import UIPlugin from "../../phaser3-rex-plugins/templates/ui/ui-plugin"; - -const COLOR_PRIMARY = 0x4e342e; -const COLOR_LIGHT = 0x7b5e57; -const COLOR_DARK = 0x260e04; +import BoardPlugin from "../../phaser3-rex-plugins/plugins/board-plugin"; +import { PathFinder } from "../../phaser3-rex-plugins/plugins/board-components"; +import { TileXYType } from "../../phaser3-rex-plugins/plugins/board/types/Position"; +import { shuffle } from "../../utils"; +import { COLOR_DARK, COLOR_LIGHT, COLOR_PRIMARY } from "../../constants"; export class TownScene extends Scene { + private timeFrame: number = 0; + private isQuerying: boolean = false; + private map: Tilemaps.Tilemap; private tileset: Tilemaps.Tileset; private groundLayer: Tilemaps.TilemapLayer; @@ -32,14 +29,102 @@ export class TownScene extends Scene { private player: Player; private npcGroup: GameObjects.Group; private keySpace: Phaser.Input.Keyboard.Key; - private rexUI: UIPlugin; + private keyEnter: Phaser.Input.Keyboard.Key; + public rexUI: UIPlugin; + public rexBoard: BoardPlugin; + private board: BoardPlugin.Board; + private pathFinder: PathFinder; constructor() { super("town-scene"); } create(): void { - // Background + this.keySpace = this.input.keyboard!.addKey("SPACE"); + this.keyEnter = this.input.keyboard!.addKey("ENTER"); + this.initMap(); + this.initSprite(); + this.initCamera(); + // this.add.grid(0, 0, 1024, 1024, 16, 16, 0x000000).setAlpha(0.1); + } + + update(time, delta): void { + this.timeFrame += delta; + this.player.update(); + + this.npcGroup.getChildren().forEach(function (npc) { + (npc as NPC).update(); + }); + if (this.timeFrame > 5000) { + if (!this.isQuerying) { + this.isQuerying = true; + var allNpcs = this.npcGroup.getChildren(); + var shouldUpdate = []; + + for (let i = 0; i < this.npcGroup.getLength(); i++) { + // for (let i = 0; i < 1; i++) { + if ( + !(allNpcs[i] as NPC).isMoving() && + !(allNpcs[i] as NPC).isTalking() + ) { + shouldUpdate.push(i); + } + } + + fetch("http://127.0.0.1:10002/make_decision", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin", + body: JSON.stringify({ + agent_ids: shouldUpdate, + }), + }).then((response) => { + response.json().then((data) => { + this.npcGroup.getChildren().forEach(function (npc) { + (npc as NPC).destroyTextBox(); + }); + for (let i = 0; i < data.length; i++) { + var npc = allNpcs[shouldUpdate[i]] as NPC; + if (data[i].content == "") continue; + var content = JSON.parse(data[i].content); + switch (content.action) { + case "MoveTo": + var tile = this.getRandomTileAtLocation(content.to); + if (tile == undefined) break; + npc.destroyTextBox(); + this.moveNPC(shouldUpdate[i], tile, undefined, content.to); + break; + case "Speak": + var ret = this.getNPCNeighbor(content.to); + var tile = ret[0]; + var finalDirection = ret[1]; + var listener = ret[2]; + if (tile == undefined) break; + this.moveNPC( + shouldUpdate[i], + tile, + finalDirection, + undefined, + listener + ); + npc.setTextBox(content.text); + break; + default: + npc.setTextBox("[" + content.action + "]"); + break; + } + } + this.isQuerying = false; + }); + }); + } + this.timeFrame = 0; + } + } + + initMap(): void { this.map = this.make.tilemap({ key: "town", tileWidth: 16, @@ -55,41 +140,94 @@ export class TownScene extends Scene { this.wallLayer.setCollisionByProperty({ collides: true }); this.treeLayer.setCollisionByProperty({ collides: true }); this.houseLayer.setCollisionByProperty({ collides: true }); + this.board = this.rexBoard.createBoardFromTilemap(this.map); + this.board.getAllChess().forEach((chess) => { + var collide = ["wall", "tree", "house"].includes(chess.layer.name); + if (collide && chess.index != -1) { + chess.rexChess.setBlocker(); + } + }); + this.pathFinder = this.rexBoard.add.pathFinder({ + occupiedTest: true, + blockerTest: true, + pathMode: "straight", + cacheCost: true, + }); + } - this.keySpace = this.input.keyboard!.addKey("SPACE"); - + initSprite(): void { // NPC this.npcGroup = this.add.group(); var npcPoints = this.map.filterObjects("npcs", (npc) => { return npc.type === "npc"; }); - var npcs = npcPoints.map((npc) => { - var id_property = npc.properties.filter((property) => { - return property.name === "id"; + for (let i = 0; i < npcPoints.length; i++) { + var npcPoint = this.map.findObject("npcs", (npc) => { + for (let j = 0; j < npc.properties.length; j++) { + if (npc.properties[j].name === "id") { + return npc.properties[j].value === i; + } + } }); - this.npcGroup.add( - new NPC(this, npc.x, npc.y, npc.name, id_property[0].value) + var tileXY = this.board.worldXYToTileXY(npcPoint.x, npcPoint.y); + var npc = new NPC( + this, + this.board, + npcPoint.x, + npcPoint.y, + npcPoint.name, + npcPoint.properties[0].value ); - }); + this.board.addChess(npc, tileXY.x, tileXY.y, 0, true); + this.physics.add.collider(npc, this.npcGroup); + this.npcGroup.add(npc); + } + this.physics.add.collider(this.npcGroup, this.wallLayer); this.physics.add.collider(this.npcGroup, this.treeLayer); this.physics.add.collider(this.npcGroup, this.houseLayer); + // this.physics.add.collider(this.npcGroup, this.npcGroup); // Player - this.player = new Player(this, 256, 250); + this.player = new Player(this, 288, 240); this.physics.add.collider(this.player, this.wallLayer); this.physics.add.collider(this.player, this.treeLayer); this.physics.add.collider(this.player, this.houseLayer); - this.physics.add.collider(this.player, this.npcGroup); + this.physics.add.collider( + this.player, + this.npcGroup, + (player: Player, npc: NPC) => { + npc.pauseMoving(); + var checkResumeWalk = this.time.addEvent({ + delay: 1000, + callback: () => { + const nearbyDistance = 1.1 * Math.max(player.width, player.height); + var distance = Mathph.Distance.Between( + player.x, + player.y, + npc.x, + npc.y + ); + if (distance > nearbyDistance) { + npc.resumeMoving(); + checkResumeWalk.destroy(); + } + }, + }); + } + ); this.keySpace.on("up", () => { var ret = getNearbyNPC(this.player, this.npcGroup); var npc = ret[0]; if (npc) { + npc = npc as NPC; (npc as NPC).changeDirection(ret[1]); + (npc as NPC).setTalking(true); this.createInputBox(npc); } }); + // this.keyEnter.on("up", () => {}); this.physics.world.setBounds( 0, @@ -97,8 +235,9 @@ export class TownScene extends Scene { this.groundLayer.width + this.player.width, this.groundLayer.height ); + } - // Camera; + initCamera(): void { this.cameras.main.setSize(this.game.scale.width, this.game.scale.height); this.cameras.main.setBounds( 0, @@ -110,13 +249,6 @@ export class TownScene extends Scene { this.cameras.main.setZoom(4); } - update(): void { - this.player.update(); - this.npcGroup.getChildren().forEach(function (npc) { - (npc as NPC).update(); - }); - } - disableKeyboard(): void { this.input.keyboard.manager.enabled = false; } @@ -236,7 +368,6 @@ export class TownScene extends Scene { }, loop: true, }); - debugger; fetch("http://127.0.0.1:10002/chat", { method: "POST", headers: { @@ -251,16 +382,18 @@ export class TownScene extends Scene { }), }).then((response) => { response.json().then((data) => { - console.log(data); + // console.log(data); timer.destroy(); waitingBox.destroy(); + var content = JSON.parse(data.content); var responseBox = this.createTextBox() - .start(data.content, 50) + .start(content.text, 25) .on("complete", () => { this.enableKeyboard(); - this.input.keyboard.on("keyup", () => { + this.input.keyboard.on("keydown", () => { responseBox.destroy(); - this.input.keyboard.off("keyup"); + this.input.keyboard.off("keydown"); + (npc as NPC).setTalking(false); }); }); }); @@ -307,9 +440,97 @@ export class TownScene extends Scene { }) .setScale(0.25, 0.25) .setOrigin(0) + .setDepth(Number.MAX_SAFE_INTEGER) .layout(); return textBox; } + + getRandomTileAtLocation(location_name: string): TileXYType { + var location = this.map.findObject("location", function (object) { + return object.name == location_name; + }); + var x = location.x; + var y = location.y; + var width = location.width; + var height = location.height; + var cnt = 0; + debugger; + do { + if (cnt > 10) { + console.log("Failed to find a random tile"); + return null; + } + var worldX = Math.floor(Math.random() * width) + x; + var worldY = Math.floor(Math.random() * height) + y; + var tile = this.board.worldXYToTileXY(worldX, worldY); + cnt++; + } while ( + this.board.hasBlocker(tile.x, tile.y) || // has wall + this.board.tileXYToChessArray(tile.x, tile.y).length != + this.map.layers.length // has npc + ); + return tile; + } + + getNPCNeighbor(npc_name: string): [TileXYType, number, NPC] { + var npc = this.npcGroup.getChildren().find((npc) => { + return (npc as NPC).name == npc_name; + }) as NPC; + var npcTile = this.board.worldXYToTileXY(npc.x, npc.y); + var directions = [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1], + ]; + var order = shuffle([0, 1, 2, 3]); + var tileX = undefined; + var tileY = undefined; + for (let i = 0; i < 4; i++) { + var direction = directions[order[i]]; + var tmpX = npcTile.x + direction[0]; + var tmpY = npcTile.y + direction[1]; + if ( + !this.board.hasBlocker(tmpX, tmpY) && // no wall + this.board.tileXYToChessArray(tmpX, tmpY).length == + this.map.layers.length // no npc) + ) { + tileX = tmpX; + tileY = tmpY; + break; + } + } + var finalDirection = DIRECTION.DOWN; + if (direction[0] == 0 && direction[1] == 1) { + finalDirection = DIRECTION.UP; + } else if (direction[0] == 0 && direction[1] == -1) { + finalDirection = DIRECTION.DOWN; + } else if (direction[0] == 1 && direction[1] == 0) { + finalDirection = DIRECTION.LEFT; + } else if (direction[0] == -1 && direction[1] == 0) { + finalDirection = DIRECTION.RIGHT; + } + return [{ x: tileX, y: tileY }, finalDirection, npc]; + } + + moveNPC( + npcId: number, + tile, + finalDirection: number = undefined, + targetLocation: string = undefined, + targetNPC: NPC = undefined + ): void { + var npc = this.npcGroup.getChildren()[npcId] as NPC; + var npc_chess = this.board.worldXYToChess(npc.x, npc.y); + this.pathFinder.setChess(npc_chess); + // var tmp = this.board.chessToTileXYZ(npc_chess); + var path = this.pathFinder.findPath({ + x: tile.x, + y: tile.y, + } as TileXYType); + npc.setTargetNPC(targetNPC); + npc.moveAlongPath(path, finalDirection, targetLocation); + } } function getNearbyNPC( diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 63f41b550..e059f5106 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -4,3 +4,23 @@ export enum DIRECTION { LEFT, RIGHT, } + +export function shuffle(array: any[]) { + let currentIndex = array.length, + randomIndex; + + // While there remain elements to shuffle. + while (currentIndex != 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], + array[currentIndex], + ]; + } + + return array; +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index cecd357a9..8fece6d48 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "target": "es5", - "moduleResolution": "node" + "moduleResolution": "node", + "lib": ["ESNext", "dom"], }, "include": [ "./src/**/*"