Repository: jsuarez5341/neural-mmo Branch: 2.1 Commit: 98b6bbdaa4aa Files: 121 Total size: 586.7 KB Directory structure: gitextract_fsthiw8v/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── documentation.md │ │ ├── enhancement.md │ │ └── feature_request.md │ └── workflows/ │ └── pylint-test.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.md ├── nmmo/ │ ├── __init__.py │ ├── core/ │ │ ├── __init__.py │ │ ├── action.py │ │ ├── agent.py │ │ ├── config.py │ │ ├── env.py │ │ ├── game_api.py │ │ ├── map.py │ │ ├── observation.py │ │ ├── realm.py │ │ ├── terrain.py │ │ └── tile.py │ ├── datastore/ │ │ ├── __init__.py │ │ ├── datastore.py │ │ ├── id_allocator.py │ │ ├── numpy_datastore.py │ │ └── serialized.py │ ├── entity/ │ │ ├── __init__.py │ │ ├── entity.py │ │ ├── entity_manager.py │ │ ├── npc.py │ │ ├── npc_manager.py │ │ └── player.py │ ├── lib/ │ │ ├── __init__.py │ │ ├── astar.py │ │ ├── colors.py │ │ ├── cython_helper.pyx │ │ ├── event_code.py │ │ ├── event_log.py │ │ ├── material.py │ │ ├── seeding.py │ │ ├── spawn.py │ │ ├── team_helper.py │ │ ├── utils.py │ │ └── vec_noise.py │ ├── minigames/ │ │ ├── __init__.py │ │ ├── center_race.py │ │ ├── comm_together.py │ │ ├── king_hill.py │ │ ├── radio_raid.py │ │ └── sandwich.py │ ├── render/ │ │ ├── __init__.py │ │ ├── overlay.py │ │ ├── render_client.py │ │ └── render_utils.py │ ├── systems/ │ │ ├── __init__.py │ │ ├── combat.py │ │ ├── droptable.py │ │ ├── exchange.py │ │ ├── inventory.py │ │ ├── item.py │ │ └── skill.py │ ├── task/ │ │ ├── __init__.py │ │ ├── base_predicates.py │ │ ├── game_state.py │ │ ├── group.py │ │ ├── predicate_api.py │ │ ├── task_api.py │ │ └── task_spec.py │ └── version.py ├── pyproject.toml ├── scripted/ │ ├── __init__.py │ ├── attack.py │ ├── baselines.py │ └── move.py ├── setup.py ├── tests/ │ ├── __init__.py │ ├── action/ │ │ ├── test_ammo_use.py │ │ ├── test_destroy_give_gold.py │ │ ├── test_monkey_action.py │ │ └── test_sell_buy.py │ ├── conftest.py │ ├── core/ │ │ ├── test_config.py │ │ ├── test_cython_masks.py │ │ ├── test_entity.py │ │ ├── test_env.py │ │ ├── test_game_api.py │ │ ├── test_gym_obs_spaces.py │ │ ├── test_map_generation.py │ │ ├── test_observation_tile.py │ │ ├── test_tile_property.py │ │ └── test_tile_seize.py │ ├── datastore/ │ │ ├── test_datastore.py │ │ ├── test_id_allocator.py │ │ ├── test_numpy_datastore.py │ │ └── test_serialized.py │ ├── render/ │ │ ├── test_load_replay.py │ │ └── test_render_save.py │ ├── systems/ │ │ ├── test_exchange.py │ │ ├── test_item.py │ │ └── test_skill_level.py │ ├── task/ │ │ ├── sample_curriculum.pkl │ │ ├── test_demo_task_creation.py │ │ ├── test_manual_curriculum.py │ │ ├── test_predicates.py │ │ ├── test_sample_task_from_file.py │ │ ├── test_task_api.py │ │ └── test_task_system_perf.py │ ├── test_death_fog.py │ ├── test_determinism.py │ ├── test_eventlog.py │ ├── test_memory_usage.py │ ├── test_mini_games.py │ ├── test_performance.py │ ├── test_pettingzoo.py │ ├── test_rollout.py │ └── testhelpers.py └── utils/ ├── git-pr.sh ├── pre-git-check.sh └── run-perf-tests.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ neural_mmo/_version.py export-subst ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a bug with the Neural MMO environment or Unity3D Embyr client title: "[Bug Report]" labels: '' assignees: '' --- Fill out as much of the below as you can. A partial bug report is better than no bug report. After submitting, link your issue on our [Discord](https://discord.gg/BkMmFUC) #support channel **OS:** Your operating system **Description:** What's wrong **Repro:** How do we reproduce the issue? Minimal scripts are best. Instructions are acceptable. "I don't know" is valid. ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.md ================================================ --- name: Documentation about: Report problems with the documentation title: "[Docs]" labels: '' assignees: '' --- One of: **Insufficient**: Something is documented, but the current documentation is inadequate **Missing**: Something is undocumented. Note that most internal functions should be considered self-documenting -- consider submitting an enhancement report for refactoring if they are not. **Other**: Something else. We will update this template to include your problem category afterwards. ================================================ FILE: .github/ISSUE_TEMPLATE/enhancement.md ================================================ --- name: Enhancement about: Suggest an improvement to an API or a refactorization of existing code for better efficiency or clarity title: "[Enhancement]" labels: '' assignees: '' --- This feature template is mostly used by the developers to track ongoing tasks, but users are also free to suggest additional enhancements or submit PRs solving existing ones. At the current scale, you should come chat with us on the Discord #development channel before writing one of these. Try to match one of the templates below. If you can't, use the "other" template for now and we'll add a new template matching your issue afterwards. **Dead code**: A piece of code is unused and should be deleted. The most common case for a dead code report occurs when we have replaced an older, clunkier routine but have neglected to delete the original. Check to make sure that you are not reporting a util function or paused-development feature before submitting. **Confusing code**: A piece of code is difficult to parse and should be refactored or at least commented. These are subjective, but we take them seriously. Neural MMO is designed to be hackable -- the internals matter just as much as the user API. **Poor performance**: A function or subroutine is slow. Describe cases in which this functionality becomes a bottleneck and submit timing data. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Request a feature for the Neural MMO environment of Unity3D Embyr client title: "[Feature Request]" labels: '' assignees: '' --- Eventually, feature requests will be treated as a scrum board for open-source contributors. At the current scale, you should come chat with us on the [Discord](https://discord.gg/BkMmFUC) #development channel before writing one of these. **I am trying to:** Describe your use case. What is the end result you would like to achieve? **It is hard/impossible because:" Is this a core missing feature? Is Neural MMO structured in a way that makes what you are trying to do unnecessarily hard? Is documentation missing or confusing? **The solution should look like:** Describe your ideal solution -- a requirement, an API, a restructuring, additional documentation, etc. ================================================ FILE: .github/workflows/pylint-test.yml ================================================ name: pylint-test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: # Randomly hitting TypeError: object int can't be used in 'await' expression in 3.11 # So, excluding 3.11 for now python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel cython pip install . python setup.py build_ext --inplace - name: Running unit tests run: pytest - name: Analysing the code with pylint run: pylint --recursive=y nmmo tests - name: Looking for xcxc, just in case run: | if grep -r --include='*.py' 'xcxc'; then echo "Found xcxc in the code. Please check the file." exit 1 fi ================================================ FILE: .gitignore ================================================ # Game maps maps/ *.swp runs/* wandb/* # local replay file from test_render_save.py tests/replay_local*.pickle replay* eval* .vscode # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions, cython *.so *.c # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ #lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .*venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ profile.run ================================================ FILE: .pylintrc ================================================ [MESSAGES CONTROL] disable=W0511, # TODO/FIXME W0105, # string is used as a statement C0114, # missing module docstring C0115, # missing class docstring C0116, # missing function docstring W0221, # arguments differ from overridden method C0415, # import outside toplevel E0611, # no name in module R0901, # too many ancestors R0902, # too many instance attributes R0903, # too few public methods R0911, # too many return statements R0912, # too many branches R0913, # too many arguments R0914, # too many local variables R0914, # too many local variables R0915, # too many statements R0401, # cyclic import [INDENTATION] indent-string=' ' [MASTER] good-names-rgxs=^[_a-zA-Z][_a-z0-9]?$ # whitelist short variables known-third-party=ordered_set,numpy,gym,pettingzoo,vec_noise,imageio,scipy,tqdm load-plugins=pylint.extensions.bad_builtin [BASIC] bad-functions=print # checks if these functions are used ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 OpenAI, 2020 Joseph Suarez 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. ================================================ FILE: MANIFEST.in ================================================ global-include *.pyx include nmmo/resource/* ================================================ FILE: README.md ================================================ ![figure](https://neuralmmo.github.io/_static/banner.jpg) # ![icon](https://neuralmmo.github.io/_build/html/_images/icon.png) Welcome to the Platform! [![PyPI version](https://badge.fury.io/py/nmmo.svg)](https://badge.fury.io/py/nmmo) [![](https://dcbadge.vercel.app/api/server/BkMmFUC?style=plastic)](https://discord.gg/BkMmFUC) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40jsuarez5341)](https://twitter.com/jsuarez5341) Neural MMO is a massively multiagent environment for artificial intelligence research inspired by Massively Multiplayer Online (MMO) role-playing games. [Documentation](https://neuralmmo.github.io "Neural MMO Documentation") is hosted by github.io. ================================================ FILE: nmmo/__init__.py ================================================ import logging from .version import __version__ from .lib import material, spawn from .render.overlay import Overlay, OverlayRegistry from .core import config, agent, action from .core.action import Action from .core.agent import Agent, Scripted from .core.env import Env from .core.terrain import MapGenerator, Terrain MOTD = rf''' ___ ___ ___ ___ /__/\ /__/\ /__/\ / /\ Version {__version__:<8} \ \:\ | |::\ | |::\ / /::\ \ \:\ | |:|:\ | |:|:\ / /:/\:\ An open source _____\__\:\ __|__|:|\:\ __|__|:|\:\ / /:/ \:\ project originally /__/::::::::\ /__/::::| \:\ /__/::::| \:\ /__/:/ \__\:\ founded by Joseph Suarez \ \:\~~\~~\/ \ \:\~~\__\/ \ \:\~~\__\/ \ \:\ / /:/ and formalized at OpenAI \ \:\ ~~~ \ \:\ \ \:\ \ \:\ /:/ \ \:\ \ \:\ \ \:\ \ \:\/:/ Now developed and \ \:\ \ \:\ \ \:\ \ \::/ maintained at MIT in \__\/ \__\/ \__\/ \__\/ Phillip Isola's lab ''' __all__ = ['Env', 'config', 'agent', 'Agent', 'Scripted', 'MapGenerator', 'Terrain', 'action', 'Action', 'material', 'spawn', 'Overlay', 'OverlayRegistry'] try: __all__.append('OpenSkillRating') except RuntimeError: logging.error('Warning: OpenSkill not installed. Ignore if you do not need this feature') ================================================ FILE: nmmo/core/__init__.py ================================================ ================================================ FILE: nmmo/core/action.py ================================================ # pylint: disable=no-method-argument,unused-argument,no-self-argument,no-member from enum import Enum, auto import numpy as np from nmmo.lib import utils from nmmo.lib.utils import staticproperty from nmmo.systems.item import Stack from nmmo.lib.event_code import EventCode from nmmo.core.observation import Observation class NodeType(Enum): #Tree edges STATIC = auto() #Traverses all edges without decisions SELECTION = auto() #Picks an edge to follow #Executable actions ACTION = auto() #No arguments CONSTANT = auto() #Constant argument VARIABLE = auto() #Variable argument class Node(metaclass=utils.IterableNameComparable): @classmethod def init(cls, config): # noop_action is used in some of the N() methods cls.noop_action = 1 if config.PROVIDE_NOOP_ACTION_TARGET else 0 @staticproperty def edges(): return [] #Fill these in @staticproperty def priority(): return None @staticproperty def type(): return None @staticproperty def leaf(): return False @classmethod def N(cls, config): return len(cls.edges) def deserialize(realm, entity, index: int, obs: Observation): return index class Fixed: pass #ActionRoot class Action(Node): nodeType = NodeType.SELECTION hooked = False @classmethod def init(cls, config): # Sets up serialization domain if Action.hooked: return Action.hooked = True #Called upon module import (see bottom of file) #Sets up serialization domain def hook(config): idx = 0 arguments = [] for action in Action.edges(config): action.init(config) for args in action.edges: # pylint: disable=not-an-iterable args.init(config) if not "edges" in args.__dict__: continue for arg in args.edges: arguments.append(arg) arg.serial = tuple([idx]) arg.idx = idx idx += 1 Action.arguments = arguments @staticproperty def n(): return len(Action.arguments) # pylint: disable=invalid-overridden-method @classmethod def edges(cls, config): """List of valid actions""" edges = [Move] if config.COMBAT_SYSTEM_ENABLED: edges.append(Attack) if config.ITEM_SYSTEM_ENABLED: edges += [Use, Give, Destroy] if config.EXCHANGE_SYSTEM_ENABLED: edges += [Buy, Sell, GiveGold] if config.COMMUNICATION_SYSTEM_ENABLED: edges.append(Comm) return edges class Move(Node): priority = 60 nodeType = NodeType.SELECTION def call(realm, entity, direction): if direction is None: return assert entity.alive, "Dead entity cannot act" assert realm.map.is_valid_pos(*entity.pos), "Invalid entity position" r, c = entity.pos ent_id = entity.ent_id entity.history.last_pos = (r, c) r_delta, c_delta = direction.delta r_new, c_new = r+r_delta, c+c_delta if not realm.map.is_valid_pos(r_new, c_new) or \ realm.map.tiles[r_new, c_new].impassible: return # ALLOW_MOVE_INTO_OCCUPIED_TILE only applies to players, NOT npcs if entity.is_player and not realm.config.ALLOW_MOVE_INTO_OCCUPIED_TILE and \ realm.map.tiles[r_new, c_new].occupied: return if entity.status.freeze > 0: return entity.set_pos(r_new, c_new) realm.map.tiles[r, c].remove_entity(ent_id) realm.map.tiles[r_new, c_new].add_entity(entity) # exploration record keeping. moved from entity.py, History.update() progress_to_center = realm.map.dist_border_center -\ utils.linf_single(realm.map.center_coord, (r_new, c_new)) if progress_to_center > entity.history.exploration: entity.history.exploration = progress_to_center if entity.is_player: realm.event_log.record(EventCode.GO_FARTHEST, entity, distance=progress_to_center) # CHECK ME: material.Impassible includes void, so this line is not reachable # Does this belong to Entity/Player.update()? if realm.map.tiles[r_new, c_new].void: entity.receive_damage(None, entity.resources.health.val) @staticproperty def edges(): return [Direction] @staticproperty def leaf(): return True def enabled(config): return True class Direction(Node): argType = Fixed @staticproperty def edges(): return [North, South, East, West, Stay] def deserialize(realm, entity, index: int, obs): return deserialize_fixed_arg(Direction, index) # a quick helper function def deserialize_fixed_arg(arg, index): if isinstance(index, (int, np.int64)): if index < 0: return None # so that the action will be discarded val = min(index, len(arg.edges)-1) return arg.edges[val] # if index is not int, it's probably already deserialized if index not in arg.edges: return None # so that the action will be discarded return index class North(Node): delta = (-1, 0) class South(Node): delta = (1, 0) class East(Node): delta = (0, 1) class West(Node): delta = (0, -1) class Stay(Node): delta = (0, 0) class Attack(Node): priority = 50 nodeType = NodeType.SELECTION @staticproperty def n(): return 3 @staticproperty def edges(): return [Style, Target] @staticproperty def leaf(): return True def enabled(config): return config.COMBAT_SYSTEM_ENABLED def in_range(entity, stim, config, N): R, C = stim.shape R, C = R//2, C//2 rets = set([entity]) for r in range(R-N, R+N+1): for c in range(C-N, C+N+1): for e in stim[r, c].entities.values(): rets.add(e) rets = list(rets) return rets def call(realm, entity, style, target): if style is None or target is None: return None assert entity.alive, "Dead entity cannot act" config = realm.config if entity.is_player and not config.COMBAT_SYSTEM_ENABLED: return None # Testing a spawn immunity against old agents to avoid spawn camping immunity = config.COMBAT_SPAWN_IMMUNITY if entity.is_player and target.is_player and \ target.history.time_alive < immunity: return None #Check if self targeted or target already dead if entity.ent_id == target.ent_id or not target.alive: return None #Can't attack out of range if utils.linf_single(entity.pos, target.pos) > style.attack_range(config): return None #Execute attack entity.history.attack = {} entity.history.attack["target"] = target.ent_id entity.history.attack["style"] = style.__name__ target.attacker = entity target.attacker_id.update(entity.ent_id) from nmmo.systems import combat dmg = combat.attack(realm, entity, target, style.skill) # record the combat tick for both entities # players and npcs both have latest_combat_tick in EntityState for ent in [entity, target]: ent.latest_combat_tick.update(realm.tick + 1) # because the tick is about to increment return dmg class Style(Node): argType = Fixed @staticproperty def edges(): return [Melee, Range, Mage] def deserialize(realm, entity, index: int, obs): return deserialize_fixed_arg(Style, index) class Target(Node): argType = None @classmethod def N(cls, config): return config.PLAYER_N_OBS + cls.noop_action def deserialize(realm, entity, index: int, obs: Observation): if index >= len(obs.entities.ids): return None return realm.entity_or_none(obs.entities.ids[index]) class Melee(Node): nodeType = NodeType.ACTION freeze=False def attack_range(config): return config.COMBAT_MELEE_REACH def skill(entity): return entity.skills.melee class Range(Node): nodeType = NodeType.ACTION freeze=False def attack_range(config): return config.COMBAT_RANGE_REACH def skill(entity): return entity.skills.range class Mage(Node): nodeType = NodeType.ACTION freeze=False def attack_range(config): return config.COMBAT_MAGE_REACH def skill(entity): return entity.skills.mage class InventoryItem(Node): argType = None @classmethod def N(cls, config): return config.INVENTORY_N_OBS + cls.noop_action def deserialize(realm, entity, index: int, obs: Observation): if index >= len(obs.inventory.ids): return None return realm.items.get(obs.inventory.ids[index]) class Use(Node): priority = 10 @staticproperty def edges(): return [InventoryItem] def enabled(config): return config.ITEM_SYSTEM_ENABLED def call(realm, entity, item): if item is None or item.owner_id.val != entity.ent_id: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot use an item" assert item.quantity.val > 0, "Item quantity cannot be 0" # indicates item leak if not realm.config.ITEM_SYSTEM_ENABLED: return if item not in entity.inventory: return if entity.in_combat: # player cannot use item during combat return # cannot use listed items or items that have higher level if item.listed_price.val > 0 or item.level_gt(entity): return item.use(entity) class Destroy(Node): priority = 40 @staticproperty def edges(): return [InventoryItem] def enabled(config): return config.ITEM_SYSTEM_ENABLED def call(realm, entity, item): if item is None or item.owner_id.val != entity.ent_id: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot destroy an item" assert item.quantity.val > 0, "Item quantity cannot be 0" # indicates item leak if not realm.config.ITEM_SYSTEM_ENABLED: return if item not in entity.inventory: return if item.equipped.val: # cannot destroy equipped item return if entity.in_combat: # player cannot destroy item during combat return item.destroy() realm.event_log.record(EventCode.DESTROY_ITEM, entity) class Give(Node): priority = 30 @staticproperty def edges(): return [InventoryItem, Target] def enabled(config): return config.ITEM_SYSTEM_ENABLED def call(realm, entity, item, target): if item is None or item.owner_id.val != entity.ent_id or target is None: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot give an item" assert item.quantity.val > 0, "Item quantity cannot be 0" # indicates item leak config = realm.config if not config.ITEM_SYSTEM_ENABLED: return if not (target.is_player and target.alive): return if item not in entity.inventory: return # cannot give the equipped or listed item if item.equipped.val or item.listed_price.val: return if entity.in_combat: # player cannot give item during combat return if not (config.ITEM_ALLOW_GIFT and entity.ent_id != target.ent_id and # but not self target.is_player): return # NOTE: allow give within the visual range if utils.linf_single(entity.pos, target.pos) > config.PLAYER_VISION_RADIUS: return if not target.inventory.space: # receiver inventory is full - see if it has an ammo stack with the same sig if isinstance(item, Stack): if not target.inventory.has_stack(item.signature): # no ammo stack with the same signature, so cannot give return else: # no space, and item is not ammo stack, so cannot give return entity.inventory.remove(item) target.inventory.receive(item) realm.event_log.record(EventCode.GIVE_ITEM, entity) class GiveGold(Node): priority = 30 @staticproperty def edges(): # CHECK ME: for now using Price to indicate the gold amount to give return [Price, Target] def enabled(config): return config.EXCHANGE_SYSTEM_ENABLED def call(realm, entity, amount, target): if amount is None or target is None: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot give gold" config = realm.config if not config.EXCHANGE_SYSTEM_ENABLED: return if not (target.is_player and target.alive): return if entity.in_combat: # player cannot give gold during combat return if not (config.ITEM_ALLOW_GIFT and entity.ent_id != target.ent_id and # but not self target.is_player): return # NOTE: allow give within the visual range if utils.linf_single(entity.pos, target.pos) > config.PLAYER_VISION_RADIUS: return if not isinstance(amount, int): amount = amount.val if amount > entity.gold.val: # no gold to give return entity.gold.decrement(amount) target.gold.increment(amount) realm.event_log.record(EventCode.GIVE_GOLD, entity) class MarketItem(Node): argType = None @classmethod def N(cls, config): return config.MARKET_N_OBS + cls.noop_action def deserialize(realm, entity, index: int, obs: Observation): if index >= len(obs.market.ids): return None return realm.items.get(obs.market.ids[index]) class Buy(Node): priority = 20 argType = Fixed @staticproperty def edges(): return [MarketItem] def enabled(config): return config.EXCHANGE_SYSTEM_ENABLED def call(realm, entity, item): if item is None or item.owner_id.val == 0: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot buy an item" assert item.quantity.val > 0, "Item quantity cannot be 0" # indicates item leak assert item.equipped.val == 0, "Listed item must not be equipped" if not realm.config.EXCHANGE_SYSTEM_ENABLED: return if entity.gold.val < item.listed_price.val: # not enough money return if entity.ent_id == item.owner_id.val: # cannot buy own item return if entity.in_combat: # player cannot buy item during combat return if not entity.inventory.space: # buyer inventory is full - see if it has an ammo stack with the same sig if isinstance(item, Stack): if not entity.inventory.has_stack(item.signature): # no ammo stack with the same signature, so cannot give return else: # no space, and item is not ammo stack, so cannot give return # one can try to buy, but the listing might have gone (perhaps bought by other) realm.exchange.buy(entity, item) class Sell(Node): priority = 70 argType = Fixed @staticproperty def edges(): return [InventoryItem, Price] def enabled(config): return config.EXCHANGE_SYSTEM_ENABLED def call(realm, entity, item, price): if item is None or item.owner_id.val != entity.ent_id or price is None: return assert entity.alive, "Dead entity cannot act" assert entity.is_player, "Npcs cannot sell an item" assert item.quantity.val > 0, "Item quantity cannot be 0" # indicates item leak if not realm.config.EXCHANGE_SYSTEM_ENABLED: return if item not in entity.inventory: return if entity.in_combat: # player cannot sell item during combat return # cannot sell the equipped or listed item if item.equipped.val or item.listed_price.val: return if not isinstance(price, int): price = price.val if not price > 0: return realm.exchange.sell(entity, item, price, realm.tick) def init_discrete(values): classes = [] for i in values: name = f"Discrete_{i}" cls = type(name, (object,), {"val": i}) classes.append(cls) return classes class Price(Node): argType = Fixed @classmethod def init(cls, config): # gold should be > 0 cls.price_range = range(1, config.PRICE_N_OBS+1) Price.classes = init_discrete(cls.price_range) @classmethod def index(cls, price): try: return cls.price_range.index(price) except ValueError: # use the max price, which is config.PRICE_N_OBS return len(cls.price_range) - 1 @staticproperty def edges(): return Price.classes def deserialize(realm, entity, index: int, obs): return deserialize_fixed_arg(Price, index) class Token(Node): argType = Fixed @classmethod def init(cls, config): Token.classes = init_discrete(range(1, config.COMMUNICATION_NUM_TOKENS+1)) @staticproperty def edges(): return Token.classes def deserialize(realm, entity, index: int, obs): return deserialize_fixed_arg(Token, index) class Comm(Node): argType = Fixed priority = 99 @staticproperty def edges(): return [Token] def enabled(config): return config.COMMUNICATION_SYSTEM_ENABLED def call(realm, entity, token): if token is None: return entity.message.update(token.val) #TODO: Solve AGI class BecomeSkynet: pass ================================================ FILE: nmmo/core/agent.py ================================================ class Agent: policy = 'Neural' def __init__(self, config, idx): '''Base class for agents Args: config: A Config object idx: Unique AgentID int ''' self.config = config self.iden = idx self._np_random = None def __call__(self, obs): '''Used by scripted agents to compute actions. Override in subclasses. Args: obs: Agent observation provided by the environment ''' def set_rng(self, np_random): '''Set the random number generator for the agent for reproducibility Args: np_random: A numpy random.Generator object ''' self._np_random = np_random class Scripted(Agent): '''Base class for scripted agents''' policy = 'Scripted' ================================================ FILE: nmmo/core/config.py ================================================ # pylint: disable=invalid-name from __future__ import annotations import os import sys import logging import re import nmmo from nmmo.core.agent import Agent from nmmo.core.terrain import MapGenerator from nmmo.lib import utils, material, spawn CONFIG_ATTR_PATTERN = r"^[A-Z_]+$" GAME_SYSTEMS = ["TERRAIN", "RESOURCE", "COMBAT", "NPC", "PROGRESSION", "ITEM", "EQUIPMENT", "PROFESSION", "EXCHANGE", "COMMUNICATION"] # These attributes are critical for trainer and must not change from the initial values OBS_ATTRS = set(["MAX_HORIZON", "PLAYER_N", "MAP_N_OBS", "PLAYER_N_OBS", "TASK_EMBED_DIM", "ITEM_INVENTORY_CAPACITY", "MARKET_N_OBS", "PRICE_N_OBS", "COMMUNICATION_NUM_TOKENS", "COMMUNICATION_N_OBS", "PROVIDE_ACTION_TARGETS", "PROVIDE_DEATH_FOG_OBS", "PROVIDE_NOOP_ACTION_TARGET"]) IMMUTABLE_ATTRS = set(["USE_CYTHON", "CURRICULUM_FILE_PATH", "PLAYER_VISION_RADIUS", "MAP_SIZE", "PLAYER_BASE_HEALTH", "RESOURCE_BASE", "PROGRESSION_LEVEL_MAX"]) class Template(metaclass=utils.StaticIterable): def __init__(self): self._data = {} cls = type(self) # Set defaults from static properties for attr in dir(cls): val = getattr(cls, attr) if re.match(CONFIG_ATTR_PATTERN, attr) and not isinstance(val, property): self._data[attr] = val def override(self, **kwargs): for k, v in kwargs.items(): err = f'CLI argument: {k} is not a Config property' assert hasattr(self, k), err self.set(k, v) def set(self, k, v): if not isinstance(v, property): try: setattr(self, k, v) except AttributeError: logging.error('Cannot set attribute: %s to %s', str(k), str(v)) sys.exit() self._data[k] = v # pylint: disable=bad-builtin def print(self): key_len = 0 for k in self._data: key_len = max(key_len, len(k)) print('Configuration') for k, v in self._data.items(): print(f' {k:{key_len}s}: {v}') def items(self): return self._data.items() def __iter__(self): for k in self._data: yield k def keys(self): return self._data.keys() def values(self): return self._data.values() def validate(config): err = 'config.Config is a base class. Use config.{Small, Medium Large}''' assert isinstance(config, Config), err assert config.HORIZON < config.MAX_HORIZON, 'HORIZON must be <= MAX_HORIZON' if not config.TERRAIN_SYSTEM_ENABLED: err = 'Invalid Config: {} requires Terrain' assert not config.RESOURCE_SYSTEM_ENABLED, err.format('Resource') assert not config.PROFESSION_SYSTEM_ENABLED, err.format('Profession') if not config.COMBAT_SYSTEM_ENABLED: err = 'Invalid Config: {} requires Combat' assert not config.NPC_SYSTEM_ENABLED, err.format('NPC') if not config.ITEM_SYSTEM_ENABLED: err = 'Invalid Config: {} requires Inventory' assert not config.EQUIPMENT_SYSTEM_ENABLED, err.format('Equipment') assert not config.PROFESSION_SYSTEM_ENABLED, err.format('Profession') assert not config.EXCHANGE_SYSTEM_ENABLED, err.format('Exchange') class Config(Template): '''An environment configuration object''' env_initialized = False def __init__(self): super().__init__() self._attr_to_reset = [] # TODO: Come up with a better way # to resolve mixin MRO conflicts for system in GAME_SYSTEMS: if not hasattr(self, f'{system}_SYSTEM_ENABLED'): self.set(f'{system}_SYSTEM_ENABLED', False) if __debug__: validate(self) deprecated_attrs = [ 'NENT', 'NPOP', 'AGENTS', 'NMAPS', 'FORCE_MAP_GENERATION', 'SPAWN'] for attr in deprecated_attrs: assert not hasattr(self, attr), f'{attr} has been deprecated or renamed' @property def original(self): return self._data def reset(self): '''Reset all attributes changed during the episode''' for attr in self._attr_to_reset: setattr(self, attr, self.original[attr]) def set(self, k, v): assert self.env_initialized is False, 'Cannot set config attr after env init' super().set(k, v) def set_for_episode(self, k, v): '''Set a config property for the current episode''' assert hasattr(self, k), f'Invalid config property: {k}' assert k not in OBS_ATTRS, f'Cannot change OBS config {k} during the episode' assert k not in IMMUTABLE_ATTRS, f'Cannot change {k} during the episode' # Cannot turn on a game system that was not enabled when the env was created if k.endswith('_SYSTEM_ENABLED') and self._data[k] is False and v is True: raise AssertionError(f'Cannot turn on {k} because it was not enabled during env init') # Change only the attribute and keep the original value in the data dict setattr(self, k, v) self._attr_to_reset.append(k) @property def enabled_systems(self): '''Return a list of the enabled systems from Env.__init__()''' return [k[:-len('_SYSTEM_ENABLED')] for k, v in self._data.items() if k.endswith('_SYSTEM_ENABLED') and v is True] @property def system_states(self): '''Return a one-hot encoding of each system enabled/disabled, which can be used as an observation and changed from episode to episode''' return [int(getattr(self, f'{system}_SYSTEM_ENABLED')) for system in GAME_SYSTEMS] def are_systems_enabled(self, systems): # systems is a list of strings '''Check if all provided systems are enabled''' return all(s.upper() in self.enabled_systems for s in systems) def toggle_systems(self, target_systems): # systems is a list of strings '''Activate only the provided game systems and turn off the others''' target_systems = [s.upper() for s in target_systems] for system in target_systems: assert system in self.enabled_systems, f'Invalid game system: {system}' self.set_for_episode(f'{system}_SYSTEM_ENABLED', True) for system in self.enabled_systems: if system not in target_systems: self.set_for_episode(f'{system}_SYSTEM_ENABLED', False) ############################################################################ ### Meta-Parameters PLAYERS = [Agent] '''Player classes from which to spawn''' @property def PLAYER_POLICIES(self): '''Number of player policies''' return len(self.PLAYERS) PLAYER_N = None '''Maximum number of players spawnable in the environment''' @property def POSSIBLE_AGENTS(self): '''List of possible agents to spawn''' return list(range(1, self.PLAYER_N + 1)) # TODO: CHECK if there could be 100+ entities within one's vision PLAYER_N_OBS = 100 '''Number of distinct agent observations''' MAX_HORIZON = 2**15 - 1 # this is arbitrary '''Maximum number of steps the environment can run for''' HORIZON = 1024 '''Number of steps before the environment resets''' GAME_PACKS = None '''List of game packs to load and sample: [(game class, sampling weight)]''' CURRICULUM_FILE_PATH = None '''Path to a curriculum task file containing a list of task specs for training''' TASK_EMBED_DIM = 4096 '''Dimensionality of task embeddings''' ALLOW_MULTI_TASKS_PER_AGENT = False '''Whether to allow multiple tasks per agent''' PROVIDE_ACTION_TARGETS = True '''Provide action targets mask''' PROVIDE_NOOP_ACTION_TARGET = True '''Provide a no-op option for each action''' PROVIDE_DEATH_FOG_OBS = False '''Provide death fog observation''' ALLOW_MOVE_INTO_OCCUPIED_TILE = True '''Whether agents can move into tiles occupied by other agents/npcs However, this does not apply to spawning''' ############################################################################ ### System/debug Parameters USE_CYTHON = True '''Whether to use cython modules for performance''' IMMORTAL = False '''Debug parameter: prevents agents from dying except by void''' ############################################################################ ### Player Parameters PLAYER_BASE_HEALTH = 100 '''Initial agent health''' PLAYER_VISION_RADIUS = 7 '''Number of tiles an agent can see in any direction''' @property def PLAYER_VISION_DIAMETER(self): '''Size of the square tile crop visible to an agent''' return 2*self.PLAYER_VISION_RADIUS + 1 PLAYER_HEALTH_INCREMENT = 0 '''The amount to increment health by 1 per tick for players, like npcs''' DEATH_FOG_ONSET = None '''How long before spawning death fog. None for no death fog''' DEATH_FOG_SPEED = 1 '''Number of tiles per tick that the fog moves in''' DEATH_FOG_FINAL_SIZE = 8 '''Number of tiles from the center that the fog stops''' PLAYER_LOADER = spawn.SequentialLoader '''Agent loader class specifying spawn sampling''' ############################################################################ ### Team Parameters TEAMS = None # Dict[Any, List[int]] '''A dictionary of team assignments: key is team_id, value is a list of agent_ids''' ############################################################################ ### Map Parameters MAP_N = 1 '''Number of maps to generate''' MAP_N_TILE = len(material.All.materials) '''Number of distinct terrain tile types''' @property def MAP_N_OBS(self): '''Number of distinct tile observations''' return int(self.PLAYER_VISION_DIAMETER ** 2) MAP_SIZE = None '''Size of the whole map, including the center and borders''' MAP_CENTER = None '''Size of each map (number of tiles along each side), where agents can move around''' @property def MAP_BORDER(self): '''Number of background, void border tiles surrounding each side of the map''' return int((self.MAP_SIZE - self.MAP_CENTER) // 2) MAP_GENERATOR = MapGenerator '''Specifies a user map generator. Uses default generator if unspecified.''' MAP_FORCE_GENERATION = True '''Whether to regenerate and overwrite existing maps''' MAP_RESET_FROM_FRACTAL = True '''Whether to regenerate the map from the fractal source''' MAP_GENERATE_PREVIEWS = False '''Whether map generation should also save .png previews (slow + large file size)''' MAP_PREVIEW_DOWNSCALE = 1 '''Downscaling factor for png previews''' ############################################################################ ### Path Parameters PATH_ROOT = os.path.dirname(nmmo.__file__) '''Global repository directory''' PATH_CWD = os.getcwd() '''Working directory''' PATH_RESOURCE = os.path.join(PATH_ROOT, 'resource') '''Resource directory''' PATH_TILE = os.path.join(PATH_RESOURCE, '{}.png') '''Tile path -- format me with tile name''' PATH_MAPS = None '''Generated map directory''' PATH_MAP_SUFFIX = 'map{}/map.npy' '''Map file name''' PATH_FRACTAL_SUFFIX = 'map{}/fractal.npy' '''Fractal file name''' ############################################################################ ### Game Systems (Static Mixins) class Terrain: '''Terrain Game System''' TERRAIN_SYSTEM_ENABLED = True '''Game system flag''' TERRAIN_FLIP_SEED = False '''Whether to negate the seed used for generation (useful for unique heldout maps)''' TERRAIN_FREQUENCY = -3 '''Base noise frequency range (log2 space)''' TERRAIN_FREQUENCY_OFFSET = 7 '''Noise frequency octave offset (log2 space)''' TERRAIN_LOG_INTERPOLATE_MIN = -2 '''Minimum interpolation log-strength for noise frequencies''' TERRAIN_LOG_INTERPOLATE_MAX = 0 '''Maximum interpolation log-strength for noise frequencies''' TERRAIN_TILES_PER_OCTAVE = 8 '''Number of octaves sampled from log2 spaced TERRAIN_FREQUENCY range''' TERRAIN_VOID = 0.0 '''Noise threshold for void generation''' TERRAIN_WATER = 0.30 '''Noise threshold for water generation''' TERRAIN_GRASS = 0.70 '''Noise threshold for grass''' TERRAIN_FOILAGE = 0.85 '''Noise threshold for foilage (food tile)''' TERRAIN_RESET_TO_GRASS = False '''Whether to make all tiles grass. Only works when MAP_RESET_FROM_FRACTAL is True''' TERRAIN_DISABLE_STONE = False '''Disable stone (obstacle) tiles''' TERRAIN_SCATTER_EXTRA_RESOURCES = True '''Whether to scatter extra food, water on the map. Only works when MAP_RESET_FROM_FRACTAL is True''' class Resource: '''Resource Game System''' RESOURCE_SYSTEM_ENABLED = True '''Game system flag''' RESOURCE_BASE = 100 '''Initial level and capacity for food and water''' RESOURCE_DEPLETION_RATE = 5 '''Depletion rate for food and water''' RESOURCE_STARVATION_RATE = 10 '''Damage per tick without food''' RESOURCE_DEHYDRATION_RATE = 10 '''Damage per tick without water''' RESOURCE_RESILIENT_POPULATION = 0 '''Training helper: proportion of population that is resilient to starvation and dehydration (e.g. 0.1 means 10% of the population is resilient to starvation and dehydration) This is to make some agents live longer during training to sample from "advanced" agents.''' RESOURCE_DAMAGE_REDUCTION = 0.5 '''Training helper: damage reduction from starvation and dehydration for resilient agents''' RESOURCE_FOILAGE_CAPACITY = 1 '''Maximum number of harvests before a foilage tile decays''' RESOURCE_FOILAGE_RESPAWN = 0.025 '''Probability that a harvested foilage tile will regenerate each tick''' RESOURCE_HARVEST_RESTORE_FRACTION = 1.0 '''Fraction of maximum capacity restored upon collecting a resource''' RESOURCE_HEALTH_REGEN_THRESHOLD = 0.5 '''Fraction of maximum resource capacity required to regen health''' RESOURCE_HEALTH_RESTORE_FRACTION = 0.1 '''Fraction of health restored per tick when above half food+water''' # NOTE: Included self to be picklable (in torch.save) since lambdas are not picklable def original_combat_damage_formula(self, offense, defense, multiplier, minimum_proportion): # pylint: disable=unused-argument return int(multiplier * (offense * (15 / (15 + defense)))) def alt_combat_damage_formula(self, offense, defense, multiplier, minimum_proportion): # pylint: disable=unused-argument return int(max(multiplier * offense - defense, offense * minimum_proportion)) class Combat: '''Combat Game System''' COMBAT_SYSTEM_ENABLED = True '''Game system flag''' COMBAT_SPAWN_IMMUNITY = 20 '''Agents older than this many ticks cannot attack agents younger than this many ticks''' COMBAT_ALLOW_FLEXIBLE_STYLE = True '''Whether to allow agents to attack with any style in a given turn''' COMBAT_STATUS_DURATION = 3 '''Combat status lasts for this many ticks after the last combat event. Combat events include both attacking and being attacked.''' COMBAT_WEAKNESS_MULTIPLIER = 1.5 '''Multiplier for super-effective attacks''' COMBAT_MINIMUM_DAMAGE_PROPORTION = 0.25 '''Minimum proportion of damage to inflict on a target''' # NOTE: When using a custom function, include "self" as the first arg COMBAT_DAMAGE_FORMULA = alt_combat_damage_formula '''Damage formula''' COMBAT_MELEE_DAMAGE = 10 '''Melee attack damage''' COMBAT_MELEE_REACH = 3 '''Reach of attacks using the Melee skill''' COMBAT_RANGE_DAMAGE = 10 '''Range attack damage''' COMBAT_RANGE_REACH = 3 '''Reach of attacks using the Range skill''' COMBAT_MAGE_DAMAGE = 10 '''Mage attack damage''' COMBAT_MAGE_REACH = 3 '''Reach of attacks using the Mage skill''' def default_exp_threshold(base_exp, max_level): import math additional_exp_per_level = [round(base_exp * math.sqrt(lvl)) for lvl in range(1, max_level+1)] return [sum(additional_exp_per_level[:lvl]) for lvl in range(max_level)] class Progression: '''Progression Game System''' PROGRESSION_SYSTEM_ENABLED = True '''Game system flag''' PROGRESSION_BASE_LEVEL = 1 '''Initial skill level''' PROGRESSION_LEVEL_MAX = 10 '''Max skill level''' PROGRESSION_EXP_THRESHOLD = default_exp_threshold(90, PROGRESSION_LEVEL_MAX) '''A list of experience thresholds for each level''' PROGRESSION_COMBAT_XP_SCALE = 6 '''Additional XP for each attack for skills Melee, Range, and Mage''' PROGRESSION_AMMUNITION_XP_SCALE = 15 '''Additional XP for each harvest for Prospecting, Carving, and Alchemy''' PROGRESSION_CONSUMABLE_XP_SCALE = 30 '''Multiplier XP for each harvest for Fishing and Herbalism''' PROGRESSION_MELEE_BASE_DAMAGE = 10 '''Base Melee attack damage''' PROGRESSION_MELEE_LEVEL_DAMAGE = 5 '''Bonus Melee attack damage per level''' PROGRESSION_RANGE_BASE_DAMAGE = 10 '''Base Range attack damage''' PROGRESSION_RANGE_LEVEL_DAMAGE = 5 '''Bonus Range attack damage per level''' PROGRESSION_MAGE_BASE_DAMAGE = 10 '''Base Mage attack damage ''' PROGRESSION_MAGE_LEVEL_DAMAGE = 5 '''Bonus Mage attack damage per level''' PROGRESSION_BASE_DEFENSE = 0 '''Base defense''' PROGRESSION_LEVEL_DEFENSE = 5 '''Bonus defense per level''' class NPC: '''NPC Game System''' NPC_SYSTEM_ENABLED = True '''Game system flag''' NPC_N = None '''Maximum number of NPCs spawnable in the environment''' NPC_DEFAULT_REFILL_DEAD_NPCS = True '''Whether to refill dead NPCs''' NPC_SPAWN_ATTEMPTS = 25 '''Number of NPC spawn attempts per tick''' NPC_SPAWN_AGGRESSIVE = 0.80 '''Percentage distance threshold from spawn for aggressive NPCs''' NPC_SPAWN_NEUTRAL = 0.50 '''Percentage distance threshold from spawn for neutral NPCs''' NPC_SPAWN_PASSIVE = 0.00 '''Percentage distance threshold from spawn for passive NPCs''' NPC_LEVEL_MIN = 1 '''Minimum NPC level''' NPC_LEVEL_MAX = 10 '''Maximum NPC level''' NPC_BASE_DEFENSE = 0 '''Base NPC defense''' NPC_LEVEL_DEFENSE = 8 '''Bonus NPC defense per level''' NPC_BASE_DAMAGE = 0 '''Base NPC damage''' NPC_LEVEL_DAMAGE = 8 '''Bonus NPC damage per level''' NPC_LEVEL_MULTIPLIER = 1.0 '''Multiplier for NPC level damage and defense, for easier difficulty tuning''' NPC_ALLOW_ATTACK_OTHER_NPCS = False '''Whether NPCs can attack other NPCs''' class Item: '''Inventory Game System''' ITEM_SYSTEM_ENABLED = True '''Game system flag''' ITEM_N = 17 '''Number of unique base item classes''' ITEM_INVENTORY_CAPACITY = 12 '''Number of inventory spaces''' ITEM_ALLOW_GIFT = True '''Whether agents can give gold/item to each other''' @property def INVENTORY_N_OBS(self): '''Number of distinct item observations''' return self.ITEM_INVENTORY_CAPACITY class Equipment: '''Equipment Game System''' EQUIPMENT_SYSTEM_ENABLED = True '''Game system flag''' WEAPON_DROP_PROB = 0.025 '''Chance of getting a weapon while harvesting ammunition''' EQUIPMENT_WEAPON_BASE_DAMAGE = 5 '''Base weapon damage''' EQUIPMENT_WEAPON_LEVEL_DAMAGE = 5 '''Added weapon damage per level''' EQUIPMENT_AMMUNITION_BASE_DAMAGE = 5 '''Base ammunition damage''' EQUIPMENT_AMMUNITION_LEVEL_DAMAGE = 10 '''Added ammunition damage per level''' EQUIPMENT_TOOL_BASE_DEFENSE = 15 '''Base tool defense''' EQUIPMENT_TOOL_LEVEL_DEFENSE = 0 '''Added tool defense per level''' EQUIPMENT_ARMOR_BASE_DEFENSE = 0 '''Base armor defense''' EQUIPMENT_ARMOR_LEVEL_DEFENSE = 3 '''Base equipment defense''' class Profession: '''Profession Game System''' PROFESSION_SYSTEM_ENABLED = True '''Game system flag''' PROFESSION_TREE_CAPACITY = 1 '''Maximum number of harvests before a tree tile decays''' PROFESSION_TREE_RESPAWN = 0.105 '''Probability that a harvested tree tile will regenerate each tick''' PROFESSION_ORE_CAPACITY = 1 '''Maximum number of harvests before an ore tile decays''' PROFESSION_ORE_RESPAWN = 0.10 '''Probability that a harvested ore tile will regenerate each tick''' PROFESSION_CRYSTAL_CAPACITY = 1 '''Maximum number of harvests before a crystal tile decays''' PROFESSION_CRYSTAL_RESPAWN = 0.10 '''Probability that a harvested crystal tile will regenerate each tick''' PROFESSION_HERB_CAPACITY = 1 '''Maximum number of harvests before an herb tile decays''' PROFESSION_HERB_RESPAWN = 0.02 '''Probability that a harvested herb tile will regenerate each tick''' PROFESSION_FISH_CAPACITY = 1 '''Maximum number of harvests before a fish tile decays''' PROFESSION_FISH_RESPAWN = 0.02 '''Probability that a harvested fish tile will regenerate each tick''' def PROFESSION_CONSUMABLE_RESTORE(self, level): '''Amount of food/water restored by consuming a consumable item''' return 50 + 5*level class Exchange: '''Exchange Game System''' EXCHANGE_SYSTEM_ENABLED = True '''Game system flag''' EXCHANGE_BASE_GOLD = 1 '''Initial gold amount''' EXCHANGE_LISTING_DURATION = 3 '''The number of ticks, during which the item is listed for sale''' MARKET_N_OBS = 384 # this should be proportion to PLAYER_N '''Number of distinct item observations''' PRICE_N_OBS = 99 # make it different from PLAYER_N_OBS '''Number of distinct price observations This also determines the maximum price one can set for an item ''' class Communication: '''Exchange Game System''' COMMUNICATION_SYSTEM_ENABLED = True '''Game system flag''' COMMUNICATION_N_OBS = 32 '''Number of players that share the same communication obs, i.e. the same team''' COMMUNICATION_NUM_TOKENS = 127 '''Number of distinct COMM tokens''' class AllGameSystems( Terrain, Resource, Combat, NPC, Progression, Item, Equipment, Profession, Exchange, Communication): pass ############################################################################ ### Config presets class Small(Config): '''A small config for debugging and experiments with an expensive outer loop''' PATH_MAPS = 'maps/small' PLAYER_N = 64 MAP_PREVIEW_DOWNSCALE = 4 MAP_SIZE = 64 MAP_CENTER = 32 TERRAIN_LOG_INTERPOLATE_MIN = 0 NPC_N = 32 NPC_LEVEL_MAX = 5 NPC_LEVEL_SPREAD = 1 PROGRESSION_SPAWN_CLUSTERS = 4 PROGRESSION_SPAWN_UNIFORMS = 16 HORIZON = 128 class Medium(Config): '''A medium config suitable for most academic-scale research''' PATH_MAPS = 'maps/medium' PLAYER_N = 128 MAP_PREVIEW_DOWNSCALE = 16 MAP_SIZE = 160 MAP_CENTER = 128 NPC_N = 128 NPC_LEVEL_MAX = 10 NPC_LEVEL_SPREAD = 1 PROGRESSION_SPAWN_CLUSTERS = 64 PROGRESSION_SPAWN_UNIFORMS = 256 HORIZON = 1024 class Large(Config): '''A large config suitable for large-scale research or fast models''' PATH_MAPS = 'maps/large' PLAYER_N = 1024 MAP_PREVIEW_DOWNSCALE = 64 MAP_SIZE = 1056 MAP_CENTER = 1024 NPC_N = 1024 NPC_LEVEL_MAX = 15 NPC_LEVEL_SPREAD = 3 PROGRESSION_SPAWN_CLUSTERS = 1024 PROGRESSION_SPAWN_UNIFORMS = 4096 HORIZON = 8192 class Default(Medium, AllGameSystems): pass ================================================ FILE: nmmo/core/env.py ================================================ import os import functools from typing import Any, Dict, List, Callable from collections import defaultdict from copy import deepcopy import gymnasium as gym import dill import numpy as np from pettingzoo.utils.env import AgentID, ParallelEnv import nmmo from nmmo.core import realm from nmmo.core import game_api from nmmo.core.config import Default from nmmo.core.observation import Observation from nmmo.core.tile import Tile from nmmo.entity.entity import Entity from nmmo.systems.item import Item from nmmo.task.game_state import GameStateGenerator from nmmo.lib import seeding class Env(ParallelEnv): # Environment wrapper for Neural MMO using the Parallel PettingZoo API #pylint: disable=no-value-for-parameter def __init__(self, config: Default = nmmo.config.Default(), seed = None): '''Initializes the Neural MMO environment. Args: config (Default, optional): Configuration object for the environment. Defaults to nmmo.config.Default(). seed (int, optional): Random seed for the environment. Defaults to None. ''' self._np_random = None self._np_seed = None self._reset_required = True self.seed(seed) super().__init__() self.config = config self.config.env_initialized = True # Generate maps if they do not exist config.MAP_GENERATOR(config).generate_all_maps(self._np_seed) self.realm = realm.Realm(config, self._np_random) self.tile_map = None self.tile_obs_shape = None self.possible_agents = self.config.POSSIBLE_AGENTS self._alive_agents = None self._current_agents = None self._dead_this_tick = None self.scripted_agents = set() self.obs = {agent_id: Observation(self.config, agent_id) for agent_id in self.possible_agents} self._dummy_task_embedding = np.zeros(self.config.TASK_EMBED_DIM, dtype=np.float16) self._dummy_obs = Observation(self.config, 0).empty_obs self._comm_obs = {} self._gamestate_generator = GameStateGenerator(self.realm, self.config) self.game_state = None self.tasks = None self.agent_task_map = {} # curriculum file path, if provided, should exist self.curriculum_file_path = config.CURRICULUM_FILE_PATH if self.curriculum_file_path is not None: # try to open the file to check if it exists with open(self.curriculum_file_path, 'rb') as f: dill.load(f) f.close() self.game = None # NOTE: The default game runs with the full provided config and unmodded realm.reset() self.default_game = game_api.DefaultGame(self) self.game_packs: List[game_api.Game] = None if config.GAME_PACKS: # assume List[Tuple(class, weight)] self.game_packs = [game_cls(self, weight) for game_cls, weight in config.GAME_PACKS] @functools.cached_property def _obs_space(self): def box(rows, cols): return gym.spaces.Box( low=-2**15, high=2**15-1, shape=(rows, cols), dtype=np.int16) def mask_box(length): return gym.spaces.Box(low=0, high=1, shape=(length,), dtype=np.int8) # NOTE: obs space-related config attributes must NOT be changed after init num_tile_attributes = len(Tile.State.attr_name_to_col) num_tile_attributes += 1 if self.config.original["PROVIDE_DEATH_FOG_OBS"] else 0 obs_space = { "CurrentTick": gym.spaces.Discrete(self.config.MAX_HORIZON), "AgentId": gym.spaces.Discrete(self.config.PLAYER_N+1), "Tile": box(self.config.MAP_N_OBS, num_tile_attributes), "Entity": box(self.config.PLAYER_N_OBS, Entity.State.num_attributes), "Task": gym.spaces.Box(low=-2**15, high=2**15-1, shape=(self.config.TASK_EMBED_DIM,), dtype=np.float16), } # NOTE: cannot turn on a game system that was not enabled during env init if self.config.original["ITEM_SYSTEM_ENABLED"]: obs_space["Inventory"] = box(self.config.INVENTORY_N_OBS, Item.State.num_attributes) if self.config.original["EXCHANGE_SYSTEM_ENABLED"]: obs_space["Market"] = box(self.config.MARKET_N_OBS, Item.State.num_attributes) if self.config.original["COMMUNICATION_SYSTEM_ENABLED"]: # Comm obs cols: id, row, col, message obs_space["Communication"] = box(self.config.COMMUNICATION_N_OBS, 4) if self.config.original["PROVIDE_ACTION_TARGETS"]: mask_spec = deepcopy(self._atn_space) for atn_str in mask_spec: for arg_str in mask_spec[atn_str]: mask_spec[atn_str][arg_str] = mask_box(self._atn_space[atn_str][arg_str].n) obs_space["ActionTargets"] = mask_spec return gym.spaces.Dict(obs_space) # pylint: disable=method-cache-max-size-none @functools.lru_cache(maxsize=None) def observation_space(self, agent: AgentID): '''Neural MMO Observation Space Args: agent (AgentID): The ID of the agent. Returns: gym.spaces.Dict: The observation space for the agent. ''' return self._obs_space # NOTE: make sure this runs once during trainer init and does NOT change afterwards @functools.cached_property def _atn_space(self): actions = {} for atn in sorted(nmmo.Action.edges(self.config)): if atn.enabled(self.config): actions[atn.__name__] = {} # use the string key for arg in sorted(atn.edges): n = arg.N(self.config) actions[atn.__name__][arg.__name__] = gym.spaces.Discrete(n) actions[atn.__name__] = gym.spaces.Dict(actions[atn.__name__]) return gym.spaces.Dict(actions) @functools.cached_property def _str_atn_map(self): '''Map action and argument names to their corresponding objects''' str_map = {} for atn in nmmo.Action.edges(self.config): str_map[atn.__name__] = atn for arg in atn.edges: str_map[arg.__name__] = arg return str_map # pylint: disable=method-cache-max-size-none @functools.lru_cache(maxsize=None) def action_space(self, agent: AgentID): '''Neural MMO Action Space Args: agent (AgentID): The ID of the agent. Returns: gym.spaces.Dict: The action space for the agent. ''' return self._atn_space ############################################################################ # Core API def reset(self, seed=None, options=None, # PettingZoo API args map_id=None, make_task_fn: Callable=None, game: game_api.Game=None): '''Resets the environment and returns the initial observations. Args: seed (int, optional): Random seed for the environment. Defaults to None. options (dict, optional): Additional options for resetting the environment. Defaults to None. map_id (int, optional): The ID of the map to load. Defaults to None. make_task_fn (callable, optional): Function to create tasks. Defaults to None. game (Game, optional): The game to be played. Defaults to None. Returns: tuple: A tuple containing: - obs (dict): Dictionary mapping agent IDs to their initial observations. - info (dict): Dictionary containing additional information. ''' # If options are provided, override the kwargs if options is not None: map_id = options.get('map_id', None) or map_id make_task_fn = options.get('make_task_fn', None) or make_task_fn game = options.get('game', None) or game self.seed(seed) map_dict = self._load_map_file(map_id) # Choose and reset the game, realm, and tasks if make_task_fn is not None: # Use the provided tasks with the default game (full config, unmodded realm) self.tasks = make_task_fn() self.game = self.default_game self.game.reset(self._np_random, map_dict, self.tasks) # also does realm.reset() elif game is not None: # Use the provided game, which comes with its own tasks self.game = game self.game.reset(self._np_random, map_dict) self.tasks = self.game.tasks elif self.curriculum_file_path is not None or self.game_packs is not None: # Assume training -- pick a random game from the game packs self.game = self.default_game if self.game_packs: weights = [game.sampling_weight for game in self.game_packs] self.game = self._np_random.choice(self.game_packs, p=weights/np.sum(weights)) self.game.reset(self._np_random, map_dict) # use the sampled tasks from self.game self.tasks = self.game.tasks else: # Just reset the same game and tasks as before self.game = self.default_game # full config, unmodded realm self.game.reset(self._np_random, map_dict, self.tasks) # use existing tasks if self.tasks is None: self.tasks = self.game.tasks else: for task in self.tasks: task.reset() # Reset the agent vars self._alive_agents = self.possible_agents self._dead_this_tick = {} self._map_task_to_agent() self._current_agents = self.possible_agents # tracking alive + dead_this_tick # Check scripted agents self.scripted_agents.clear() for eid, ent in self.realm.players.items(): if isinstance(ent.agent, nmmo.Scripted): self.scripted_agents.add(eid) ent.agent.set_rng(self._np_random) # Tile map placeholder, to reduce redudunt obs computation self.tile_map = Tile.Query.get_map(self.realm.datastore, self.config.MAP_SIZE) if self.config.PROVIDE_DEATH_FOG_OBS: fog_map = np.round(self.realm.fog_map)[:,:,np.newaxis].astype(np.int16) self.tile_map = np.concatenate((self.tile_map, fog_map), axis=-1) self.tile_obs_shape = (self.config.PLAYER_VISION_DIAMETER**2, self.tile_map.shape[-1]) # Reset the obs, game state generator infos = {} for agent_id in self.possible_agents: # NOTE: the tasks for each agent is in self.agent_task_map, and task embeddings are # available in each task instance, via task.embedding # For now, each agent is assigned to a single task, so we just use the first task # TODO: can the embeddings of multiple tasks be superposed while preserving the # task-specific information? This needs research task_embedding = self._dummy_task_embedding if agent_id in self.agent_task_map: task_embedding = self.agent_task_map[agent_id][0].embedding infos[agent_id] = {"task": self.agent_task_map[agent_id][0].name} self.obs[agent_id].reset(self.realm.map.habitable_tiles, task_embedding) self._compute_observations() self._gamestate_generator = GameStateGenerator(self.realm, self.config) if self.game_state is not None: self.game_state.clear_cache() self.game_state = None self._reset_required = False return {a: o.to_gym() for a,o in self.obs.items()}, infos def _load_map_file(self, map_id: int=None): '''Loads a map file, which is a 2D numpy array''' map_dict= {} map_id = map_id or self._np_random.integers(self.config.MAP_N) + 1 map_file_path = os.path.join(self.config.PATH_CWD, self.config.PATH_MAPS, self.config.PATH_MAP_SUFFIX.format(map_id)) map_dict["map"] = np.load(map_file_path) if self.config.MAP_RESET_FROM_FRACTAL: fractal_file_path = os.path.join(self.config.PATH_CWD, self.config.PATH_MAPS, self.config.PATH_FRACTAL_SUFFIX.format(map_id)) map_dict["fractal"] = np.load(fractal_file_path).astype(float) return map_dict def _map_task_to_agent(self): self.agent_task_map.clear() for agent_id in self.agents: self.realm.players[agent_id].my_task = None for task in self.tasks: if task.embedding is None: task.set_embedding(self._dummy_task_embedding) # map task to agents for agent_id in task.assignee: if agent_id in self.agent_task_map: self.agent_task_map[agent_id].append(task) else: self.agent_task_map[agent_id] = [task] # for now we only support one task per agent if self.config.ALLOW_MULTI_TASKS_PER_AGENT is False: for agent_id, agent_tasks in self.agent_task_map.items(): assert len(agent_tasks) == 1, "Only one task per agent is supported" self.realm.players[agent_id].my_task = agent_tasks[0] def step(self, actions: Dict[int, Dict[str, Dict[str, Any]]]): '''Performs one step in the environment given the provided actions. Args: actions (dict): Dictionary mapping agent IDs to their actions. Returns: tuple: A tuple containing: - obs (dict): Dictionary mapping agent IDs to their new observations. - rewards (dict): Dictionary mapping agent IDs to their rewards. - terminated (dict): Dictionary mapping agent IDs to whether they reached a terminal state. - truncated (dict): Dictionary mapping agent IDs to whether the episode was truncated (e.g. reached maximum number of steps). - infos (dict): Dictionary containing additional information. ''' assert not self._reset_required, 'step() called before reset' # Add in scripted agents' actions, if any if self.scripted_agents: actions = self._compute_scripted_agent_actions(actions) # Drop invalid actions of BOTH neural and scripted agents # we don't need _deserialize_scripted_actions() anymore actions = self._validate_actions(actions) # Execute actions self._dead_this_tick, dead_npcs = self.realm.step(actions) self._alive_agents = list(self.realm.players.keys()) self._current_agents = list(set(self._alive_agents + list(self._dead_this_tick.keys()))) terminated = {} for agent_id in self._current_agents: if agent_id in self._dead_this_tick: # NOTE: Even though players can be resurrected, the time of death must be marked. terminated[agent_id] = True else: terminated[agent_id] = False if self.realm.tick >= self.config.HORIZON: self._alive_agents = [] # pettingzoo requires agents to be empty # Update the game stats, determine winners, etc. # Also, resurrect dead agents and/or spawn new npcs if the game allows it self.game.update(terminated, self._dead_this_tick, dead_npcs) # Some games do additional player cull during update(), so process truncated here truncated = {} for agent_id in self._current_agents: if self.realm.tick >= self.config.HORIZON: truncated[agent_id] = agent_id in self.realm.players else: truncated[agent_id] = False # Store the observations, since actions reference them self._compute_observations() gym_obs = {a: self.obs[a].to_gym() for a in self._current_agents} rewards, infos = self._compute_rewards() # NOTE: all obs, rewards, dones, infos have data for each agent in self.agents return gym_obs, rewards, terminated, truncated, infos @property def dead_this_tick(self): return self._dead_this_tick def _validate_actions(self, actions: Dict[int, Dict[str, Dict[str, Any]]]): '''Deserialize action arg values and validate actions For now, it does a basic validation (e.g., value is not none). ''' validated_actions = {} for ent_id, atns in actions.items(): if ent_id not in self.realm.players: #assert ent_id in self.realm.players, f'Entity {ent_id} not in realm' continue # Entity not in the realm -- invalid actions entity = self.realm.players[ent_id] if not entity.alive: #assert entity.alive, f'Entity {ent_id} is dead' continue # Entity is dead -- invalid actions validated_actions[ent_id] = {} for atn_key, args in sorted(atns.items()): action_valid = True deserialized_action = {} # If action/system is not enabled, it's not in self._str_atn_map if isinstance(atn_key, str) and atn_key not in self._str_atn_map: action_valid = False continue atn = self._str_atn_map[atn_key] if isinstance(atn_key, str) else atn_key if not atn.enabled(self.config): # This can change from episode to episode action_valid = False continue for arg_key, val in sorted(args.items()): arg = self._str_atn_map[arg_key] if isinstance(arg_key, str) else arg_key obj = arg.deserialize(self.realm, entity, val, self.obs[ent_id]) if obj is None: action_valid = False break deserialized_action[arg] = obj if action_valid: validated_actions[ent_id][atn] = deserialized_action return validated_actions def _compute_scripted_agent_actions(self, actions: Dict[int, Dict[str, Dict[str, Any]]]): '''Compute actions for scripted agents and add them into the action dict''' dead_agents = set() for agent_id in self.scripted_agents: if agent_id in self.realm.players: # override the provided scripted agents' actions actions[agent_id] = self.realm.players[agent_id].agent(self.obs[agent_id]) else: dead_agents.add(agent_id) # remove the dead scripted agent from the list self.scripted_agents -= dead_agents return actions def _compute_observations(self): radius = self.config.PLAYER_VISION_RADIUS market = Item.Query.for_sale(self.realm.datastore) \ if self.config.EXCHANGE_SYSTEM_ENABLED else None self._update_comm_obs() if self.config.PROVIDE_DEATH_FOG_OBS: self.tile_map[:, :, -1] = np.round(self.realm.fog_map) for agent_id in self._current_agents: if agent_id not in self.realm.players: self.obs[agent_id].set_agent_dead() else: r, c = self.realm.players.get(agent_id).pos visible_entities = Entity.Query.window(self.realm.datastore, r, c, radius) visible_tiles = self.tile_map[r-radius:r+radius+1, c-radius:c+radius+1, :].reshape(self.tile_obs_shape) inventory = Item.Query.owned_by(self.realm.datastore, agent_id) \ if self.config.ITEM_SYSTEM_ENABLED else None comm_obs = self._comm_obs[agent_id] \ if self.config.COMMUNICATION_SYSTEM_ENABLED else None self.obs[agent_id].update(self.realm.tick, visible_tiles, visible_entities, inventory=inventory, market=market, comm=comm_obs) def _update_comm_obs(self): if not self.config.COMMUNICATION_SYSTEM_ENABLED: return comm_obs = Entity.Query.comm_obs(self.realm.datastore) agent_ids = comm_obs[:, Entity.State.attr_name_to_col['id']] self._comm_obs.clear() for agent_id in self.realm.players: if agent_id not in self._comm_obs: my_team = [agent_id] if agent_id not in self.agent_task_map \ else self.agent_task_map[agent_id][0].assignee # NOTE: first task only team_obs = [comm_obs[agent_ids == eid] for eid in my_team] if len(team_obs) == 1: team_obs = team_obs[0] else: team_obs = np.concatenate(team_obs, axis=0) for eid in my_team: self._comm_obs[eid] = team_obs def _compute_rewards(self): # Initialization agents = set(self._current_agents) infos = {agent_id: {'task': {}} for agent_id in agents} rewards = defaultdict(int) # Clean up unnecessary game state, which cause memory leaks if self.game_state is not None: self.game_state.clear_cache() self.game_state = None # Compute Rewards and infos self.game_state = self._gamestate_generator.generate(self.realm, self.obs) for task in self.tasks: if agents.intersection(task.assignee): # evaluate only if the agents are current task_rewards, task_infos = task.compute_rewards(self.game_state) for agent_id, reward in task_rewards.items(): if agent_id in agents: rewards[agent_id] = rewards.get(agent_id,0) + reward infos[agent_id]['task'][task.name] = task_infos[agent_id] # include progress, etc. else: task.close() # To prevent memory leak # Reward for frozen agents (recon, resurrected, frozen) is 0 because they cannot act for agent_id, agent in self.realm.players.items(): if agent.status.frozen: rewards[agent_id] = 0 # Reward for dead agents is defined by the game # NOTE: Resurrected agents are frozen and in the realm.players, so run through # self._dead_this_tick to give out the dead reward if self.game.assign_dead_reward: for agent_id in self._dead_this_tick: rewards[agent_id] = -1 return rewards, infos ############################################################################ # PettingZoo API ############################################################################ def render(self, mode='human'): '''For conformity with the PettingZoo API only; rendering is external''' @property def agents(self) -> List[AgentID]: '''For conformity with the PettingZoo API; retuning only the alive agents''' return self._alive_agents def close(self): '''For conformity with the PettingZoo API only; rendering is external''' def seed(self, seed=None): '''Reseeds the environment. reset() must be called after seed(), and before step(). - self._np_seed is None: seed() has not been called, e.g. __init__() -> new RNG - self._np_seed is set, and seed is not None: seed() or reset() with seed -> new RNG If self._np_seed is set, but seed is None probably called from reset() without seed, so don't change the RNG ''' if self._np_seed is None or seed is not None: self._np_random, self._np_seed = seeding.np_random(seed) self._reset_required = True def state(self) -> np.ndarray: raise NotImplementedError metadata = {'render.modes': ['human'], 'name': 'neural-mmo'} ================================================ FILE: nmmo/core/game_api.py ================================================ # pylint: disable=no-member,bare-except from abc import ABC, abstractmethod from typing import Dict from collections import deque import dill import numpy as np from nmmo.task import task_api, task_spec, base_predicates from nmmo.lib import team_helper, utils GAME_MODE = ["agent_training", "team_training", "team_battle"] class Game(ABC): game_mode = None def __init__(self, env, sampling_weight=None): self.config = env.config self.realm = env.realm self._np_random = env._np_random self.sampling_weight = sampling_weight or 1.0 self.tasks = None self.assign_dead_reward = True self._next_tasks = None self._agent_stats = {} self._winners = None self._game_done = False self.history: deque[Dict] = deque(maxlen=100) assert self.is_compatible(), "Game is not compatible with the config" @abstractmethod def is_compatible(self): """Check if the game is compatible with the config (e.g., required systems)""" raise NotImplementedError @property def name(self): return self.__class__.__name__ @property def winners(self): return self._winners @property def winning_score(self): if self._winners: # CHECK ME: should we return the winners" tasks" reward multiplier? return 1.0 # default score for task completion return 0.0 def reset(self, np_random, map_dict, tasks=None): self._np_random = np_random self._set_config() self._set_realm(map_dict) if tasks: # tasks comes from env.reset() self.tasks = tasks elif self._next_tasks: # env.reset() cannot take both game and tasks # so set next_tasks in the game first self.tasks = self._next_tasks self._next_tasks = None else: self.tasks = self._define_tasks() self._post_setup() self._reset_stats() def _set_config(self): # pylint: disable=unused-argument """Set config for the episode. Can customize config using config.set_for_episode()""" self.config.reset() def _set_realm(self, map_dict): """Set up the realm for the episode. Can customize map and spawn""" self.realm.reset(self._np_random, map_dict, custom_spawn=False) def _post_setup(self): """Post-setup processes, e.g., attach team tags, etc.""" def _reset_stats(self): """Reset stats for the episode""" self._agent_stats.clear() self._winners = None self._game_done = False # result = False means the game ended without a winner self.history.append({"result": False, "winners": None, "winning_score": None}) @abstractmethod def _define_tasks(self): """Define tasks for the episode.""" # NOTE: Task embeddings should be provided somehow, e.g., from curriculum file. # Otherwise, policies cannot be task-conditioned. raise NotImplementedError def set_next_tasks(self, tasks): """Set the next task to be completed""" self._next_tasks = tasks def update(self, terminated, dead_players, dead_npcs): """Process dead players/npcs, update the game stats, winners, etc.""" self._process_dead_players(terminated, dead_players) self._process_dead_npcs(dead_npcs) self._winners = self._check_winners(terminated) if self._winners and not self._game_done: self._game_done = self.history[-1]["result"] = True self.history[-1]["winners"] = self._winners self.history[-1]["winning_score"] = self.winning_score self.history[-1]["winning_tick"] = self.realm.tick self.history[-1].update(self.get_episode_stats()) def _process_dead_players(self, terminated, dead_players): for agent_id in terminated: if terminated[agent_id]: agent = dead_players[agent_id] if agent_id in dead_players\ else self.realm.players[agent_id] self._agent_stats[agent_id] = {"time_alive": self.realm.tick, "progress_to_center": agent.history.exploration} def _process_dead_npcs(self, dead_npcs): if self.config.NPC_SYSTEM_ENABLED and self.config.NPC_DEFAULT_REFILL_DEAD_NPCS: for npc in dead_npcs.values(): if npc.spawn_danger: self.realm.npcs.spawn_dangers.append(npc.spawn_danger) # refill npcs to target config.NPC_N, within config.NPC_SPAWN_ATTEMPTS self.realm.npcs.default_spawn() def _check_winners(self, terminated): # Determine winners for the default task if self.realm.num_players == 1: # only one survivor return list(self.realm.players.keys()) if all(terminated.values()): # declare all winners when they died at the same time return list(terminated.keys()) if self.realm.tick >= self.config.HORIZON: # declare all survivors as winners when the time is up return [agent_id for agent_id, done in terminated.items() if not done] return None @property def is_over(self): return self.winners is not None or self.realm.num_players == 0 or \ self.realm.tick >= self.config.HORIZON def get_episode_stats(self): """A helper function for trainers""" total_agent_steps = 0 progress_to_center = 0 max_progress = self.config.PLAYER_N * self.config.MAP_SIZE // 2 for stat in self._agent_stats.values(): total_agent_steps += stat["time_alive"] progress_to_center += stat["progress_to_center"] return { "total_agent_steps": total_agent_steps, "norm_progress_to_center": float(progress_to_center) / max_progress } ############################ # Helper functions for Game def _who_completed_task(self): # Return all assignees who completed their tasks winners = [] for task in self.tasks: if task.completed: winners += task.assignee return winners or None class DefaultGame(Game): """The default NMMO game""" game_mode = "agent_training" def is_compatible(self): return True def _define_tasks(self): return task_api.nmmo_default_task(self.config.POSSIBLE_AGENTS) class AgentTraining(Game): """Game setting for agent training tasks""" game_mode = "agent_training" @property def winning_score(self): return 0.0 def is_compatible(self): try: # Check is the curriculum file exists and opens with open(self.config.CURRICULUM_FILE_PATH, "rb") as f: dill.load(f) # a list of TaskSpec except: return False return True def _define_tasks(self): with open(self.config.CURRICULUM_FILE_PATH, "rb") as f: # curriculum file may have been changed, so read the file when sampling curriculum = dill.load(f) # a list of TaskSpec cand_specs = [spec for spec in curriculum if spec.reward_to == "agent"] assert len(cand_specs) > 0, "No agent task is defined in the curriculum file" sampling_weights = [spec.sampling_weight for spec in cand_specs] sampled_spec = self._np_random.choice(cand_specs, size=self.config.PLAYER_N, p=sampling_weights/np.sum(sampling_weights)) return task_spec.make_task_from_spec(self.config.POSSIBLE_AGENTS, sampled_spec) class TeamGameTemplate(Game): """A helper class with common utils for team games""" assign_dead_reward = False # Do NOT always assign -1 to dead agents def is_compatible(self): try: assert self.config.TEAMS is not None, "Team game requires TEAMS to be defined" num_agents = sum(len(v) for v in self.config.TEAMS.values()) assert self.config.PLAYER_N == num_agents,\ "PLAYER_N must match the number of agents in TEAMS" # Check is the curriculum file exists and opens with open(self.config.CURRICULUM_FILE_PATH, "rb") as f: dill.load(f) # a list of TaskSpec except: return False return True def _set_realm(self, map_dict): self.realm.reset(self._np_random, map_dict, custom_spawn=True) # Custom spawning team_loader = team_helper.TeamLoader(self.config, self._np_random) self.realm.players.spawn(team_loader) self.realm.npcs.default_spawn() def _post_setup(self): self._attach_team_tag() @property def teams(self): return self.config.TEAMS def _attach_team_tag(self): # setup team names for team_id, members in self.teams.items(): if isinstance(team_id, int): team_id = f"Team{team_id:02d}" for idx, agent_id in enumerate(members): self.realm.players[agent_id].name = f"{team_id}_{agent_id}" if idx == 0: self.realm.players[agent_id].name = f"{team_id}_leader" def _get_cand_team_tasks(self, num_tasks, tags=None): # NOTE: use different file to store different set of tasks? with open(self.config.CURRICULUM_FILE_PATH, "rb") as f: curriculum = dill.load(f) # a list of TaskSpec cand_specs = [spec for spec in curriculum if spec.reward_to == "team"] if tags: cand_specs = [spec for spec in cand_specs if tags in spec.tags] assert len(cand_specs) > 0, "No team task is defined in the curriculum file" sampling_weights = [spec.sampling_weight for spec in cand_specs] sampled_spec = self._np_random.choice(cand_specs, size=num_tasks, p=sampling_weights/np.sum(sampling_weights)) return sampled_spec class TeamTraining(TeamGameTemplate): """Game setting for team training tasks""" game_mode = "team_training" def _define_tasks(self): sampled_spec = self._get_cand_team_tasks(len(self.config.TEAMS)) return task_spec.make_task_from_spec(self.config.TEAMS, sampled_spec) def team_survival_task(num_tick, embedding=None): return task_spec.TaskSpec( eval_fn=base_predicates.TickGE, eval_fn_kwargs={"num_tick": num_tick}, reward_to="team", embedding=embedding) class TeamBattle(TeamGameTemplate): """Game setting for team battle""" game_mode = "team_battle" def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self.task_embedding = utils.get_hash_embedding(base_predicates.TickGE, self.config.TASK_EMBED_DIM) def is_compatible(self): assert self.config.are_systems_enabled(["COMBAT"]), "Combat system must be enabled" assert self.config.TEAMS is not None, "Team battle mode requires TEAMS to be defined" num_agents = sum(len(v) for v in self.config.TEAMS.values()) assert self.config.PLAYER_N == num_agents,\ "PLAYER_N must match the number of agents in TEAMS" return True def _define_tasks(self): # NOTE: Teams can win by eliminating all other teams, # or fully cooperating to survive for the entire episode survive_task = team_survival_task(self.config.HORIZON, self.task_embedding) return task_spec.make_task_from_spec(self.config.TEAMS, [survive_task] * len(self.config.TEAMS)) def _check_winners(self, terminated): # A team is won, when their task is completed first or only one team remains current_teams = self._check_remaining_teams() if len(current_teams) == 1: winner_team = list(current_teams.keys())[0] return self.config.TEAMS[winner_team] # Return all assignees who completed their tasks # Assuming the episode gets ended externally return self._who_completed_task() def _check_remaining_teams(self): current_teams = {} for team_id, team in self.config.TEAMS.items(): alive_members = [agent_id for agent_id in team if agent_id in self.realm.players] if len(alive_members) > 0: current_teams[team_id] = alive_members return current_teams class ProtectTheKing(TeamBattle): def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self.team_helper = team_helper.TeamHelper(self.config.TEAMS) self.task_embedding = utils.get_hash_embedding(base_predicates.ProtectLeader, self.config.TASK_EMBED_DIM) def _define_tasks(self): protect_task = task_spec.TaskSpec( eval_fn=base_predicates.ProtectLeader, eval_fn_kwargs={ "target_protect": "my_team_leader", "target_destroy": "all_foe_leaders", }, reward_to="team" ) return task_spec.make_task_from_spec(self.config.TEAMS, [protect_task] * len(self.config.TEAMS)) def update(self, terminated, dead_players, dead_npcs): # If a team's leader is dead, the whole team is dead for team_id, members in self.config.TEAMS.items(): if self.team_helper.get_target_agent(team_id, "my_team_leader") in dead_players: for agent_id in members: if agent_id in self.realm.players: self.realm.players[agent_id].health.update(0) # Addition dead players cull for agent in [agent for agent in self.realm.players.values() if not agent.alive]: agent_id = agent.ent_id self.realm.players.dead_this_tick[agent_id] = agent self.realm.players.cull_entity(agent) agent.datastore_record.delete() terminated[agent_id] = True super().update(terminated, dead_players, dead_npcs) ================================================ FILE: nmmo/core/map.py ================================================ from typing import List, Tuple import numpy as np from ordered_set import OrderedSet from nmmo.core.tile import Tile from nmmo.lib import material, utils from nmmo.core.terrain import ( fractal_to_material, process_map_border, spawn_profession_resources, scatter_extra_resources, ) class Map: '''Map object representing a list of tiles Also tracks a sparse list of tile updates ''' def __init__(self, config, realm, np_random): self.config = config self._repr = None self.realm = realm self.update_list = None self.pathfinding_cache = {} # Avoid recalculating A*, paths don't move sz = config.MAP_SIZE self.tiles = np.zeros((sz,sz), dtype=object) self.habitable_tiles = np.zeros((sz,sz), dtype=np.int8) for r in range(sz): for c in range(sz): self.tiles[r, c] = Tile(realm, r, c, np_random) # the map center, and the centers in each quadrant are important targets self.dist_border_center = None self.center_coord = None self.quad_centers = None self.seize_targets: List[Tuple] = None # a list of (r, c) coords # used to place border self.l1 = utils.l1_map(sz) @property def packet(self): '''Packet of degenerate resource states''' missing_resources = [] for e in self.update_list: missing_resources.append(e.pos) return missing_resources @property def repr(self): '''Flat matrix of tile material indices''' if not self._repr: self._repr = [[t.material.index for t in row] for row in self.tiles] return self._repr def reset(self, map_dict, np_random, seize_targets=None): '''Reuse the current tile objects to load a new map''' config = self.config assert map_dict["map"].shape == (config.MAP_SIZE,config.MAP_SIZE),\ "Map shape is inconsistent with config.MAP_SIZE" # NOTE: MAP_CENTER and MAP_BORDER can change from episode to episode self.center_coord = (config.MAP_SIZE//2, config.MAP_SIZE//2) self.dist_border_center = config.MAP_CENTER // 2 half_dist = self.dist_border_center // 2 self.quad_centers = { "first": (self.center_coord[0] + half_dist, self.center_coord[1] + half_dist), "second": (self.center_coord[0] - half_dist, self.center_coord[1] + half_dist), "third": (self.center_coord[0] - half_dist, self.center_coord[1] - half_dist), "fourth": (self.center_coord[0] + half_dist, self.center_coord[1] - half_dist), } assert config.MAP_BORDER > config.PLAYER_VISION_RADIUS,\ "MAP_BORDER must be greater than PLAYER_VISION_RADIUS" self._repr = None self.update_list = OrderedSet() # critical for determinism self.seize_targets = [] if seize_targets: assert isinstance(seize_targets, list), "seize_targets must be a list of reserved words" for target in seize_targets: # pylint: disable=consider-iterating-dictionary assert target in list(self.quad_centers.keys()) + ["center"], "Invalid seize target" self.seize_targets.append(self.center_coord if target == "center" else self.quad_centers[target]) # process map_np_array according to config matl_map = self._process_map(map_dict, np_random) if "mark_center" in map_dict and map_dict["mark_center"]: self._mark_tile(matl_map, *self.center_coord) for r, c in self.seize_targets: self._mark_tile(matl_map, r, c) # reset tiles with new materials materials = {mat.index: mat for mat in material.All} for r, row in enumerate(matl_map): for c, idx in enumerate(row): mat = materials[idx] tile = self.tiles[r, c] tile.reset(mat, config, np_random) self.habitable_tiles[r, c] = tile.habitable def _process_map(self, map_dict, np_random): map_np_array = map_dict["map"] if not self.config.TERRAIN_SYSTEM_ENABLED: map_np_array[:] = material.Grass.index else: if self.config.MAP_RESET_FROM_FRACTAL: map_tiles = fractal_to_material(self.config, map_dict["fractal"], self.config.TERRAIN_RESET_TO_GRASS) # Place materials here, before converting map_tiles into an int array if self.config.PROFESSION_SYSTEM_ENABLED: spawn_profession_resources(self.config, map_tiles, np_random) if self.config.TERRAIN_SCATTER_EXTRA_RESOURCES: scatter_extra_resources(self.config, map_tiles, np_random) map_np_array = map_tiles.astype(int) # Disable materials here if self.config.TERRAIN_DISABLE_STONE: map_np_array[map_np_array == material.Stone.index] = material.Grass.index # Make the edge tiles habitable, and place the void tiles outside the border map_np_array = process_map_border(self.config, map_np_array, self.l1) return map_np_array @staticmethod def _mark_tile(map_np_array, row, col, dist=2): map_np_array[row-dist:row+dist+1,col-dist:col+dist+1] = material.Grass.index map_np_array[row,col] = material.Herb.index def step(self): '''Evaluate updatable tiles''' for tile in self.update_list.copy(): if not tile.depleted: self.update_list.remove(tile) tile.step() if self.seize_targets: for r, c in self.seize_targets: self.tiles[r, c].update_seize() def harvest(self, r, c, deplete=True): '''Called by actions that harvest a resource tile''' if deplete: self.update_list.add(self.tiles[r, c]) return self.tiles[r, c].harvest(deplete) def is_valid_pos(self, row, col): '''Check if a position is valid''' return 0 <= row < self.config.MAP_SIZE and 0 <= col < self.config.MAP_SIZE def make_spawnable(self, row, col, radius=2): '''Make the area centered around row, col spawnable''' assert self._repr is None, "Cannot make spawnable after map is generated" assert radius > 0, "Radius must be positive" assert self.config.MAP_BORDER < row-radius and self.config.MAP_BORDER < col-radius \ and row+radius < self.config.MAP_SIZE-self.config.MAP_BORDER \ and col+radius < self.config.MAP_SIZE-self.config.MAP_BORDER,\ "Cannot make spawnable near the border" for r in range(row-radius, row+radius+1): for c in range(col-radius, col+radius+1): tile = self.tiles[r, c] # pylint: disable=protected-access tile.reset(material.Grass, self.config, self.realm._np_random) self.habitable_tiles[r, c] = tile.habitable # must be true @property def seize_status(self): if self.seize_targets is None: return {} return { (r, c): self.tiles[r, c].seize_history[-1] for r, c in self.seize_targets if self.tiles[r, c].seize_history } ================================================ FILE: nmmo/core/observation.py ================================================ # pylint: disable=no-member,c-extension-no-member from functools import lru_cache import numpy as np from nmmo.core.tile import TileState from nmmo.entity.entity import EntityState from nmmo.systems.item import ItemState import nmmo.systems.item as item_system from nmmo.core import action from nmmo.lib import material import nmmo.lib.cython_helper as chp ROW_DELTA = np.array([-1, 1, 0, 0], dtype=np.int64) COL_DELTA = np.array([0, 0, 1, -1], dtype=np.int64) EMPTY_TILE = TileState.parse_array( np.array([0, 0, material.Void.index], dtype=np.int16)) class BasicObs: def __init__(self, id_col, obs_dim): self.values = None self.ids = None self.id_col = id_col self.obs_dim = obs_dim def reset(self): self.values = None self.ids = None def update(self, values): self.values = values[:self.obs_dim] self.ids = values[:, self.id_col] @property def len(self): return self.ids.shape[0] def id(self, i): return self.ids[i] if i < self.len else None def index(self, val): return np.nonzero(self.ids == val)[0][0] if val in self.ids else None class InventoryObs(BasicObs): def __init__(self, id_col, obs_dim): super().__init__(id_col, obs_dim) self.inv_type = None self.inv_level = None def update(self, values): super().update(values) self.inv_type = self.values[:,ItemState.State.attr_name_to_col["type_id"]] self.inv_level = self.values[:,ItemState.State.attr_name_to_col["level"]] def sig(self, item: item_system.Item, level: int): idx = np.nonzero((self.inv_type == item.ITEM_TYPE_ID) & (self.inv_level == level))[0] return idx[0] if len(idx) else None class GymObs: keys_to_clear = ["Tile", "Entity", "Inventory", "Market", "Communication"] def __init__(self, config, agent_id): self.config = config self.agent_id = agent_id self.values = self._make_empty_obs() def reset(self, task_embedding=None): self.clear() self.values["Task"][:] = 0 if task_embedding is None else task_embedding def clear(self, tick=None): self.values["CurrentTick"] = tick or 0 for key in self.keys_to_clear: if key in self.values: if key == "Inventory" and not self.config.ITEM_SYSTEM_ENABLED: continue if key == "Market" and not self.config.EXCHANGE_SYSTEM_ENABLED: continue if key == "Communication" and not self.config.COMMUNICATION_SYSTEM_ENABLED: continue self.values[key][:] = 0 def _make_empty_obs(self): num_tile_attributes = TileState.State.num_attributes num_tile_attributes += 1 if self.config.original["PROVIDE_DEATH_FOG_OBS"] else 0 gym_obs = { "CurrentTick": 0, "AgentId": self.agent_id, "Task": np.zeros(self.config.TASK_EMBED_DIM, dtype=np.float16), "Tile": np.zeros((self.config.MAP_N_OBS, num_tile_attributes), dtype=np.int16), "Entity": np.zeros((self.config.PLAYER_N_OBS, EntityState.State.num_attributes), dtype=np.int16)} if self.config.original["ITEM_SYSTEM_ENABLED"]: gym_obs["Inventory"] = np.zeros((self.config.INVENTORY_N_OBS, ItemState.State.num_attributes), dtype=np.int16) if self.config.original["EXCHANGE_SYSTEM_ENABLED"]: gym_obs["Market"] = np.zeros((self.config.MARKET_N_OBS, ItemState.State.num_attributes), dtype=np.int16) if self.config.original["COMMUNICATION_SYSTEM_ENABLED"]: gym_obs["Communication"] = np.zeros((self.config.COMMUNICATION_N_OBS, len(EntityState.State.comm_attr_map)), dtype=np.int16) return gym_obs def set_arr_values(self, key, values): obs_shape = self.values[key].shape self.values[key][:values.shape[0], :] = values[:, :obs_shape[1]] def export(self): return self.values.copy() # shallow copy class ActionTargets: no_op_keys = ["Direction", "Target", "InventoryItem", "MarketItem"] all_ones = ["Style", "Price", "Token"] def __init__(self, config): self.config = config if not self.config.original["PROVIDE_ACTION_TARGETS"]: return self._no_op = 1 if config.original["PROVIDE_NOOP_ACTION_TARGET"] else 0 self.values = self._make_empty_targets() self.keys_to_clear = None self.clear(reset=True) # to set the no-op option to 1, if needed def _get_keys_to_clear(self): keys = [] if self.config.COMBAT_SYSTEM_ENABLED: keys.append("Attack") if self.config.ITEM_SYSTEM_ENABLED: keys.extend(["Use", "Give", "Destroy"]) if self.config.EXCHANGE_SYSTEM_ENABLED: keys.extend(["Sell", "Buy", "GiveGold"]) if self.config.COMMUNICATION_SYSTEM_ENABLED: keys.append("Comm") return keys def reset(self): if not self.config.original["PROVIDE_ACTION_TARGETS"]: return self.keys_to_clear = self._get_keys_to_clear() self.clear(reset=True) def clear(self, reset=False): if not self.config.original["PROVIDE_ACTION_TARGETS"]: return for key, mask in self.values.items(): if reset is True or key in self.keys_to_clear: for sub_key in mask: mask[sub_key][:] = 1 if sub_key in self.all_ones else 0 if self._no_op > 0 and sub_key in self.no_op_keys: mask[sub_key][-1] = 1 # set the no-op option to 1 def _make_empty_targets(self): masks = {} masks["Move"] = {"Direction": np.zeros(len(action.Direction.edges), dtype=np.int8)} if self.config.original["COMBAT_SYSTEM_ENABLED"]: masks["Attack"] = { "Style": np.ones(len(action.Style.edges), dtype=np.int8), "Target": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)} if self.config.original["ITEM_SYSTEM_ENABLED"]: masks["Use"] = { "InventoryItem": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8)} masks["Give"] = { "InventoryItem": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8), "Target": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)} masks["Destroy"] = { "InventoryItem": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8)} if self.config.original["EXCHANGE_SYSTEM_ENABLED"]: masks["Sell"] = { "InventoryItem": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8), "Price": np.ones(self.config.PRICE_N_OBS, dtype=np.int8)} masks["Buy"] = { "MarketItem": np.zeros(self.config.MARKET_N_OBS + self._no_op, dtype=np.int8)} masks["GiveGold"] = { "Price": np.ones(self.config.PRICE_N_OBS, dtype=np.int8), "Target": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)} if self.config.original["COMMUNICATION_SYSTEM_ENABLED"]: masks["Comm"] = {"Token": np.ones(self.config.COMMUNICATION_NUM_TOKENS, dtype=np.int8)} return masks class Observation: def __init__(self, config, agent_id: int) -> None: self.config = config self.agent_id = agent_id self.agent = None self.current_tick = None self._is_agent_dead = None self.habitable_tiles = None self.agent_in_combat = None self.gym_obs = GymObs(config, agent_id) self.empty_obs = GymObs(config, agent_id).export() self.action_targets = ActionTargets(config) if self.config.original["PROVIDE_ACTION_TARGETS"]: self.empty_obs["ActionTargets"] = ActionTargets(config).values self.vision_radius = self.config.PLAYER_VISION_RADIUS self.vision_diameter = self.config.PLAYER_VISION_DIAMETER self._noop_action = 1 if config.original["PROVIDE_NOOP_ACTION_TARGET"] else 0 self.tiles = None self.entities = BasicObs(EntityState.State.attr_name_to_col["id"], config.PLAYER_N_OBS) self.inventory = InventoryObs(ItemState.State.attr_name_to_col["id"], config.INVENTORY_N_OBS) \ if config.original["ITEM_SYSTEM_ENABLED"] else None self.market = BasicObs(ItemState.State.attr_name_to_col["id"], config.MARKET_N_OBS) \ if config.original["EXCHANGE_SYSTEM_ENABLED"] else None self.comm = BasicObs(EntityState.State.attr_name_to_col["id"], config.COMMUNICATION_N_OBS) \ if config.original["COMMUNICATION_SYSTEM_ENABLED"] else None def reset(self, habitable_tiles, task_embedding=None): self.gym_obs.reset(task_embedding) self.action_targets.reset() self.habitable_tiles = habitable_tiles self._is_agent_dead = False self.agent_in_combat = None self.current_tick = 0 self.tiles = None self.entities.reset() if self.config.ITEM_SYSTEM_ENABLED: self.inventory.reset() if self.config.EXCHANGE_SYSTEM_ENABLED: self.market.reset() if self.config.COMMUNICATION_SYSTEM_ENABLED: self.comm.reset() return self @property def return_dummy_obs(self): return self._is_agent_dead def set_agent_dead(self): self._is_agent_dead = True def update(self, tick, visible_tiles, visible_entities, inventory=None, market=None, comm=None): if self._is_agent_dead: return # cache has previous tick's data, so clear it self.clear_cache() # update the obs self.current_tick = tick self.tiles = visible_tiles # assert len(visible_tiles) == self.config.MAP_N_OBS self.entities.update(visible_entities) if self.config.ITEM_SYSTEM_ENABLED: assert inventory is not None, "Inventory must be provided if ITEM_SYSTEM_ENABLED" self.inventory.update(inventory) if self.config.EXCHANGE_SYSTEM_ENABLED: assert market is not None, "Market must be provided if EXCHANGE_SYSTEM_ENABLED" self.market.update(market) if self.config.COMMUNICATION_SYSTEM_ENABLED: assert comm is not None, "Comm must be provided if COMMUNICATION_SYSTEM_ENABLED" self.comm.update(comm) # update helper vars self.agent = self.entity(self.agent_id) if self.config.COMBAT_SYSTEM_ENABLED: latest_combat_tick = self.agent.latest_combat_tick self.agent_in_combat = False if latest_combat_tick == 0 else \ (tick - latest_combat_tick) < self.config.COMBAT_STATUS_DURATION else: self.agent_in_combat = False @lru_cache def tile(self, r_delta, c_delta): '''Return the array object corresponding to a nearby tile Args: r_delta: row offset from current agent c_delta: col offset from current agent Returns: Vector corresponding to the specified tile ''' idx_1d = (self.vision_radius+r_delta)*self.vision_diameter + self.vision_radius+c_delta try: return TileState.parse_array(self.tiles[idx_1d]) except IndexError: return EMPTY_TILE @lru_cache def entity(self, entity_id): rows = self.entities.values[self.entities.ids == entity_id] if rows.shape[0] == 0: return None return EntityState.parse_array(rows[0]) def clear_cache(self): # clear the outdated cache self.entity.cache_clear() self.tile.cache_clear() def to_gym(self): '''Convert the observation to a format that can be used by OpenAI Gym''' if self.return_dummy_obs: return self.empty_obs self.gym_obs.clear(self.current_tick) # NOTE: assume that all len(self.tiles) == self.config.MAP_N_OBS self.gym_obs.set_arr_values('Tile', self.tiles) self.gym_obs.set_arr_values('Entity', self.entities.values) if self.config.ITEM_SYSTEM_ENABLED: self.gym_obs.set_arr_values('Inventory', self.inventory.values) if self.config.EXCHANGE_SYSTEM_ENABLED: self.gym_obs.set_arr_values('Market', self.market.values) if self.config.COMMUNICATION_SYSTEM_ENABLED: self.gym_obs.set_arr_values('Communication', self.comm.values) gym_obs = self.gym_obs.export() if self.config.PROVIDE_ACTION_TARGETS: gym_obs["ActionTargets"] = self._make_action_targets() return gym_obs def _make_action_targets(self): self.action_targets.clear() masks = self.action_targets.values self._make_move_mask(masks["Move"]) if self.config.COMBAT_SYSTEM_ENABLED: # Test below. see tests/core/test_observation_tile.py, test_action_target_consts() # assert len(action.Style.edges) == 3 self._make_attack_mask(masks["Attack"]) if self.config.ITEM_SYSTEM_ENABLED: self._make_use_mask(masks["Use"]) self._make_destroy_item_mask(masks["Destroy"]) self._make_give_mask(masks["Give"]) if self.config.EXCHANGE_SYSTEM_ENABLED: self._make_sell_mask(masks["Sell"]) self._make_give_gold_mask(masks["GiveGold"]) self._make_buy_mask(masks["Buy"]) return masks def _make_move_mask(self, move_mask, use_cython=None): use_cython = use_cython or self.config.USE_CYTHON if use_cython: chp.make_move_mask(move_mask["Direction"], self.habitable_tiles, self.agent.row, self.agent.col, ROW_DELTA, COL_DELTA) return move_mask["Direction"][:4] = self.habitable_tiles[self.agent.row+ROW_DELTA, self.agent.col+COL_DELTA] def _make_attack_mask(self, attack_mask, use_cython=None): if self.config.COMBAT_ALLOW_FLEXIBLE_STYLE: # NOTE: if the style is flexible, then the reach of all styles should be the same assert self.config.COMBAT_MELEE_REACH == self.config.COMBAT_RANGE_REACH assert self.config.COMBAT_MELEE_REACH == self.config.COMBAT_MAGE_REACH assert self.config.COMBAT_RANGE_REACH == self.config.COMBAT_MAGE_REACH if not self.config.COMBAT_SYSTEM_ENABLED or self.return_dummy_obs: return use_cython = use_cython or self.config.USE_CYTHON if use_cython: chp.make_attack_mask( attack_mask["Target"], self.entities.values, EntityState.State.attr_name_to_col, {"agent_id": self.agent_id, "row": self.agent.row, "col": self.agent.col, "immunity": self.config.COMBAT_SPAWN_IMMUNITY, "attack_range": self.config.COMBAT_RANGE_REACH}) return # allow friendly fire but no self shooting targetable = self.entities.ids != self.agent.id # NOTE: this is a hack. Only target "normal" agents, which has npc_type of 0, 1, 2, 3 # For example, immortal "scout" agents has npc_type of -1 targetable &= self.entities.values[:,EntityState.State.attr_name_to_col["npc_type"]] >= 0 immunity = self.config.COMBAT_SPAWN_IMMUNITY if self.agent.time_alive < immunity: # NOTE: CANNOT attack players during immunity, thus mask should set to 0 targetable &= ~(self.entities.ids > 0) # ids > 0 equals entity.is_player within_range = np.maximum( # calculating the l-inf dist np.abs(self.entities.values[:,EntityState.State.attr_name_to_col["row"]] - self.agent.row), np.abs(self.entities.values[:,EntityState.State.attr_name_to_col["col"]] - self.agent.col) ) <= self.config.COMBAT_MELEE_REACH attack_mask["Target"][:self.entities.len] = targetable & within_range if np.count_nonzero(attack_mask["Target"][:self.entities.len]): # Mask the no-op option, since there should be at least one allowed move # NOTE: this will make agents always attack if there is a valid target attack_mask["Target"][-1] = 0 def _make_use_mask(self, use_mask): # empty inventory -- nothing to use if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0)\ or self.return_dummy_obs or self.agent_in_combat: return item_skill = self._item_skill() not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col["listed_price"]] == 0 item_type = self.inventory.values[:,ItemState.State.attr_name_to_col["type_id"]] item_level = self.inventory.values[:,ItemState.State.attr_name_to_col["level"]] # level limits are differently applied depending on item types type_flt = np.tile(np.array(list(item_skill.keys())), (self.inventory.len,1)) level_flt = np.tile(np.array(list(item_skill.values())), (self.inventory.len,1)) item_type = np.tile(np.transpose(np.atleast_2d(item_type)), (1,len(item_skill))) item_level = np.tile(np.transpose(np.atleast_2d(item_level)), (1,len(item_skill))) level_satisfied = np.any((item_type==type_flt) & (item_level<=level_flt), axis=1) use_mask["InventoryItem"][:self.inventory.len] = not_listed & level_satisfied def _item_skill(self): agent = self.agent # the minimum agent level is 1 level = max(1, agent.melee_level, agent.range_level, agent.mage_level, agent.fishing_level, agent.herbalism_level, agent.prospecting_level, agent.carving_level, agent.alchemy_level) return { item_system.Hat.ITEM_TYPE_ID: level, item_system.Top.ITEM_TYPE_ID: level, item_system.Bottom.ITEM_TYPE_ID: level, item_system.Spear.ITEM_TYPE_ID: agent.melee_level, item_system.Bow.ITEM_TYPE_ID: agent.range_level, item_system.Wand.ITEM_TYPE_ID: agent.mage_level, item_system.Rod.ITEM_TYPE_ID: agent.fishing_level, item_system.Gloves.ITEM_TYPE_ID: agent.herbalism_level, item_system.Pickaxe.ITEM_TYPE_ID: agent.prospecting_level, item_system.Axe.ITEM_TYPE_ID: agent.carving_level, item_system.Chisel.ITEM_TYPE_ID: agent.alchemy_level, item_system.Whetstone.ITEM_TYPE_ID: agent.melee_level, item_system.Arrow.ITEM_TYPE_ID: agent.range_level, item_system.Runes.ITEM_TYPE_ID: agent.mage_level, item_system.Ration.ITEM_TYPE_ID: level, item_system.Potion.ITEM_TYPE_ID: level } def _make_destroy_item_mask(self, destroy_mask): # empty inventory -- nothing to destroy if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0)\ or self.return_dummy_obs or self.agent_in_combat: return # not equipped items in the inventory can be destroyed not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col["equipped"]] == 0 destroy_mask["InventoryItem"][:self.inventory.len] = not_equipped def _make_give_mask(self, give_mask): if not self.config.ITEM_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat\ or self.inventory.len == 0: return # InventoryItem not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col["equipped"]] == 0 not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col["listed_price"]] == 0 give_mask["InventoryItem"][:self.inventory.len] = not_equipped & not_listed # Give Target # NOTE: Allow give to entities within visual range. So no distance check is needed # entities_pos = self.entities.values[:,[EntityState.State.attr_name_to_col["row"], # EntityState.State.attr_name_to_col["col"]]] # same_tile = utils.linf(entities_pos, (self.agent.row, self.agent.col)) == 0 not_me = self.entities.ids != self.agent_id player = (self.entities.values[:,EntityState.State.attr_name_to_col["npc_type"]] == 0) give_mask["Target"][:self.entities.len] = player & not_me def _make_sell_mask(self, sell_mask): # empty inventory -- nothing to sell if not (self.config.EXCHANGE_SYSTEM_ENABLED and self.inventory.len > 0) \ or self.return_dummy_obs or self.agent_in_combat: return not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col["equipped"]] == 0 not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col["listed_price"]] == 0 sell_mask["InventoryItem"][:self.inventory.len] = not_equipped & not_listed def _make_give_gold_mask(self, give_mask): if not self.config.EXCHANGE_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat\ or int(self.agent.gold) <= 2: # NOTE: this is a hack to reduce mask computation return # GiveGold Target # NOTE: Allow give to entities within visual range. So no distance check is needed # entities_pos = self.entities.values[:,[EntityState.State.attr_name_to_col["row"], # EntityState.State.attr_name_to_col["col"]]] # same_tile = utils.linf(entities_pos, (self.agent.row, self.agent.col)) == 0 not_me = self.entities.ids != self.agent_id player = (self.entities.values[:,EntityState.State.attr_name_to_col["npc_type"]] == 0) give_mask["Target"][:self.entities.len] = player & not_me # GiveGold Amount (Price) gold = int(self.agent.gold) give_mask["Price"][gold:] = 0 # NOTE: Price masks starts with all ones def _make_buy_mask(self, buy_mask): if not self.config.EXCHANGE_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat \ or self.market.len == 0: return market_items = self.market.values not_mine = market_items[:,ItemState.State.attr_name_to_col["owner_id"]] != self.agent_id # if the inventory is full, one can only buy existing ammo stack # otherwise, one can buy anything owned by other, having enough money if self.inventory.len >= self.config.ITEM_INVENTORY_CAPACITY: exist_ammo_listings = self._existing_ammo_listings() if not np.any(exist_ammo_listings): return not_mine &= exist_ammo_listings enough_gold = market_items[:,ItemState.State.attr_name_to_col["listed_price"]] \ <= self.agent.gold buy_mask["MarketItem"][:self.market.len] = not_mine & enough_gold def _existing_ammo_listings(self): sig_col = (ItemState.State.attr_name_to_col["type_id"], ItemState.State.attr_name_to_col["level"]) ammo_id = [ammo.ITEM_TYPE_ID for ammo in [item_system.Whetstone, item_system.Arrow, item_system.Runes]] # search ammo stack from the inventory type_flt = np.tile(np.array(ammo_id), (self.inventory.len,1)) item_type = np.tile( np.transpose(np.atleast_2d(self.inventory.values[:,sig_col[0]])), (1, len(ammo_id))) exist_ammo = self.inventory.values[np.any(item_type == type_flt, axis=1)] # self does not have ammo if exist_ammo.shape[0] == 0: return np.zeros(self.market.len, dtype=bool) # search the existing ammo stack from the market that's not mine type_flt = np.tile(np.array(exist_ammo[:,sig_col[0]]), (self.market.len,1)) level_flt = np.tile(np.array(exist_ammo[:,sig_col[1]]), (self.market.len,1)) item_type = np.tile(np.transpose(np.atleast_2d(self.market.values[:,sig_col[0]])), (1, exist_ammo.shape[0])) item_level = np.tile(np.transpose(np.atleast_2d(self.market.values[:,sig_col[1]])), (1, exist_ammo.shape[0])) exist_ammo_listings = np.any((item_type==type_flt) & (item_level==level_flt), axis=1) not_mine = self.market.values[:,ItemState.State.attr_name_to_col["owner_id"]] != self.agent_id return exist_ammo_listings & not_mine ================================================ FILE: nmmo/core/realm.py ================================================ from __future__ import annotations from collections import defaultdict from typing import Dict import numpy as np import nmmo from nmmo.core.map import Map from nmmo.core.tile import TileState from nmmo.core.action import Action, Buy, Comm from nmmo.entity.entity import EntityState from nmmo.entity.entity_manager import PlayerManager from nmmo.entity.npc_manager import NPCManager from nmmo.datastore.numpy_datastore import NumpyDatastore from nmmo.systems.exchange import Exchange from nmmo.systems.item import ItemState from nmmo.lib.event_log import EventLogger, EventState from nmmo.render.replay_helper import ReplayHelper def prioritized(entities: Dict, merged: Dict): """Sort actions into merged according to priority""" for idx, actions in entities.items(): for atn, args in actions.items(): merged[atn.priority].append((idx, (atn, args.values()))) return merged class Realm: """Top-level world object""" def __init__(self, config, np_random): self.config = config self._np_random = np_random # rng assert isinstance( config, nmmo.config.Config ), f"Config {config} is not a config instance (did you pass the class?)" Action.hook(config) self.datastore = NumpyDatastore() for s in [TileState, EntityState, ItemState, EventState]: self.datastore.register_object_type(s._name, s.State.num_attributes) self.tick = None # to use as a "reset" checker # Load the world file self.map = Map(config, self, self._np_random) self.fog_map = np.zeros((config.MAP_SIZE, config.MAP_SIZE), dtype=np.float16) # Event logger self.event_log = EventLogger(self) # Entity handlers self.players = PlayerManager(self, self._np_random) self.npcs = NPCManager(self, self._np_random) # Global item registry self.items = {} # Global item exchange self.exchange = Exchange(self) # Replay helper self._replay_helper = None # Initialize actions nmmo.Action.init(config) def reset(self, np_random, map_dict, custom_spawn=False, seize_targets=None, delete_dead_player=True): """Reset the sub-systems and load the provided map""" self._np_random = np_random self.tick = 0 self.update_fog_map(reset=True) #self.event_log.reset() self.items.clear() self.exchange.reset() if self._replay_helper is not None: self._replay_helper.reset() # Load the map np array into the map, tiles and reset self.map.reset(map_dict, self._np_random, seize_targets) # EntityState and ItemState tables must be empty after players/npcs.reset() self.players.reset(self._np_random, delete_dead_player) self.npcs.reset(self._np_random) # assert EntityState.State.table(self.datastore).is_empty(), \ # "EntityState table is not empty" # assert ItemState.State.table(self.datastore).is_empty(), \ # "ItemState table is not empty" # DataStore id allocator must be reset to be deterministic EntityState.State.table(self.datastore).reset() ItemState.State.table(self.datastore).reset() self.event_log.reset() # reset this last for debugging if custom_spawn is False: # NOTE: custom spawning npcs and agents can be done outside, after reset() self.npcs.default_spawn() self.players.spawn() def packet(self): """Client packet""" return { "environment": self.map.repr, "border": self.config.MAP_BORDER, "size": self.config.MAP_SIZE, "resource": self.map.packet, "player": self.players.packet, "npc": self.npcs.packet, "market": self.exchange.packet, } @property def num_players(self): """Number of alive player agents""" return len(self.players.entities) @property def seize_status(self): return self.map.seize_status def entity(self, ent_id): e = self.entity_or_none(ent_id) assert e is not None, f"Entity {ent_id} does not exist" return e def entity_or_none(self, ent_id): if ent_id is None: return None """Get entity by ID""" if ent_id < 0: return self.npcs.get(ent_id) return self.players.get(ent_id) def step(self, actions): """Run game logic for one tick Args: actions: Dict of agent actions Returns: dead: List of dead agents """ # Prioritize actions npc_actions = self.npcs.actions() merged = defaultdict(list) prioritized(actions, merged) prioritized(npc_actions, merged) # Update entities and perform actions self.players.update(actions) self.npcs.update(npc_actions) # Execute actions -- CHECK ME the below priority # - 10: Use - equip ammo, restore HP, etc. # - 20: Buy - exchange while sellers, items, buyers are all intact # - 30: Give, GiveGold - transfer while both are alive and at the same tile # - 40: Destroy - use with SELL/GIVE, if not gone, destroy and recover space # - 50: Attack # - 60: Move # - 70: Sell - to guarantee the listed items are available to buy # - 99: Comm for priority in sorted(merged): # TODO: we should be randomizing these, otherwise the lower ID agents # will always go first. --> ONLY SHUFFLE BUY if priority == Buy.priority: self._np_random.shuffle(merged[priority]) # CHECK ME: do we need this line? # ent_id, (atn, args) = merged[priority][0] for ent_id, (atn, args) in merged[priority]: ent = self.entity(ent_id) if (ent.alive and not ent.status.frozen) or \ (ent.is_recon and priority == Comm.priority): # recons can always comm atn.call(self, ent, *args) dead_players = self.players.cull() dead_npcs = self.npcs.cull() self.tick += 1 # These require the updated tick self.map.step() self.update_fog_map() self.exchange.step() self.event_log.update() if self._replay_helper is not None: self._replay_helper.update() return dead_players, dead_npcs def update_fog_map(self, reset=False): fog_start_tick = self.config.DEATH_FOG_ONSET if fog_start_tick is None: return fog_speed = self.config.DEATH_FOG_SPEED center = self.config.MAP_SIZE // 2 safe = self.config.DEATH_FOG_FINAL_SIZE if reset: dist = -self.config.MAP_BORDER for i in range(center): l, r = i, self.config.MAP_SIZE - i # positive value represents the poison strength # negative value represents the shortest distance to poison area self.fog_map[l:r, l:r] = -dist dist += 1 # mark the safe area self.fog_map[center-safe:center+safe+1, center-safe:center+safe+1] = -self.config.MAP_SIZE return # consider the map border so that the fog can hit the border at fog_start_tick if self.tick >= fog_start_tick: self.fog_map += fog_speed # mark the safe area self.fog_map[center-safe:center+safe+1, center-safe:center+safe+1] = -self.config.MAP_SIZE def record_replay(self, replay_helper: ReplayHelper) -> ReplayHelper: self._replay_helper = replay_helper self._replay_helper.set_realm(self) return replay_helper ================================================ FILE: nmmo/core/terrain.py ================================================ import os import logging import numpy as np from imageio.v2 import imread, imsave from scipy import stats from nmmo.lib import material, seeding, utils, vec_noise def sharp(noise): '''Exponential noise sharpener for perlin ridges''' return 2 * (0.5 - abs(0.5 - noise)) class Save: '''Save utility for map files''' @staticmethod def render(mats, lookup, path): '''Render tiles to png''' images = [[lookup[e] for e in l] for l in mats] image = np.vstack([np.hstack(e) for e in images]) imsave(path, image) @staticmethod def fractal(terrain, path): '''Save fractal to both png and npy''' imsave(os.path.join(path, 'fractal.png'), (256*terrain).astype(np.uint8)) np.save(os.path.join(path, 'fractal.npy'), terrain.astype(np.float16)) @staticmethod def as_numpy(mats, path): '''Save map to .npy''' path = os.path.join(path, 'map.npy') np.save(path, mats.astype(int)) # pylint: disable=E1101:no-member # Terrain uses setattr() class Terrain: '''Terrain material class; populated at runtime''' @staticmethod def generate_terrain(config, map_id, interpolaters): center = config.MAP_CENTER border = config.MAP_BORDER size = config.MAP_SIZE frequency = config.TERRAIN_FREQUENCY offset = config.TERRAIN_FREQUENCY_OFFSET octaves = center // config.TERRAIN_TILES_PER_OCTAVE #Compute a unique seed based on map index #Flip seed used to ensure train/eval maps are different seed = map_id + 1 if config.TERRAIN_FLIP_SEED: seed = -seed #Log interpolation factor if not interpolaters: interpolaters = np.logspace(config.TERRAIN_LOG_INTERPOLATE_MIN, config.TERRAIN_LOG_INTERPOLATE_MAX, config.MAP_N) interpolate = interpolaters[map_id] #Data buffers val = np.zeros((size, size, octaves)) scale = np.zeros((size, size, octaves)) s = np.arange(size) X, Y = np.meshgrid(s, s) #Compute noise over logscaled octaves start = frequency end = min(start, start - np.log2(center) + offset) for idx, freq in enumerate(np.logspace(start, end, octaves, base=2)): val[:, :, idx] = vec_noise.snoise2(seed*size + freq*X, idx*size + freq*Y) #Compute L1 distance l1 = utils.l1_map(size) #Interpolation Weights rrange = np.linspace(-1, 1, 2*octaves-1) pdf = stats.norm.pdf(rrange, 0, interpolate) pdf = pdf / max(pdf) high = center / 2 delta = high / octaves #Compute perlin mask noise = np.zeros((size, size)) X, Y = np.meshgrid(s, s) expand = int(np.log2(center)) - 2 for idx, octave in enumerate(range(expand, 1, -1)): freq, mag = 1 / 2**octave, 1 / 2**idx noise += mag * vec_noise.snoise2(seed*size + freq*X, idx*size + freq*Y) noise -= np.min(noise) noise = octaves * noise / np.max(noise) - 1e-12 noise = noise.astype(int) #Compute L1 and Perlin scale factor for i in range(octaves): start = octaves - i - 1 scale[l1 <= high] = np.arange(start, start + octaves) high -= delta start = noise - 1 l1_scale = np.clip(l1, 0, size//2 - border - 2) l1_scale = l1_scale / np.max(l1_scale) for i in range(octaves): idxs = l1_scale*scale[:, :, i] + (1-l1_scale)*(start + i) scale[:, :, i] = pdf[idxs.astype(int)] #Blend octaves std = np.std(val) val = val / std val = scale * val val = np.sum(scale * val, -1) val = std * val / np.std(val) val = 0.5 + np.clip(val, -1, 1)/2 # Transform fractal noise to terrain matl = fractal_to_material(config, val) matl = process_map_border(config, matl, l1) return val, matl, interpolaters def fractal_to_material(config, fractal, all_grass=False): size = config.MAP_SIZE matl_map = np.zeros((size, size), dtype=np.int16) for y in range(size): for x in range(size): if all_grass: matl_map[y, x] = Terrain.GRASS continue v = fractal[y, x] if v <= config.TERRAIN_WATER: mat = Terrain.WATER elif v <= config.TERRAIN_GRASS: mat = Terrain.GRASS elif v <= config.TERRAIN_FOILAGE: mat = Terrain.FOILAGE else: mat = Terrain.STONE matl_map[y, x] = mat return matl_map def process_map_border(config, matl_map, l1=None): size = config.MAP_SIZE border = config.MAP_BORDER if l1 is None: l1 = utils.l1_map(size) # Void and grass border matl_map[l1 > size/2 - border] = material.Void.index matl_map[l1 == size//2 - border] = material.Grass.index edge = l1 == size//2 - border - 1 stone = (matl_map == material.Stone.index) | (matl_map == material.Water.index) matl_map[edge & stone] = material.Foilage.index return matl_map def place_fish(tiles, mmin, mmax, np_random, num_fish): placed = 0 # if USE_CYTHON: # water_loc = chp.tile_where(tiles, Terrain.WATER, mmin, mmax) # else: water_loc = np.where(tiles == Terrain.WATER) water_loc = [(r, c) for r, c in zip(water_loc[0], water_loc[1]) if mmin < r < mmax and mmin < c < mmax] if len(water_loc) < num_fish: raise RuntimeError('Not enough water tiles to place fish.') np_random.shuffle(water_loc) allow = {Terrain.GRASS} # Fish should be placed adjacent to grass for r, c in water_loc: if tiles[r-1, c] in allow or tiles[r+1, c] in allow or \ tiles[r, c-1] in allow or tiles[r, c+1] in allow: tiles[r, c] = Terrain.FISH placed += 1 if placed == num_fish: break if placed < num_fish: raise RuntimeError('Could not find the water tile to place fish.') def uniform(config, tiles, mat, mmin, mmax, np_random): r = np_random.integers(mmin, mmax) c = np_random.integers(mmin, mmax) if tiles[r, c] not in {Terrain.GRASS}: uniform(config, tiles, mat, mmin, mmax, np_random) else: tiles[r, c] = mat def cluster(config, tiles, mat, mmin, mmax, np_random): mmin = mmin + 1 mmax = mmax - 1 r = np_random.integers(mmin, mmax) c = np_random.integers(mmin, mmax) matls = {Terrain.GRASS} if tiles[r, c] not in matls: cluster(config, tiles, mat, mmin-1, mmax+1, np_random) return tiles[r, c] = mat if tiles[r-1, c] in matls: tiles[r-1, c] = mat if tiles[r+1, c] in matls: tiles[r+1, c] = mat if tiles[r, c-1] in matls: tiles[r, c-1] = mat if tiles[r, c+1] in matls: tiles[r, c+1] = mat def spawn_profession_resources(config, tiles, np_random=None): if np_random is None: np_random = np.random mmin = config.MAP_BORDER + 1 mmax = config.MAP_SIZE - config.MAP_BORDER - 1 for _ in range(config.PROGRESSION_SPAWN_CLUSTERS): cluster(config, tiles, Terrain.ORE, mmin, mmax, np_random) cluster(config, tiles, Terrain.TREE, mmin, mmax, np_random) cluster(config, tiles, Terrain.CRYSTAL, mmin, mmax, np_random) for _ in range(config.PROGRESSION_SPAWN_UNIFORMS): uniform(config, tiles, Terrain.HERB, mmin, mmax, np_random) place_fish(tiles, mmin, mmax, np_random, config.PROGRESSION_SPAWN_UNIFORMS) def try_add_tile(map_tiles, row, col, tile_to_add): if map_tiles[row, col] == Terrain.GRASS: map_tiles[row, col] = tile_to_add return True return False def scatter_extra_resources(config, tiles, np_random=None, density_factor=6): if np_random is None: np_random = np.random center = config.MAP_CENTER mmin = config.MAP_BORDER + 1 mmax = config.MAP_SIZE - config.MAP_BORDER - 1 water_to_add, water_added = (center//density_factor)**2, 0 food_to_add, food_added = (center//density_factor)**2, 0 while True: if water_added >= water_to_add and food_added >= food_to_add: break r, c = tuple(np_random.integers(mmin, mmax, size=(2,))) if water_added < water_to_add: water_added += 1 if try_add_tile(tiles, r, c, Terrain.WATER) else 0 if food_added < food_to_add: food_added += 1 if try_add_tile(tiles, r, c, Terrain.FOILAGE) else 0 class MapGenerator: '''Procedural map generation''' def __init__(self, config): self.config = config self.load_textures() self.interpolaters = None def load_textures(self): '''Called during setup; loads and resizes tile pngs''' lookup = {} path = self.config.PATH_TILE scale = self.config.MAP_PREVIEW_DOWNSCALE for mat in material.All: key = mat.tex tex = imread(path.format(key)) lookup[mat.index] = tex[:, :, :3][::scale, ::scale] setattr(Terrain, key.upper(), mat.index) self.textures = lookup def generate_all_maps(self, seed=None): '''Generates MAP_N maps according to generate_map Provides additional utilities for saving to .npy and rendering png previews''' config = self.config np_random, _ = seeding.np_random(seed) #Only generate if maps are not cached path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS) os.makedirs(path_maps, exist_ok=True) existing_maps = set(map_dir + '/map.npy' for map_dir in os.listdir(path_maps)) if not config.MAP_FORCE_GENERATION and existing_maps: required_maps = { f'map{idx}/map.npy' for idx in range(1, config.MAP_N+1) } missing = required_maps - existing_maps if not missing: return if __debug__: logging.info('Generating %s maps', str(config.MAP_N)) for idx in range(config.MAP_N): path = path_maps + '/map' + str(idx+1) os.makedirs(path, exist_ok=True) terrain, tiles = self.generate_map(idx, np_random) #Save/render Save.as_numpy(tiles, path) Save.fractal(terrain, path) if config.MAP_GENERATE_PREVIEWS: b = config.MAP_BORDER tiles = [e[b:-b+1] for e in tiles][b:-b+1] Save.render(tiles, self.textures, path+'/map.png') def generate_map(self, idx, np_random=None): '''Generate a single map The default method is a relatively complex multiscale perlin noise method. This is not just standard multioctave noise -- we are seeding multioctave noise itself with perlin noise to create localized deviations in scale, plus additional biasing to decrease terrain frequency towards the center of the map We found that this creates more visually interesting terrain and more deviation in required planning horizon across different parts of the map. This is by no means a gold-standard: you are free to override this method and create customized terrain generation more suitable for your application. Simply pass MAP_GENERATOR=YourMapGenClass as a config argument.''' config = self.config if config.TERRAIN_SYSTEM_ENABLED: if not hasattr(self, 'interpolaters'): self.interpolaters = None terrain, tiles, _ = Terrain.generate_terrain(config, idx, self.interpolaters) else: size = config.MAP_SIZE terrain = np.zeros((size, size)) tiles = np.zeros((size, size), dtype=object) for r in range(size): for c in range(size): linf = max(abs(r - size//2), abs(c - size//2)) if linf <= size//2 - config.MAP_BORDER: tiles[r, c] = Terrain.GRASS else: tiles[r, c] = Terrain.VOID if config.PROFESSION_SYSTEM_ENABLED: spawn_profession_resources(config, tiles, np_random) return terrain, tiles ================================================ FILE: nmmo/core/tile.py ================================================ from types import SimpleNamespace from nmmo.datastore.serialized import SerializedState from nmmo.lib import material, event_code # pylint: disable=no-member,protected-access TileState = SerializedState.subclass( "Tile", [ "row", "col", "material_id", ]) TileState.Limits = lambda config: { "row": (0, config.MAP_SIZE-1), "col": (0, config.MAP_SIZE-1), "material_id": (0, config.MAP_N_TILE), } TileState.Query = SimpleNamespace( window=lambda ds, r, c, radius: ds.table("Tile").window( TileState.State.attr_name_to_col["row"], TileState.State.attr_name_to_col["col"], r, c, radius), get_map=lambda ds, map_size: ds.table("Tile")._data[1:(map_size*map_size+1)] .reshape((map_size,map_size,len(TileState.State.attr_name_to_col))) ) class Tile(TileState): def __init__(self, realm, r, c, np_random): super().__init__(realm.datastore, TileState.Limits(realm.config)) self.realm = realm self.config = realm.config self._np_random = np_random self.row.update(r) self.col.update(c) self.state = None self.material = None self.depleted = False self.entities = {} self.seize_history = [] @property def occupied(self): # NOTE: ONLY players consider whether the tile is occupied or not # NPCs can move into occupied tiles. # Surprisingly, this has huge effect on training, so be careful. # Tried this -- "sum(1 for ent_id in self.entities if ent_id > 0) > 0" return len(self.entities) > 0 @property def repr(self): return ((self.row.val, self.col.val)) @property def pos(self): return self.row.val, self.col.val @property def habitable(self): return self.material in material.Habitable @property def impassible(self): return self.material in material.Impassible @property def void(self): return self.material == material.Void @property def tex(self): return self.state.tex def reset(self, mat, config, np_random): self._np_random = np_random # reset the RNG self.entities = {} self.seize_history.clear() self.material = mat(config) self._respawn() def set_depleted(self): self.depleted = True self.state = self.material.deplete self.material_id.update(self.state.index) def _respawn(self): self.depleted = False self.state = self.material self.material_id.update(self.state.index) def add_entity(self, ent): assert ent.ent_id not in self.entities self.entities[ent.ent_id] = ent def remove_entity(self, ent_id): assert ent_id in self.entities self.entities.pop(ent_id) def step(self): if not self.depleted or self.material.respawn == 0: return if self._np_random.random() < self.material.respawn: self._respawn() def harvest(self, deplete): assert not self.depleted, f'{self.state} is depleted' assert self.state in material.Harvestable, f'{self.state} not harvestable' if deplete: self.set_depleted() return self.material.harvest() def update_seize(self): if len(self.entities) != 1: # only one entity can seize a tile return ent_id, entity = list(self.entities.items())[0] if ent_id < 0: # not counting npcs return team_members = entity.my_task.assignee # NOTE: only one task per player if self.seize_history and self.seize_history[-1][0] in team_members: # no need to add another entry if the last entry is from the same team (incl. self) return self.seize_history.append((ent_id, self.realm.tick)) if self.realm.event_log: self.realm.event_log.record(event_code.EventCode.SEIZE_TILE, entity, tile=self.pos) ================================================ FILE: nmmo/datastore/__init__.py ================================================ ================================================ FILE: nmmo/datastore/datastore.py ================================================ from __future__ import annotations from typing import Dict, List from nmmo.datastore.id_allocator import IdAllocator """ This code defines a data storage system that allows for the creation, manipulation, and querying of records. The DataTable class serves as the foundation for the data storage, providing methods for updating and retrieving data, as well as filtering and querying records. The DatastoreRecord class represents a single record within a table and provides a simple interface for interacting with the data. The Datastore class serves as the main entry point for the data storage system, allowing for the creation and management of tables and records. The implementation of the DataTable class is left to the developer, but the DatastoreRecord and Datastore classes should be sufficient for most use cases. See numpy_datastore.py for an implementation. """ class DataTable: def __init__(self, num_columns: int): self._num_columns = num_columns self._id_allocator = IdAllocator(100) def reset(self): self._id_allocator = IdAllocator(100) def update(self, row_id: int, col: int, value): raise NotImplementedError def get(self, ids: List[id]): raise NotImplementedError def where_in(self, col: int, values: List): raise NotImplementedError def where_eq(self, col: str, value): raise NotImplementedError def where_neq(self, col: str, value): raise NotImplementedError def window(self, row_idx: int, col_idx: int, row: int, col: int, radius: int): raise NotImplementedError def remove_row(self, row_id: int): raise NotImplementedError def add_row(self) -> int: raise NotImplementedError def is_empty(self) -> bool: raise NotImplementedError class DatastoreRecord: def __init__(self, datastore, table: DataTable, row_id: int) -> None: self.datastore = datastore self.table = table self.id = row_id def update(self, col: int, value): self.table.update(self.id, col, value) def get(self, col: int): return self.table.get(self.id)[col] def delete(self): self.table.remove_row(self.id) class Datastore: def __init__(self) -> None: self._tables: Dict[str, DataTable] = {} def register_object_type(self, object_type: str, num_colums: int): if object_type not in self._tables: self._tables[object_type] = self._create_table(num_colums) def create_record(self, object_type: str) -> DatastoreRecord: table = self._tables[object_type] row_id = table.add_row() return DatastoreRecord(self, table, row_id) def table(self, object_type: str) -> DataTable: return self._tables[object_type] def _create_table(self, num_columns: int) -> DataTable: raise NotImplementedError ================================================ FILE: nmmo/datastore/id_allocator.py ================================================ from ordered_set import OrderedSet class IdAllocator: def __init__(self, max_id): # Key 0 is reserved as padding self.max_id = 1 self.free = OrderedSet() self.expand(max_id) def full(self): return len(self.free) == 0 def remove(self, row_id): self.free.add(row_id) def allocate(self): return self.free.pop(0) def expand(self, max_id): self.free.update(range(self.max_id, max_id)) self.max_id = max_id ================================================ FILE: nmmo/datastore/numpy_datastore.py ================================================ from typing import List import numpy as np from nmmo.datastore.datastore import Datastore, DataTable class NumpyTable(DataTable): def __init__(self, num_columns: int, initial_size: int, dtype=np.int16): super().__init__(num_columns) self._dtype = dtype self._initial_size = initial_size self._max_rows = 0 self._data = np.zeros((0, self._num_columns), dtype=self._dtype) self._expand(self._initial_size) def reset(self): super().reset() # resetting _id_allocator self._max_rows = 0 self._data = np.zeros((0, self._num_columns), dtype=self._dtype) self._expand(self._initial_size) def update(self, row_id: int, col: int, value): self._data[row_id, col] = value def get(self, ids: List[int]): return self._data[ids] def where_eq(self, col: int, value): return self._data[self._data[:,col] == value] def where_neq(self, col: int, value): return self._data[self._data[:,col] != value] def where_gt(self, col: int, value): return self._data[self._data[:,col] > value] def where_in(self, col: int, values: List): return self._data[np.in1d(self._data[:,col], values)] def window(self, row_idx: int, col_idx: int, row: int, col: int, radius: int): return self._data[( (np.abs(self._data[:,row_idx] - row) <= radius) & (np.abs(self._data[:,col_idx] - col) <= radius) ).ravel()] def add_row(self) -> int: if self._id_allocator.full(): self._expand(self._max_rows * 2) row_id = self._id_allocator.allocate() return row_id def remove_row(self, row_id: int) -> int: self._id_allocator.remove(row_id) self._data[row_id] = 0 def _expand(self, max_rows: int): assert max_rows > self._max_rows data = np.zeros((max_rows, self._num_columns), dtype=self._dtype) data[:self._max_rows] = self._data self._max_rows = max_rows self._id_allocator.expand(max_rows) self._data = data def is_empty(self) -> bool: all_data_zero = np.all(self._data == 0) # 0th row is reserved as padding, so # of free ids is _max_rows-1 all_id_free = len(self._id_allocator.free) == self._max_rows-1 return all_data_zero and all_id_free class NumpyDatastore(Datastore): def _create_table(self, num_columns: int) -> DataTable: return NumpyTable(num_columns, 100) ================================================ FILE: nmmo/datastore/serialized.py ================================================ # pylint: disable=bare-except,c-extension-no-member from __future__ import annotations from ast import Tuple import math from types import SimpleNamespace from typing import Dict, List from nmmo.datastore.datastore import Datastore, DatastoreRecord try: import nmmo.lib.cython_helper as chp USE_CYTHON = True except: USE_CYTHON = False """ This code defines classes for serializing and deserializing data in a structured way. The SerializedAttribute class represents a single attribute of a record and provides methods for updating and querying its value, as well as enforcing minimum and maximum bounds on the value. The SerializedState class serves as a base class for creating serialized representations of specific types of data, using a list of attribute names to define the structure of the data. The subclass method is a factory method for creating subclasses of SerializedState that are tailored to specific types of data. """ class SerializedAttribute(): def __init__(self, name: str, datastore_record: DatastoreRecord, column: int, min_val=-math.inf, max_val=math.inf) -> None: self._name = name self.datastore_record = datastore_record self._column = column self._min = min_val self._max = max_val self._val = 0 @property def val(self): return self._val def update(self, value): if value > self._max: value = self._max elif value < self._min: value = self._min self.datastore_record.update(self._column, value) self._val = value @property def min(self): return self._min @property def max(self): return self._max def increment(self, val=1, max_v=math.inf): self.update(min(max_v, self.val + val)) return self def decrement(self, val=1, min_v=-math.inf): self.update(max(min_v, self.val - val)) return self @property def empty(self): return self.val == 0 def __eq__(self, other): return self.val == other def __ne__(self, other): return self.val != other def __lt__(self, other): return self.val < other def __le__(self, other): return self.val <= other def __gt__(self, other): return self.val > other def __ge__(self, other): return self.val >= other class SerializedState(): @staticmethod def subclass(name: str, attributes: List[str]): class Subclass(SerializedState): _name = name State = SimpleNamespace( attr_name_to_col = {a: i for i, a in enumerate(attributes)}, num_attributes = len(attributes), table = lambda ds: ds.table(name) ) def __init__(self, datastore: Datastore, limits: Dict[str, Tuple[float, float]] = None): limits = limits or {} self.datastore_record = datastore.create_record(name) for attr, col in self.State.attr_name_to_col.items(): try: setattr(self, attr, SerializedAttribute(attr, self.datastore_record, col, *limits.get(attr, (-math.inf, math.inf)))) except Exception as exc: raise RuntimeError('Failed to set attribute "' + attr + '"') from exc @classmethod def parse_array(cls, data) -> SimpleNamespace: # Takes in a data array and returns a SimpleNamespace object with # attribute names as keys and corresponding values from the input # data array. assert len(data) == cls.State.num_attributes, \ f"Expected {cls.State.num_attributes} attributes, got {len(data)}" if USE_CYTHON: return chp.parse_array(data, cls.State.attr_name_to_col) return SimpleNamespace(**{ attr: data[col] for attr, col in cls.State.attr_name_to_col.items() }) return Subclass ================================================ FILE: nmmo/entity/__init__.py ================================================ from nmmo.entity.entity import Entity from nmmo.entity.player import Player ================================================ FILE: nmmo/entity/entity.py ================================================ import math from types import SimpleNamespace import numpy as np from nmmo.datastore.serialized import SerializedState from nmmo.systems import inventory from nmmo.lib.event_code import EventCode # pylint: disable=no-member EntityState = SerializedState.subclass( "Entity", [ "id", "npc_type", # 1 - passive, 2 - neutral, 3 - aggressive "row", "col", # Status "damage", "time_alive", "freeze", "item_level", "attacker_id", "latest_combat_tick", "message", # Resources "gold", "health", "food", "water", # Combat Skills "melee_level", "melee_exp", "range_level", "range_exp", "mage_level", "mage_exp", # Harvest Skills "fishing_level", "fishing_exp", "herbalism_level", "herbalism_exp", "prospecting_level", "prospecting_exp", "carving_level", "carving_exp", "alchemy_level", "alchemy_exp", ]) EntityState.Limits = lambda config: { **{ "id": (-math.inf, math.inf), "npc_type": (-1, 3), # -1 for immortal "row": (0, config.MAP_SIZE-1), "col": (0, config.MAP_SIZE-1), "damage": (0, math.inf), "time_alive": (0, math.inf), "freeze": (0, math.inf), "item_level": (0, math.inf), "attacker_id": (-np.inf, math.inf), "latest_combat_tick": (0, math.inf), "health": (0, config.PLAYER_BASE_HEALTH), }, **({ "message": (0, config.COMMUNICATION_NUM_TOKENS), } if config.COMMUNICATION_SYSTEM_ENABLED else {}), **({ "gold": (0, math.inf), "food": (0, config.RESOURCE_BASE), "water": (0, config.RESOURCE_BASE), } if config.RESOURCE_SYSTEM_ENABLED else {}), **({ "melee_level": (0, config.PROGRESSION_LEVEL_MAX), "melee_exp": (0, math.inf), "range_level": (0, config.PROGRESSION_LEVEL_MAX), "range_exp": (0, math.inf), "mage_level": (0, config.PROGRESSION_LEVEL_MAX), "mage_exp": (0, math.inf), "fishing_level": (0, config.PROGRESSION_LEVEL_MAX), "fishing_exp": (0, math.inf), "herbalism_level": (0, config.PROGRESSION_LEVEL_MAX), "herbalism_exp": (0, math.inf), "prospecting_level": (0, config.PROGRESSION_LEVEL_MAX), "prospecting_exp": (0, math.inf), "carving_level": (0, config.PROGRESSION_LEVEL_MAX), "carving_exp": (0, math.inf), "alchemy_level": (0, config.PROGRESSION_LEVEL_MAX), "alchemy_exp": (0, math.inf), } if config.PROGRESSION_SYSTEM_ENABLED else {}), } EntityState.State.comm_attr_map = {name: EntityState.State.attr_name_to_col[name] for name in ["id", "row", "col", "message"]} CommAttr = np.array(list(EntityState.State.comm_attr_map.values()), dtype=np.int64) EntityState.Query = SimpleNamespace( # Whole table table=lambda ds: ds.table("Entity").where_neq( EntityState.State.attr_name_to_col["id"], 0), # Single entity by_id=lambda ds, id: ds.table("Entity").where_eq( EntityState.State.attr_name_to_col["id"], id)[0], # Multiple entities by_ids=lambda ds, ids: ds.table("Entity").where_in( EntityState.State.attr_name_to_col["id"], ids), # Entities in a radius window=lambda ds, r, c, radius: ds.table("Entity").window( EntityState.State.attr_name_to_col["row"], EntityState.State.attr_name_to_col["col"], r, c, radius), # Communication obs comm_obs=lambda ds: ds.table("Entity").where_gt( EntityState.State.attr_name_to_col["id"], 0)[:, CommAttr] ) class Resources: def __init__(self, ent, config): self.config = config self.health = ent.health self.water = ent.water self.food = ent.food self.health_restore = 0 self.resilient = False self.health.update(config.PLAYER_BASE_HEALTH) if config.RESOURCE_SYSTEM_ENABLED: self.water.update(config.RESOURCE_BASE) self.food.update(config.RESOURCE_BASE) def update(self, immortal=False): if not self.config.RESOURCE_SYSTEM_ENABLED or immortal: return regen = self.config.RESOURCE_HEALTH_RESTORE_FRACTION thresh = self.config.RESOURCE_HEALTH_REGEN_THRESHOLD food_thresh = self.food > thresh * self.config.RESOURCE_BASE water_thresh = self.water > thresh * self.config.RESOURCE_BASE org_health = self.health.val if food_thresh and water_thresh: restore = np.floor(self.health.max * regen) self.health.increment(restore) if self.food.empty: starvation_damage = self.config.RESOURCE_STARVATION_RATE if self.resilient: starvation_damage *= self.config.RESOURCE_DAMAGE_REDUCTION self.health.decrement(int(starvation_damage)) if self.water.empty: dehydration_damage = self.config.RESOURCE_DEHYDRATION_RATE if self.resilient: dehydration_damage *= self.config.RESOURCE_DAMAGE_REDUCTION self.health.decrement(int(dehydration_damage)) # records both increase and decrease in health due to food and water self.health_restore = self.health.val - org_health def packet(self): data = {} data['health'] = { 'val': self.health.val, 'max': self.config.PLAYER_BASE_HEALTH } data['food'] = data['water'] = { 'val': 0, 'max': 0 } if self.config.RESOURCE_SYSTEM_ENABLED: data['food'] = { 'val': self.food.val, 'max': self.config.RESOURCE_BASE } data['water'] = { 'val': self.water.val, 'max': self.config.RESOURCE_BASE } return data class Status: def __init__(self, ent): self.freeze = ent.freeze def update(self): if self.frozen: self.freeze.decrement(1) def packet(self): data = {} data['freeze'] = self.freeze.val return data @property def frozen(self): return self.freeze.val > 0 # NOTE: History.packet() is actively used in visulazing attacks class History: def __init__(self, ent): self.actions = {} self.attack = None self.starting_position = ent.pos self.exploration = 0 self.player_kills = 0 self.damage_received = 0 self.damage_inflicted = 0 self.damage = ent.damage self.time_alive = ent.time_alive self.last_pos = None def update(self, entity, actions): self.attack = None self.damage.update(0) self.actions = {} if entity.ent_id in actions: self.actions = actions[entity.ent_id] self.time_alive.increment() def packet(self): data = {} data['damage'] = self.damage.val data['timeAlive'] = self.time_alive.val data['damage_inflicted'] = self.damage_inflicted data['damage_received'] = self.damage_received if self.attack is not None: data['attack'] = self.attack # NOTE: the client seems to use actions for visualization # but produces errors with the new actions. So we comment out these for now # actions = {} # for atn, args in self.actions.items(): # atn_packet = {} # # Avoid recursive player packet # if atn.__name__ == 'Attack': # continue # for key, val in args.items(): # if hasattr(val, 'packet'): # atn_packet[key.__name__] = val.packet # else: # atn_packet[key.__name__] = val.__name__ # actions[atn.__name__] = atn_packet # data['actions'] = actions data['actions'] = {} return data # pylint: disable=no-member class Entity(EntityState): def __init__(self, realm, pos, entity_id, name): super().__init__(realm.datastore, EntityState.Limits(realm.config)) self.realm = realm self.config = realm.config # TODO: do not access realm._np_random directly # related to the whole NPC, scripted logic # pylint: disable=protected-access self._np_random = realm._np_random self.policy = name self.repr = None self.name = name + str(entity_id) self._pos = None self.set_pos(*pos) self.ent_id = entity_id self.id.update(entity_id) self.vision = self.config.PLAYER_VISION_RADIUS self.attacker = None self.target = None self.closest = None self.spawn_pos = pos self._immortal = False # used for testing/player recon self._recon = False # Submodules self.status = Status(self) self.history = History(self) self.resources = Resources(self, self.config) self.inventory = inventory.Inventory(realm, self) # @property # def ent_id(self): # return self.id.val def packet(self): data = {} data['status'] = self.status.packet() data['history'] = self.history.packet() data['inventory'] = self.inventory.packet() data['alive'] = self.alive data['base'] = { 'r': self.pos[0], 'c': self.pos[1], 'name': self.name, 'level': self.attack_level, 'item_level': self.item_level.val,} return data def update(self, realm, actions): '''Update occurs after actions, e.g. does not include history''' self._pos = None if self.history.damage == 0: self.attacker = None self.attacker_id.update(0) if realm.config.EQUIPMENT_SYSTEM_ENABLED: self.item_level.update(self.equipment.total(lambda e: e.level)) self.status.update() self.history.update(self, actions) # Returns True if the entity is alive def receive_damage(self, source, dmg): self.history.damage_received += dmg self.history.damage.update(dmg) self.resources.health.decrement(dmg) if self.alive: return True # at this point, self is dead if source: source.history.player_kills += 1 self.realm.event_log.record(EventCode.PLAYER_KILL, source, target=self) # if self is dead, unlist its items from the market regardless of looting if self.config.EXCHANGE_SYSTEM_ENABLED: for item in list(self.inventory.items): self.realm.exchange.unlist_item(item) # if self is dead but no one can loot, destroy its items if source is None or not source.is_player: # nobody or npcs cannot loot if self.config.ITEM_SYSTEM_ENABLED: for item in list(self.inventory.items): item.destroy() return False # now, source can loot the dead self return False # pylint: disable=unused-argument def apply_damage(self, dmg, style): self.history.damage_inflicted += dmg @property def pos(self): if self._pos is None: self._pos = (self.row.val, self.col.val) return self._pos def set_pos(self, row, col): self._pos = (row, col) self.row.update(row) self.col.update(col) @property def alive(self): return self.resources.health.val > 0 @property def immortal(self): return self._immortal @property def is_player(self) -> bool: return False @property def is_npc(self) -> bool: return False @property def is_recon(self): return self._recon @property def attack_level(self) -> int: melee = self.skills.melee.level.val ranged = self.skills.range.level.val mage = self.skills.mage.level.val return int(max(melee, ranged, mage)) @property def in_combat(self) -> bool: # NOTE: the initial latest_combat_tick is 0, and valid values are greater than 0 if not self.config.COMBAT_SYSTEM_ENABLED or self.latest_combat_tick.val == 0: return False return (self.realm.tick - self.latest_combat_tick.val) < self.config.COMBAT_STATUS_DURATION ================================================ FILE: nmmo/entity/entity_manager.py ================================================ from collections.abc import Mapping from typing import Dict from nmmo.entity.entity import Entity, EntityState from nmmo.entity.player import Player from nmmo.lib import spawn, event_code class EntityGroup(Mapping): def __init__(self, realm, np_random): self.datastore = realm.datastore self.realm = realm self.config = realm.config self._np_random = np_random self._entity_table = EntityState.Query.table(self.datastore) self.entities: Dict[int, Entity] = {} self.dead_this_tick: Dict[int, Entity] = {} self._delete_dead_entity = True # is default def __len__(self): return len(self.entities) def __contains__(self, e): return e in self.entities def __getitem__(self, key) -> Entity: return self.entities[key] def __iter__(self) -> Entity: yield from self.entities def items(self): return self.entities.items() @property def corporeal(self): return {**self.entities, **self.dead_this_tick} @property def packet(self): return {k: v.packet() for k, v in self.corporeal.items()} def reset(self, np_random, delete_dead_entity=True): self._np_random = np_random # reset the RNG self._delete_dead_entity = delete_dead_entity for ent in self.entities.values(): # destroy the items if self.config.ITEM_SYSTEM_ENABLED: for item in list(ent.inventory.items): item.destroy() ent.datastore_record.delete() self.entities.clear() self.dead_this_tick.clear() def spawn_entity(self, entity): pos, ent_id = entity.pos, entity.id.val self.realm.map.tiles[pos].add_entity(entity) self.entities[ent_id] = entity def cull_entity(self, entity): pos, ent_id = entity.pos, entity.id.val self.realm.map.tiles[pos].remove_entity(ent_id) self.entities.pop(ent_id) # destroy the remaining items (of starved/dehydrated players) # of the agents who don't go through receive_damage() if self.config.ITEM_SYSTEM_ENABLED: for item in list(entity.inventory.items): item.destroy() if ent_id > 0: self.realm.event_log.record(event_code.EventCode.AGENT_CULLED, entity) def cull(self): self.dead_this_tick.clear() for ent in [ent for ent in self.entities.values() if not ent.alive]: self.dead_this_tick[ent.ent_id] = ent self.cull_entity(ent) if self._delete_dead_entity: ent.datastore_record.delete() return self.dead_this_tick def update(self, actions): # # batch updates # # time_alive, damage are from entity.py, History.update() # ent_idx = self._entity_table[:, EntityState.State.attr_name_to_col["id"]] != 0 # self._entity_table[ent_idx, EntityState.State.attr_name_to_col["time_alive"]] += 1 # self._entity_table[ent_idx, EntityState.State.attr_name_to_col["damage"]] = 0 # # freeze from entity.py, Status.update() # freeze_idx = self._entity_table[:, EntityState.State.attr_name_to_col["freeze"]] > 0 # self._entity_table[freeze_idx, EntityState.State.attr_name_to_col["freeze"]] -= 1 for entity in self.entities.values(): entity.update(self.realm, actions) class PlayerManager(EntityGroup): def spawn(self, agent_loader: spawn.SequentialLoader = None): if agent_loader is None: agent_loader = self.config.PLAYER_LOADER(self.config, self._np_random) # Check and assign the reslient flag resilient_flag = [False] * self.config.PLAYER_N if self.config.RESOURCE_SYSTEM_ENABLED: num_resilient = round(self.config.RESOURCE_RESILIENT_POPULATION * self.config.PLAYER_N) for idx in range(num_resilient): resilient_flag[idx] = self.config.RESOURCE_DAMAGE_REDUCTION > 0 self._np_random.shuffle(resilient_flag) # Spawn the players for agent_id in self.config.POSSIBLE_AGENTS: r, c = agent_loader.get_spawn_position(agent_id) if agent_id in self.entities: continue # NOTE: put spawn_individual() here. Is a separate function necessary? agent = next(agent_loader) # get agent cls from config.PLAYERS agent = agent(self.config, agent_id) player = Player(self.realm, (r, c), agent, resilient_flag[agent_id-1]) super().spawn_entity(player) ================================================ FILE: nmmo/entity/npc.py ================================================ import numpy as np from nmmo.entity import entity from nmmo.core import action as Action from nmmo.systems import combat, droptable from nmmo.systems import item as Item from nmmo.systems import skill from nmmo.systems.inventory import EquipmentSlot from nmmo.lib.event_code import EventCode from nmmo.lib import utils, astar DIRECTIONS = [ # row delta, col delta, action (-1, 0, Action.North), (1, 0, Action.South), (0, -1, Action.West), (0, 1, Action.East)] * 2 DELTA_TO_DIR = {(r, c): atn for r, c, atn in DIRECTIONS} DELTA_TO_DIR[(0, 0)] = None def get_habitable_dir(ent): r, c = ent.pos is_habitable = ent.realm.map.habitable_tiles start = ent._np_random.get_direction() # pylint: disable=protected-access for i in range(4): delta_r, delta_c, direction = DIRECTIONS[start + i] if is_habitable[r + delta_r, c + delta_c]: return direction return Action.North def meander_toward(ent, goal, dist_crit=10, toward_weight=3): r, c = ent.pos delta_r, delta_c = goal[0] - r, goal[1] - c abs_dr, abs_dc = abs(delta_r), abs(delta_c) dist_l1 = abs_dr + abs_dc # If close (less than dist_crit), use expensive aStar if dist_l1 <= dist_crit: delta = astar.aStar(ent.realm.map, ent.pos, goal) return move_action(DELTA_TO_DIR[delta] if delta in DELTA_TO_DIR else None) # Otherwise, use a weighted random walk cand_dirs = [] weights = [] for i in range(4): r_offset, c_offset, direction = DIRECTIONS[i] if ent.realm.map.habitable_tiles[r + r_offset, c + c_offset]: cand_dirs.append(direction) weights.append(1) if r_offset * delta_r > 0: weights[-1] += toward_weight * abs_dr/dist_l1 if c_offset * delta_c > 0: weights[-1] += toward_weight * abs_dc/dist_l1 if len(cand_dirs) == 0: return move_action(Action.North) if len(cand_dirs) == 1: return move_action(cand_dirs[0]) weights = np.array(weights) # pylint: disable=protected-access return move_action(ent._np_random.choice(cand_dirs, p=weights/np.sum(weights))) def move_action(direction): return {Action.Move: {Action.Direction: direction}} if direction else {} class Equipment: def __init__(self, total, melee_attack, range_attack, mage_attack, melee_defense, range_defense, mage_defense): self.level = total self.ammunition = EquipmentSlot() self.melee_attack = melee_attack self.range_attack = range_attack self.mage_attack = mage_attack self.melee_defense = melee_defense self.range_defense = range_defense self.mage_defense = mage_defense def total(self, getter): return getter(self) # pylint: disable=R0801 # Similar lines here and in inventory.py @property def packet(self): packet = {} packet["item_level"] = self.total packet["melee_attack"] = self.melee_attack packet["range_attack"] = self.range_attack packet["mage_attack"] = self.mage_attack packet["melee_defense"] = self.melee_defense packet["range_defense"] = self.range_defense packet["mage_defense"] = self.mage_defense return packet # pylint: disable=no-member class NPC(entity.Entity): def __init__(self, realm, pos, iden, name, npc_type): super().__init__(realm, pos, iden, name) self.skills = skill.Combat(realm, self) self.realm = realm self.last_action = None self.droptable = None self.spawn_danger = None self.equipment = None self.npc_type.update(npc_type) @property def is_npc(self) -> bool: return True def update(self, realm, actions): super().update(realm, actions) if not self.alive: return self.resources.health.increment(1) self.last_action = actions def can_see(self, target): if target is None or target.immortal: return False distance = utils.linf_single(self.pos, target.pos) return distance <= self.vision def _move_toward(self, goal): delta = astar.aStar(self.realm.map, self.pos, goal) return move_action(DELTA_TO_DIR[delta] if delta in DELTA_TO_DIR else None) def _meander(self): return move_action(get_habitable_dir(self)) def can_attack(self, target): if target is None or not self.config.NPC_SYSTEM_ENABLED or target.immortal: return False if not self.config.NPC_ALLOW_ATTACK_OTHER_NPCS and target.is_npc: return False distance = utils.linf_single(self.pos, target.pos) return distance <= self.skills.style.attack_range(self.realm.config) def _has_target(self, search=False): if self.target and (not self.target.alive or not self.can_see(self.target)): self.target = None # NOTE: when attacked by several agents, this will always target the last attacker if self.attacker and self.target is None: self.target = self.attacker if self.target is None and search is True: self.target = utils.identify_closest_target(self) return self.target def _add_attack_action(self, actions, target): actions.update({Action.Attack: {Action.Style: self.skills.style, Action.Target: target}}) def _charge_toward(self, target): actions = self._move_toward(target.pos) if self.can_attack(target): self._add_attack_action(actions, target) return actions # Returns True if the entity is alive def receive_damage(self, source, dmg): if super().receive_damage(source, dmg): return True # run the next lines if the npc is killed # source receive gold & items in the droptable # pylint: disable=no-member if self.gold.val > 0: source.gold.increment(self.gold.val) self.realm.event_log.record(EventCode.LOOT_GOLD, source, amount=self.gold.val, target=self) self.gold.update(0) if self.droptable: for item in self.droptable.roll(self.realm, self.attack_level): if source.is_player and source.inventory.space: # inventory.receive() returns True if the item is received # if source does not have space, inventory.receive() destroys the item if source.inventory.receive(item): self.realm.event_log.record(EventCode.LOOT_ITEM, source, item=item, target=self) else: item.destroy() return False @staticmethod def default_spawn(realm, pos, iden, np_random, danger=None): config = realm.config # check the position if realm.map.tiles[pos].impassible: return None # Select AI Policy danger = danger or combat.danger(config, pos) if danger >= config.NPC_SPAWN_AGGRESSIVE: ent = Aggressive(realm, pos, iden) elif danger >= config.NPC_SPAWN_NEUTRAL: ent = PassiveAggressive(realm, pos, iden) elif danger >= config.NPC_SPAWN_PASSIVE: ent = Passive(realm, pos, iden) else: return None ent.spawn_danger = danger # Select combat focus style = np_random.integers(0,3) if style == 0: style = Action.Melee elif style == 1: style = Action.Range else: style = Action.Mage ent.skills.style = style # Compute level level = 0 if config.PROGRESSION_SYSTEM_ENABLED: level_min = config.NPC_LEVEL_MIN level_max = config.NPC_LEVEL_MAX level = int(danger * (level_max - level_min) + level_min) # Set skill levels if style == Action.Melee: ent.skills.melee.set_experience_by_level(level) elif style == Action.Range: ent.skills.range.set_experience_by_level(level) elif style == Action.Mage: ent.skills.mage.set_experience_by_level(level) # Gold if config.EXCHANGE_SYSTEM_ENABLED: # pylint: disable=no-member ent.gold.update(level) ent.droptable = droptable.Standard() # Equipment to instantiate if config.EQUIPMENT_SYSTEM_ENABLED: lvl = level - np_random.random() ilvl = int(5 * lvl) level_damage = config.NPC_LEVEL_DAMAGE * config.NPC_LEVEL_MULTIPLIER level_defense = config.NPC_LEVEL_DEFENSE * config.NPC_LEVEL_MULTIPLIER offense = int(config.NPC_BASE_DAMAGE + lvl * level_damage) defense = int(config.NPC_BASE_DEFENSE + lvl * level_defense) ent.equipment = Equipment(ilvl, offense, offense, offense, defense, defense, defense) armor = [Item.Hat, Item.Top, Item.Bottom] ent.droptable.add(np_random.choice(armor)) if config.PROFESSION_SYSTEM_ENABLED: tools = [Item.Rod, Item.Gloves, Item.Pickaxe, Item.Axe, Item.Chisel] ent.droptable.add(np_random.choice(tools)) return ent def packet(self): data = super().packet() data["skills"] = self.skills.packet() data["resource"] = { "health": { "val": self.resources.health.val, "max": self.config.PLAYER_BASE_HEALTH } } return data class Passive(NPC): def __init__(self, realm, pos, iden, name=None): super().__init__(realm, pos, iden, name or "Passive", 1) def decide(self): # Move only, no attack return self._meander() class PassiveAggressive(NPC): def __init__(self, realm, pos, iden, name=None): super().__init__(realm, pos, iden, name or "Neutral", 2) def decide(self): if self._has_target() is None: return self._meander() return self._charge_toward(self.target) class Aggressive(NPC): def __init__(self, realm, pos, iden, name=None): super().__init__(realm, pos, iden, name or "Hostile", 3) def decide(self): if self._has_target(search=True) is None: return self._meander() return self._charge_toward(self.target) class Soldier(NPC): def __init__(self, realm, pos, iden, name, order): super().__init__(realm, pos, iden, name or "Soldier", 3) # Hostile with order self.target_entity = None self.rally_point = None self._process_order(order) def _process_order(self, order): if order is None: return if "destroy" in order: # destroy the specified entity id self.target_entity = self.realm.entity(order["destroy"]) if "rally" in order: # rally until spotting an enemy self.rally_point = order["rally"] # (row, col) def _is_order_done(self, radius=5): if self.target_entity and not self.target_entity.alive: self.target_entity = None if self.rally_point and utils.linf_single(self.pos, self.rally_point) <= radius: self.rally_point = None def decide(self): self._is_order_done() # NOTE: destroying the target entity is the highest priority if self.target_entity is None and self._has_target(search=True): if self.can_attack(self.target): return self._charge_toward(self.target) actions = self._decide_move_action() self._decide_attack_action(actions) return actions def _decide_move_action(self): # in the order of priority if self.target_entity: return self._move_toward(self.target_entity.pos) if self.target: # If it"s close enough, it will use A*. Otherwise, random. return meander_toward(self, self.target.pos) if self.rally_point: return meander_toward(self, self.rally_point) return self._meander() def _decide_attack_action(self, actions): # The default is to attack the target entity, if within range if self.target_entity and self.can_attack(self.target_entity): self._add_attack_action(actions, self.target_entity) elif self.can_attack(self.target): self._add_attack_action(actions, self.target) ================================================ FILE: nmmo/entity/npc_manager.py ================================================ from typing import Callable from nmmo.entity.entity_manager import EntityGroup from nmmo.entity.npc import NPC, Soldier, Aggressive, PassiveAggressive, Passive from nmmo.core import action from nmmo.systems import combat from nmmo.lib import spawn class NPCManager(EntityGroup): def __init__(self, realm, np_random): super().__init__(realm, np_random) self.next_id = -1 self.spawn_dangers = [] def reset(self, np_random): super().reset(np_random) self.next_id = -1 self.spawn_dangers.clear() def actions(self): return {idx: entity.decide() for idx, entity in self.entities.items()} def default_spawn(self): config = self.config if not config.NPC_SYSTEM_ENABLED: return for _ in range(config.NPC_SPAWN_ATTEMPTS): if len(self.entities) >= config.NPC_N: break if len(self.spawn_dangers) > 0: danger = self.spawn_dangers.pop(0) # FIFO r, c = combat.spawn(config, danger, self._np_random) else: center = config.MAP_CENTER border = self.config.MAP_BORDER # pylint: disable=unbalanced-tuple-unpacking r, c = self._np_random.integers(border, center+border, 2).tolist() npc = NPC.default_spawn(self.realm, (r, c), self.next_id, self._np_random) if npc: super().spawn_entity(npc) self.next_id -= 1 def spawn_npc(self, r, c, danger=None, name=None, order=None, apply_beta_to_danger=True): if not self.realm.map.tiles[r, c].habitable: return None if danger and apply_beta_to_danger: danger = min(1.0, max(0.0, danger)) # normalize danger = self._np_random.beta(10*danger+0.01, 10.01-10*danger) # beta cannot take 0 if danger is None: npc = Soldier(self.realm, (r, c), self.next_id, name, order) elif danger >= self.config.NPC_SPAWN_AGGRESSIVE: npc = Aggressive(self.realm, (r, c), self.next_id, name) elif danger >= self.config.NPC_SPAWN_NEUTRAL: npc = PassiveAggressive(self.realm, (r, c), self.next_id, name) elif danger >= self.config.NPC_SPAWN_PASSIVE: npc = Passive(self.realm, (r, c), self.next_id, name) else: return None if npc: super().spawn_entity(npc) self.next_id -= 1 # NOTE: randomly set the combat style. revisit later npc.skills.style = self._np_random.choice([action.Melee, action.Range, action.Mage]) return npc def area_spawn(self, r_min, r_max, c_min, c_max, num_spawn, npc_init_fn: Callable): assert r_min < r_max and c_min < c_max, "Invalid area" assert num_spawn > 0, "Invalid number of spawns" while num_spawn > 0: r = self._np_random.integers(r_min, r_max+1) c = self._np_random.integers(c_min, c_max+1) if npc_init_fn(r, c): num_spawn -= 1 def edge_spawn(self, num_spawn, npc_init_fn: Callable): assert num_spawn > 0, "Invalid number of spawns" edge_locs = spawn.get_edge_tiles(self.config, self._np_random, shuffle=True) assert len(edge_locs) >= num_spawn, "Not enough edge locations" while num_spawn > 0: r, c = edge_locs.pop() npc = npc_init_fn(r, c) if npc: num_spawn -= 1 ================================================ FILE: nmmo/entity/player.py ================================================ from nmmo.systems.skill import Skills from nmmo.entity import entity from nmmo.lib.event_code import EventCode from nmmo.lib import spawn # pylint: disable=no-member class Player(entity.Entity): def __init__(self, realm, pos, agent, resilient=False): super().__init__(realm, pos, agent.iden, agent.policy) self.agent = agent self._immortal = realm.config.IMMORTAL self.resources.resilient = resilient self.my_task = None self._make_mortal_tick = None # set to realm.tick when the player is made mortal # Scripted hooks self.target = None self.vision = 7 # Logs self.buys = 0 self.sells = 0 self.ration_consumed = 0 self.poultice_consumed = 0 self.ration_level_consumed = 0 self.poultice_level_consumed = 0 # initialize skills with the base level self.skills = Skills(realm, self) if realm.config.PROGRESSION_SYSTEM_ENABLED: for skill in self.skills.skills: skill.level.update(realm.config.PROGRESSION_BASE_LEVEL) # Gold: initialize with 1 gold (EXCHANGE_BASE_GOLD). # If the base amount is more than 1, alss check the npc's init gold. if realm.config.EXCHANGE_SYSTEM_ENABLED: self.gold.update(realm.config.EXCHANGE_BASE_GOLD) @property def serial(self): return self.ent_id @property def is_player(self) -> bool: return True @property def level(self) -> int: # a player's level is the max of all skills # CHECK ME: the initial level is 1 because of Basic skills, # which are harvesting food/water and don't progress return max(e.level.val for e in self.skills.skills) def _set_immortal(self, value=True, duration=None): self._immortal = value # NOTE: a hack to mark the player as immortal in action targets self.npc_type.update(-1 if value else 0) if value and duration is not None: self._make_mortal_tick = self.realm.tick + duration if value is False: self._make_mortal_tick = None def make_recon(self, new_pos=None): # NOTE: scout cannot act and cannot die self.status.freeze.update(self.config.MAX_HORIZON) self._set_immortal() self._recon = True if new_pos is not None: if self.ent_id in self.realm.map.tiles[self.pos].entities: self.realm.map.tiles[self.pos].remove_entity(self.ent_id) self.realm.map.tiles[new_pos].add_entity(self) self.set_pos(*new_pos) def apply_damage(self, dmg, style): super().apply_damage(dmg, style) self.skills.apply_damage(style) # TODO(daveey): The returns for this function are a mess def receive_damage(self, source, dmg): if self.immortal: return False # super().receive_damage returns True if self is alive after taking dmg if super().receive_damage(source, dmg): return True if not self.config.ITEM_SYSTEM_ENABLED: return False # starting from here, source receive gold & inventory items if self.config.EXCHANGE_SYSTEM_ENABLED and source is not None: if self.gold.val > 0: source.gold.increment(self.gold.val) self.realm.event_log.record(EventCode.LOOT_GOLD, source, amount=self.gold.val, target=self) self.gold.update(0) # TODO: make source receive the highest-level items first # because source cannot take it if the inventory is full item_list = list(self.inventory.items) self._np_random.shuffle(item_list) for item in item_list: self.inventory.remove(item) # if source is None or NPC, destroy the item if source.is_player: # inventory.receive() returns True if the item is received # if source doesn't have space, inventory.receive() destroys the item if source.inventory.receive(item): self.realm.event_log.record(EventCode.LOOT_ITEM, source, item=item, target=self) else: item.destroy() # CHECK ME: this is an empty function. do we still need this? self.skills.receive_damage(dmg) return False @property def equipment(self): return self.inventory.equipment def packet(self): data = super().packet() data['entID'] = self.ent_id data['resource'] = self.resources.packet() data['skills'] = self.skills.packet() data['inventory'] = self.inventory.packet() # added for the 2.0 web client data["metrics"] = { "PlayerDefeats": self.history.player_kills, "TimeAlive": self.time_alive.val, "Gold": self.gold.val, "DamageTaken": self.history.damage_received,} return data def update(self, realm, actions): '''Post-action update. Do not include history''' super().update(realm, actions) # Spawn battle royale style death fog # Starts at 0 damage on the specified config tick # Moves in from the edges by 1 damage per tile per tick # So after 10 ticks, you take 10 damage at the edge and 1 damage # 10 tiles in, 0 damage in farther # This means all agents will be force killed around # MAP_CENTER / 2 + 100 ticks after spawning fog = self.config.DEATH_FOG_ONSET if fog is not None and self.realm.tick >= fog: dmg = self.realm.fog_map[self.pos] if dmg > 0.5: # fog_map has float values self.receive_damage(None, round(dmg)) if not self.alive: return if self.config.PLAYER_HEALTH_INCREMENT > 0: self.resources.health.increment(self.config.PLAYER_HEALTH_INCREMENT) self.resources.update(self.immortal) self.skills.update() if self._make_mortal_tick is not None and self.realm.tick >= self._make_mortal_tick: self._set_immortal(False) def resurrect(self, health_prop=0.5, freeze_duration=10, edge_spawn=True): # Respawn dead players at the edge assert not self.alive, "Player is not dead" self.status.freeze.update(freeze_duration) self.resources.health.update(self.config.PLAYER_BASE_HEALTH*health_prop) if self.config.RESOURCE_SYSTEM_ENABLED: self.resources.water.update(self.config.RESOURCE_BASE) self.resources.food.update(self.config.RESOURCE_BASE) if edge_spawn: new_spawn_pos = spawn.get_random_coord(self.config, self._np_random, edge=True) else: while True: new_spawn_pos = spawn.get_random_coord(self.config, self._np_random, edge=False) if self.realm.map.tiles[new_spawn_pos].habitable: break self.set_pos(*new_spawn_pos) self.message.update(0) self.realm.players.spawn_entity(self) # put back to the system self._set_immortal(duration=freeze_duration) if self.my_task and len(self.my_task.assignee) == 1: # NOTE: Only one task per agent is supported for now # Agent's task progress need to be reset ONLY IF the task is an agent task self.my_task.reset() ================================================ FILE: nmmo/lib/__init__.py ================================================ ================================================ FILE: nmmo/lib/astar.py ================================================ #pylint: disable=invalid-name import heapq from nmmo.lib.utils import in_bounds CUTOFF = 100 def l1(start, goal): sr, sc = start gr, gc = goal return abs(gr - sr) + abs(gc - sc) def adjacentPos(pos): r, c = pos return [(r - 1, c), (r, c - 1), (r + 1, c), (r, c + 1)] def aStar(realm_map, start, goal, cutoff = CUTOFF): tiles = realm_map.tiles if start == goal: return (0, 0) if (start, goal) in realm_map.pathfinding_cache: return realm_map.pathfinding_cache[(start, goal)] initial_goal = goal pq = [(0, start)] backtrace = {} cost = {start: 0} closestPos = start closestHeuristic = l1(start, goal) closestCost = closestHeuristic while pq: # Use approximate solution if budget exhausted cutoff -= 1 if cutoff <= 0: if goal not in backtrace: goal = closestPos break priority, cur = heapq.heappop(pq) if cur == goal: break for nxt in adjacentPos(cur): if not in_bounds(*nxt, tiles.shape) or realm_map.habitable_tiles[nxt] == 0: continue newCost = cost[cur] + 1 if nxt not in cost or newCost < cost[nxt]: cost[nxt] = newCost heuristic = l1(goal, nxt) priority = newCost + heuristic # Compute approximate solution if heuristic < closestHeuristic or ( heuristic == closestHeuristic and priority < closestCost): closestPos = nxt closestHeuristic = heuristic closestCost = priority heapq.heappush(pq, (priority, nxt)) backtrace[nxt] = cur while goal in backtrace and backtrace[goal] != start: gr, gc = goal goal = backtrace[goal] sr, sc = goal realm_map.pathfinding_cache[(goal, initial_goal)] = (gr - sr, gc - sc) sr, sc = start gr, gc = goal realm_map.pathfinding_cache[(start, initial_goal)] = (gr - sr, gc - sc) return (gr - sr, gc - sc) # End A* ================================================ FILE: nmmo/lib/colors.py ================================================ # pylint: disable=all #Various Enums used for handling materials, entity types, etc. #Data texture pairs are used for enums that require textures. #These textures are filled in by the Render class at run time. import numpy as np import colorsys def rgb(h): h = h.lstrip('#') return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) def rgbNorm(h): h = h.lstrip('#') return tuple(int(h[i:i+2], 16)/255.0 for i in (0, 2, 4)) def makeColor(idx, h=1, s=1, v=1): r, g, b = colorsys.hsv_to_rgb(h, s, v) rgbval = tuple(int(255*e) for e in [r, g, b]) hexval = '%02x%02x%02x' % rgbval return Color(str(idx), hexval) class Color: def __init__(self, name, hexVal): self.name = name self.hex = hexVal self.rgb = rgb(hexVal) self.norm = rgbNorm(hexVal) self.value = self.rgb #Emulate enum def packet(self): return self.hex class Color256: def make256(): parh, parv = np.meshgrid(np.linspace(0.075, 1, 16), np.linspace(0.25, 1, 16)[::-1]) parh, parv = parh.T.ravel(), parv.T.ravel() idxs = np.arange(256) params = zip(idxs, parh, parv) colors = [makeColor(idx, h=h, s=1, v=v) for idx, h, v in params] return colors colors = make256() class Color16: def make(): hues = np.linspace(0, 1, 16) idxs = np.arange(256) params = zip(idxs, hues) colors = [makeColor(idx, h=h, s=1, v=1) for idx, h in params] return colors colors = make() class Tier: BLACK = Color('BLACK', '#000000') WOOD = Color('WOOD', '#784d1d') BRONZE = Color('BRONZE', '#db4508') SILVER = Color('SILVER', '#dedede') GOLD = Color('GOLD', '#ffae00') PLATINUM = Color('PLATINUM', '#cd75ff') DIAMOND = Color('DIAMOND', '#00bbbb') class Swatch: def colors(): '''Return list of swatch colors''' return def rand(): '''Return random swatch color''' all_colors = Swatch.colors() randInd = np.random.randint(0, len(all_colors)) return all_colors[randInd] class Neon(Swatch): RED = Color('RED', '#ff0000') ORANGE = Color('ORANGE', '#ff8000') YELLOW = Color('YELLOW', '#ffff00') GREEN = Color('GREEN', '#00ff00') MINT = Color('MINT', '#00ff80') CYAN = Color('CYAN', '#00ffff') BLUE = Color('BLUE', '#0000ff') PURPLE = Color('PURPLE', '#8000ff') MAGENTA = Color('MAGENTA', '#ff00ff') FUCHSIA = Color('FUCHSIA', '#ff0080') SPRING = Color('SPRING', '#80ff80') SKY = Color('SKY', '#0080ff') WHITE = Color('WHITE', '#ffffff') GRAY = Color('GRAY', '#666666') BLACK = Color('BLACK', '#000000') BLOOD = Color('BLOOD', '#bb0000') BROWN = Color('BROWN', '#7a3402') GOLD = Color('GOLD', '#eec600') SILVER = Color('SILVER', '#b8b8b8') TERM = Color('TERM', '#41ff00') MASK = Color('MASK', '#d67fff') def colors(): return ( Neon.CYAN, Neon.MINT, Neon.GREEN, Neon.BLUE, Neon.PURPLE, Neon.MAGENTA, Neon.FUCHSIA, Neon.SPRING, Neon.SKY, Neon.RED, Neon.ORANGE, Neon.YELLOW) class Solid(Swatch): BLUE = Color('BLUE', '#1f77b4') ORANGE = Color('ORANGE', '#ff7f0e') GREEN = Color('GREEN', '#2ca02c') RED = Color('RED', '#D62728') PURPLE = Color('PURPLE', '#9467bd') BROWN = Color('BROWN', '#8c564b') PINK = Color('PINK', '#e377c2') GREY = Color('GREY', '#7f7f7f') CHARTREUSE = Color('CHARTREUSE', '#bcbd22') SKY = Color('SKY', '#17becf') def colors(): return ( Solid.BLUE, Solid.ORANGE, Solid.GREEN, Solid.RED, Solid.PURPLE, Solid.BROWN, Solid.PINK, Solid.CHARTREUSE, Solid.SKY, Solid.GREY) class Palette: def __init__(self, initial_swatch=Neon): self.colors = {} for idx, color in enumerate(initial_swatch.colors()): self.colors[idx] = color def color(self, idx): if idx in self.colors: return self.colors[idx] color = makeColor(idx, h=np.random.rand(), s=1, v=1) self.colors[idx] = color return color ================================================ FILE: nmmo/lib/cython_helper.pyx ================================================ #cython: boundscheck=True #cython: wraparound=True #cython: nonecheck=True from types import SimpleNamespace import numpy as np cimport numpy as cnp # for array indexing cnp.import_array() def make_move_mask(cnp.ndarray[cnp.int8_t] mask, cnp.ndarray[cnp.int8_t, ndim=2] habitable_tiles, short row, short col, cnp.ndarray[cnp.int64_t] row_delta, cnp.ndarray[cnp.int64_t] col_delta): for i in range(4): mask[i] = habitable_tiles[row_delta[i] + row, col_delta[i] + col] # NOTE: assume that incoming mask are all zeros def make_attack_mask(cnp.ndarray[cnp.int8_t] mask, cnp.ndarray[cnp.int16_t, ndim=2] entities, dict entity_attr, dict my_info): cdef short idx cdef short num_valid_target = 0 cdef short attr_id = entity_attr["id"] cdef short attr_time_alive = entity_attr["time_alive"] cdef short attr_npc_type = entity_attr["npc_type"] cdef short attr_row = entity_attr["row"] cdef short attr_col = entity_attr["col"] for idx in range(len(entities)): # skip empty row if entities[idx, attr_id] == 0: continue # out of range if abs(entities[idx, attr_row] - my_info["row"]) > my_info["attack_range"] or \ abs(entities[idx, attr_col] - my_info["col"]) > my_info["attack_range"]: continue # cannot attack during immunity if entities[idx, attr_id] > 0 and \ entities[idx, attr_time_alive] < my_info["immunity"]: continue # cannot attack self if entities[idx, attr_id] == my_info["agent_id"]: continue # npc_type must be 0, 1, 2, 3 if entities[idx, attr_npc_type] < 0: # immortal (-1) continue mask[idx] = 1 num_valid_target += 1 # cython: wraparound need to be True # if any valid target, set the no-op to 0 mask[-1] = 0 if num_valid_target > 0 else 1 def parse_array(short[:] data, dict attr_name_to_col): cdef short col cdef str attr cdef dict result = {} for attr, col in attr_name_to_col.items(): result[attr] = data[col] return SimpleNamespace(**result) ================================================ FILE: nmmo/lib/event_code.py ================================================ class EventCode: # Move EAT_FOOD = 1 DRINK_WATER = 2 GO_FARTHEST = 3 # record when breaking the previous record SEIZE_TILE = 4 # Attack SCORE_HIT = 11 PLAYER_KILL = 12 FIRE_AMMO = 13 # Item CONSUME_ITEM = 21 GIVE_ITEM = 22 DESTROY_ITEM = 23 HARVEST_ITEM = 24 EQUIP_ITEM = 25 LOOT_ITEM = 26 # Exchange GIVE_GOLD = 31 LIST_ITEM = 32 EARN_GOLD = 33 BUY_ITEM = 34 LOOT_GOLD = 35 # Level up LEVEL_UP = 41 # System-related AGENT_CULLED = 91 # player is removed from the realm (culled) ================================================ FILE: nmmo/lib/event_log.py ================================================ from types import SimpleNamespace from typing import List from copy import deepcopy from collections import defaultdict import numpy as np from nmmo.datastore.serialized import SerializedState from nmmo.entity import Entity from nmmo.systems.item import Item from nmmo.lib.event_code import EventCode # pylint: disable=no-member EventState = SerializedState.subclass("Event", [ "recorded", # event_log is write-only, no update or delete, so no need for row id "ent_id", "tick", "event", "type", "level", "number", "gold", "target_ent", ]) EventAttr = EventState.State.attr_name_to_col EventState.Query = SimpleNamespace( table=lambda ds: ds.table("Event").where_eq(EventAttr["recorded"], 1), by_event=lambda ds, event_code: ds.table("Event").where_eq( EventAttr["event"], event_code), by_tick=lambda ds, tick: ds.table("Event").where_eq( EventAttr["tick"], tick), ) # defining col synoyms for different event types ATTACK_COL_MAP = { 'combat_style': EventAttr['type'], 'damage': EventAttr['number']} ITEM_COL_MAP = { 'item_type': EventAttr['type'], 'quantity': EventAttr['number'], 'price': EventAttr['gold'], 'item_id': EventAttr['target_ent']} LEVEL_COL_MAP = {'skill': EventAttr['type']} EXPLORE_COL_MAP = {'distance': EventAttr['number']} TILE_COL_MAP = {'tile_row': EventAttr['number'], 'tile_col': EventAttr['gold']} class EventLogger(EventCode): def __init__(self, realm): self.realm = realm self.config = realm.config self.datastore = realm.datastore self.valid_events = { val: evt for evt, val in EventCode.__dict__.items() if isinstance(val, int) } self._data_by_tick = {} self._last_tick = 0 self._empty_data = np.empty((0, len(EventAttr))) # add synonyms to the attributes self.attr_to_col = deepcopy(EventAttr) self.attr_to_col.update(ATTACK_COL_MAP) self.attr_to_col.update(ITEM_COL_MAP) self.attr_to_col.update(LEVEL_COL_MAP) self.attr_to_col.update(EXPLORE_COL_MAP) self.attr_to_col.update(TILE_COL_MAP) def reset(self): EventState.State.table(self.datastore).reset() # define event logging def _create_event(self, entity: Entity, event_code: int): log = EventState(self.datastore) log.recorded.update(1) log.ent_id.update(entity.ent_id) # the tick increase by 1 after executing all actions log.tick.update(self.realm.tick+1) log.event.update(event_code) return log def record(self, event_code: int, entity: Entity, **kwargs): if event_code in [EventCode.EAT_FOOD, EventCode.DRINK_WATER, EventCode.GIVE_ITEM, EventCode.DESTROY_ITEM, EventCode.GIVE_GOLD, EventCode.AGENT_CULLED]: # Logs for these events are for counting only self._create_event(entity, event_code) return if event_code == EventCode.GO_FARTHEST: # use EXPLORE_COL_MAP if ('distance' in kwargs and kwargs['distance'] > 0): log = self._create_event(entity, event_code) log.number.update(kwargs['distance']) return if event_code == EventCode.SCORE_HIT: # kwargs['combat_style'] should be Skill.CombatSkill if ('combat_style' in kwargs and kwargs['combat_style'].SKILL_ID in [1, 2, 3]) & \ ('target' in kwargs and isinstance(kwargs['target'], Entity)) & \ ('damage' in kwargs and kwargs['damage'] >= 0): log = self._create_event(entity, event_code) log.type.update(kwargs['combat_style'].SKILL_ID) log.number.update(kwargs['damage']) log.target_ent.update(kwargs['target'].ent_id) return if event_code == EventCode.PLAYER_KILL: if ('target' in kwargs and isinstance(kwargs['target'], Entity)): target = kwargs['target'] log = self._create_event(entity, event_code) log.target_ent.update(target.ent_id) log.level.update(target.attack_level) return if event_code == EventCode.LOOT_ITEM: if ('item' in kwargs and isinstance(kwargs['item'], Item)) & \ ('target' in kwargs and isinstance(kwargs['target'], Entity)): item = kwargs['item'] log = self._create_event(entity, event_code) log.type.update(item.ITEM_TYPE_ID) log.level.update(item.level.val) log.number.update(item.quantity.val) log.target_ent.update(item.id.val) return if event_code == EventCode.LOOT_GOLD: if ('amount' in kwargs and kwargs['amount'] > 0) & \ ('target' in kwargs and isinstance(kwargs['target'], Entity)): log = self._create_event(entity, event_code) log.gold.update(kwargs['amount']) log.target_ent.update(kwargs['target'].ent_id) return if event_code in [EventCode.CONSUME_ITEM, EventCode.HARVEST_ITEM, EventCode.EQUIP_ITEM, EventCode.FIRE_AMMO]: if ('item' in kwargs and isinstance(kwargs['item'], Item)): item = kwargs['item'] log = self._create_event(entity, event_code) log.type.update(item.ITEM_TYPE_ID) log.level.update(item.level.val) log.number.update(item.quantity.val) log.target_ent.update(item.id.val) return if event_code in [EventCode.LIST_ITEM, EventCode.BUY_ITEM]: if ('item' in kwargs and isinstance(kwargs['item'], Item)) & \ ('price' in kwargs and kwargs['price'] > 0): item = kwargs['item'] log = self._create_event(entity, event_code) log.type.update(item.ITEM_TYPE_ID) log.level.update(item.level.val) log.number.update(item.quantity.val) log.gold.update(kwargs['price']) log.target_ent.update(item.id.val) return if event_code == EventCode.EARN_GOLD: if ('amount' in kwargs and kwargs['amount'] > 0): log = self._create_event(entity, event_code) log.gold.update(kwargs['amount']) return if event_code == EventCode.LEVEL_UP: # kwargs['skill'] should be Skill.Skill if ('skill' in kwargs and kwargs['skill'].SKILL_ID in range(1,9)) & \ ('level' in kwargs and kwargs['level'] >= 0): log = self._create_event(entity, event_code) log.type.update(kwargs['skill'].SKILL_ID) log.level.update(kwargs['level']) return if event_code == EventCode.SEIZE_TILE: if ('tile' in kwargs and isinstance(kwargs['tile'], tuple)): log = self._create_event(entity, event_code) log.number.update(kwargs['tile'][0]) # row log.gold.update(kwargs['tile'][1]) # col return # If reached here, then something is wrong # CHECK ME: The below should be commented out after debugging raise ValueError(f"Event code: {event_code}", kwargs) def update(self): curr_tick = self.realm.tick if curr_tick > self._last_tick: self._data_by_tick[curr_tick] = EventState.Query.by_tick(self.datastore, curr_tick) self._last_tick = curr_tick def get_data(self, event_code=None, agents: List[int]=None, tick: int=None) -> np.ndarray: if tick is not None: if tick == -1: tick = self._last_tick if tick not in self._data_by_tick: return self._empty_data event_data = self._data_by_tick[tick] else: event_data = EventState.Query.table(self.datastore) if event_data.shape[0] > 0: if event_code is None: flt_idx = event_data[:, EventAttr["event"]] > 0 else: flt_idx = event_data[:, EventAttr["event"]] == event_code if agents: flt_idx &= np.in1d(event_data[:, EventAttr["ent_id"]], agents) return event_data[flt_idx] return self._empty_data def get_stat(self): event_stat = defaultdict(lambda: defaultdict(int)) event_data = EventState.Query.table(self.datastore) for row in event_data: agent_id = row[EventAttr['ent_id']] if agent_id > 0: key = extract_event_key(row) if key is None: continue if key[0] == EventCode.GO_FARTHEST: event_stat[agent_id][key] = max(event_stat[agent_id][key], row[EventAttr['number']]) # distance elif key[0] in [EventCode.LEVEL_UP, EventCode.EQUIP_ITEM]: event_stat[agent_id][key] = max(event_stat[agent_id][key], row[EventAttr['level']]) elif key[0] == EventCode.AGENT_CULLED: event_stat[agent_id][key] = row[EventAttr['tick']] # lifespan else: event_stat[agent_id][key] += 1 return event_stat def extract_event_key(event_row): event_code = event_row[EventAttr['event']] if event_code in [ EventCode.EAT_FOOD, EventCode.DRINK_WATER, EventCode.GO_FARTHEST, EventCode.AGENT_CULLED, ]: return (event_code,) if event_code in [ EventCode.SCORE_HIT, EventCode.FIRE_AMMO, EventCode.LEVEL_UP, EventCode.HARVEST_ITEM, EventCode.CONSUME_ITEM, EventCode.EQUIP_ITEM, EventCode.LIST_ITEM, EventCode.BUY_ITEM, ]: return (event_code, event_row[EventAttr['type']]) if event_code == EventCode.PLAYER_KILL: return (event_code, int(event_row[EventAttr['target_ent']] > 0)) # if target is agent or npc return None ================================================ FILE: nmmo/lib/material.py ================================================ from nmmo.systems import item, droptable class Material: capacity = 0 tool = None table = None index = None respawn = 0 def __init__(self, config): pass def __eq__(self, mtl): return self.index == mtl.index def __equals__(self, mtl): return self == mtl def harvest(self): return self.__class__.table class Void(Material): tex = 'void' index = 0 class Water(Material): tex = 'water' index = 1 table = droptable.Empty() def __init__(self, config): self.deplete = __class__ self.respawn = 1.0 class Grass(Material): tex = 'grass' index = 2 class Scrub(Material): tex = 'scrub' index = 3 class Foilage(Material): tex = 'foilage' index = 4 deplete = Scrub table = droptable.Empty() def __init__(self, config): if config.RESOURCE_SYSTEM_ENABLED: self.capacity = config.RESOURCE_FOILAGE_CAPACITY self.respawn = config.RESOURCE_FOILAGE_RESPAWN class Stone(Material): tex = 'stone' index = 5 class Slag(Material): tex = 'slag' index = 6 class Ore(Material): tex = 'ore' index = 7 deplete = Slag tool = item.Pickaxe def __init__(self, config): cls = self.__class__ if cls.table is None: cls.table = droptable.Standard() cls.table.add(item.Whetstone) if config.EQUIPMENT_SYSTEM_ENABLED: cls.table.add(item.Wand, prob=config.WEAPON_DROP_PROB) if config.PROFESSION_SYSTEM_ENABLED: self.capacity = config.PROFESSION_ORE_CAPACITY self.respawn = config.PROFESSION_ORE_RESPAWN tool = item.Pickaxe deplete = Slag class Stump(Material): tex = 'stump' index = 8 class Tree(Material): tex = 'tree' index = 9 deplete = Stump tool = item.Axe def __init__(self, config): cls = self.__class__ if cls.table is None: cls.table = droptable.Standard() cls.table.add(item.Arrow) if config.EQUIPMENT_SYSTEM_ENABLED: cls.table.add(item.Spear, prob=config.WEAPON_DROP_PROB) if config.PROFESSION_SYSTEM_ENABLED: self.capacity = config.PROFESSION_TREE_CAPACITY self.respawn = config.PROFESSION_TREE_RESPAWN class Fragment(Material): tex = 'fragment' index = 10 class Crystal(Material): tex = 'crystal' index = 11 deplete = Fragment tool = item.Chisel def __init__(self, config): cls = self.__class__ if cls.table is None: cls.table = droptable.Standard() cls.table.add(item.Runes) if config.EQUIPMENT_SYSTEM_ENABLED: cls.table.add(item.Bow, prob=config.WEAPON_DROP_PROB) if config.PROFESSION_SYSTEM_ENABLED: self.capacity = config.PROFESSION_CRYSTAL_CAPACITY self.respawn = config.PROFESSION_CRYSTAL_RESPAWN class Weeds(Material): tex = 'weeds' index = 12 class Herb(Material): tex = 'herb' index = 13 deplete = Weeds tool = item.Gloves table = droptable.Standard() table.add(item.Potion) def __init__(self, config): if config.PROFESSION_SYSTEM_ENABLED: self.capacity = config.PROFESSION_HERB_CAPACITY self.respawn = config.PROFESSION_HERB_RESPAWN class Ocean(Material): tex = 'ocean' index = 14 class Fish(Material): tex = 'fish' index = 15 deplete = Ocean tool = item.Rod table = droptable.Standard() table.add(item.Ration) def __init__(self, config): if config.PROFESSION_SYSTEM_ENABLED: self.capacity = config.PROFESSION_FISH_CAPACITY self.respawn = config.PROFESSION_FISH_RESPAWN # TODO: Fix lint errors # pylint: disable=all class Meta(type): def __init__(self, name, bases, dict): self.indices = {mtl.index for mtl in self.materials} def __iter__(self): yield from self.materials def __contains__(self, mtl): if isinstance(mtl, Material): mtl = type(mtl) if isinstance(mtl, type): return mtl in self.materials return mtl in self.indices class All(metaclass=Meta): '''List of all materials''' materials = { Void, Water, Grass, Scrub, Foilage, Stone, Slag, Ore, Stump, Tree, Fragment, Crystal, Weeds, Herb, Ocean, Fish} class Impassible(metaclass=Meta): '''Materials that agents cannot walk through''' materials = {Void, Water, Stone, Ocean, Fish} class Habitable(metaclass=Meta): '''Materials that agents cannot walk on''' materials = {Grass, Scrub, Foilage, Ore, Slag, Tree, Stump, Crystal, Fragment, Herb, Weeds} class Harvestable(metaclass=Meta): '''Materials that agents can harvest''' materials = {Water, Foilage, Ore, Tree, Crystal, Herb, Fish} ================================================ FILE: nmmo/lib/seeding.py ================================================ # copied from https://github.com/openai/gym/blob/master/gym/utils/seeding.py """Set of random number generator functions: seeding, generator, hashing seeds.""" from typing import Any, Optional, Tuple import numpy as np class RandomNumberGenerator(np.random.Generator): def __init__(self, bit_generator): super().__init__(bit_generator) self._dir_seq_len = 1024 self._wrap = self._dir_seq_len - 1 self._dir_seq = list(self.integers(0, 4, size=self._dir_seq_len)) self._dir_idx = 0 # provide a random direction from the pre-generated sequence def get_direction(self): self._dir_idx = (self._dir_idx + 1) & self._wrap return self._dir_seq[self._dir_idx] def np_random(seed: Optional[int] = None) -> Tuple[np.random.Generator, Any]: """Generates a random number generator from the seed and returns the Generator and seed. Args: seed: The seed used to create the generator Returns: The generator and resulting seed Raises: Error: Seed must be a non-negative integer or omitted """ if seed is not None and not (isinstance(seed, int) and 0 <= seed): raise ValueError(f"Seed must be a non-negative integer or omitted, not {seed}") seed_seq = np.random.SeedSequence(seed) np_seed = seed_seq.entropy rng = RandomNumberGenerator(np.random.PCG64(seed_seq)) return rng, np_seed ================================================ FILE: nmmo/lib/spawn.py ================================================ from itertools import chain class SequentialLoader: '''config.PLAYER_LOADER that spreads out agent populations''' def __init__(self, config, np_random, candidate_spawn_pos=None): items = config.PLAYERS self.items = items self.idx = -1 if candidate_spawn_pos: self.candidate_spawn_pos = candidate_spawn_pos else: # np_random is the env-level rng self.candidate_spawn_pos = get_edge_tiles(config, np_random, shuffle=True) def __iter__(self): return self def __next__(self): self.idx = (self.idx + 1) % len(self.items) return self.items[self.idx] # pylint: disable=unused-argument def get_spawn_position(self, agent_id): # the basic SequentialLoader just provides a random spawn position return self.candidate_spawn_pos.pop() def get_random_coord(config, np_random, edge=True): '''Generates spawn positions for new agents Randomly selects spawn positions around the borders of the square game map Returns: tuple(int, int): position: The position (row, col) to spawn the given agent ''' mmax = config.MAP_CENTER + config.MAP_BORDER mmin = config.MAP_BORDER # np_random is the env-level RNG, a drop-in replacement of numpy.random if edge: var = np_random.integers(mmin, mmax) fixed = np_random.choice([mmin, mmax]) r, c = int(var), int(fixed) if np_random.random() > 0.5: r, c = c, r else: r, c = np_random.integers(mmin, mmax, 2).tolist() return (r, c) def get_edge_tiles(config, np_random=None, shuffle=False): '''Returns a list of all edge tiles. To shuffle the tile, provide a np_random object ''' # Accounts for void borders in coord calcs left = config.MAP_BORDER right = config.MAP_CENTER + config.MAP_BORDER lows = config.MAP_CENTER * [left] highs = config.MAP_CENTER * [right] inc = list(range(config.MAP_BORDER, config.MAP_CENTER+config.MAP_BORDER)) # All edge tiles in order sides = [] sides.append(list(zip(lows, inc))) sides.append(list(zip(inc, highs))) sides.append(list(zip(highs, inc[::-1]))) sides.append(list(zip(inc[::-1], lows))) tiles = list(chain(*sides)) if shuffle and np_random: np_random.shuffle(tiles) return tiles ================================================ FILE: nmmo/lib/team_helper.py ================================================ from typing import Any, Dict, List import numpy.random from nmmo.lib import spawn def make_teams(config, num_teams): num_per_team = config.PLAYER_N // num_teams teams = {} for team_id in range(num_teams): range_max = (team_id+1)*num_per_team+1 if team_id < num_teams-1 else config.PLAYER_N+1 teams[team_id] = list(range(team_id*num_per_team+1, range_max)) return teams class TeamHelper: def __init__(self, teams: Dict[Any, List[int]], np_random=None): self.teams = teams self.num_teams = len(teams) self.team_list = list(teams.keys()) self.team_size = {} self.team_and_position_for_agent = {} self.agent_for_team_and_position = {} for team_id, team in teams.items(): self.team_size[team_id] = len(team) for position, agent_id in enumerate(team): self.team_and_position_for_agent[agent_id] = (team_id, position) self.agent_for_team_and_position[team_id, position] = agent_id # Left/right team order is determined by team_list, so shuffling it # TODO: check if this is correct np_random = np_random or numpy.random # np_random.shuffle(self.team_list) def agent_position(self, agent_id: int) -> int: return self.team_and_position_for_agent[agent_id][1] def agent_id(self, team_id: Any, position: int) -> int: return self.agent_for_team_and_position[team_id, position] def is_agent_in_team(self, agent_id:int , team_id: Any) -> bool: return agent_id in self.teams[team_id] def get_team_idx(self, agent_id:int) -> int: team_id, _ = self.team_and_position_for_agent[agent_id] return self.team_list.index(team_id) def get_target_agent(self, team_id: Any, target: str): idx = self.team_list.index(team_id) if target == "left_team": target_id = self.team_list[(idx+1) % self.num_teams] return self.teams[target_id] if target == "left_team_leader": target_id = self.team_list[(idx+1) % self.num_teams] return self.teams[target_id][0] if target == "right_team": target_id = self.team_list[(idx-1) % self.num_teams] return self.teams[target_id] if target == "right_team_leader": target_id = self.team_list[(idx-1) % self.num_teams] return self.teams[target_id][0] if target == "my_team_leader": return self.teams[team_id][0] if target == "all_foes": all_foes = [] for foe_team_id in self.team_list: if foe_team_id != team_id: all_foes += self.teams[foe_team_id] return all_foes if target == "all_foe_leaders": leaders = [] for foe_team_id in self.team_list: if foe_team_id != team_id: leaders.append(self.teams[foe_team_id][0]) return leaders return None class RefillPopper: def __init__(self, original_list, np_random=None): assert isinstance(original_list, list), "original_list must be a list of (row, col) tuples" self._original_list = original_list self._np_random = np_random or numpy.random self._refill_list = list(original_list) # copy def pop(self): if len(self._original_list) == 1: return self._original_list[0] if not self._refill_list: self._refill_list = list(self._original_list) pop_idx = self._np_random.integers(len(self._refill_list)) return self._refill_list.pop(pop_idx) class TeamLoader(spawn.SequentialLoader): def __init__(self, config, np_random, candidate_spawn_pos: List[List] = None): assert config.TEAMS is not None, "config.TEAMS must be specified" self.team_helper = TeamHelper(config.TEAMS, np_random) # Check if the team specification is valid for spawning assert len(self.team_helper.team_and_position_for_agent.keys()) == config.PLAYER_N,\ "Number of agents in config.TEAMS must be equal to config.PLAYER_N" for agent_id in range(1, config.PLAYER_N + 1): assert agent_id in self.team_helper.team_and_position_for_agent,\ f"Agent id {agent_id} is not specified in config.TEAMS" super().__init__(config, np_random) if candidate_spawn_pos is None: candidate_spawn_pos = spawn_team_together(config, self.team_helper.num_teams) elif not isinstance(candidate_spawn_pos[0], list): # candidate_spawn_pos for teams should be List[List] candidate_spawn_pos = [[pos] for pos in candidate_spawn_pos] np_random.shuffle(candidate_spawn_pos) self.candidate_spawn_pos = [RefillPopper(pos_list, np_random) for pos_list in candidate_spawn_pos] def get_spawn_position(self, agent_id): idx = self.team_helper.get_team_idx(agent_id) return self.candidate_spawn_pos[idx].pop() def spawn_team_together(config, num_teams): '''Generates spawn positions for new teams Agents in the same team spawn together in the same tile Evenly spaces teams around the square map borders Returns: list of tuple(int, int): position: The position (row, col) to spawn the given teams ''' teams_per_sides = (num_teams + 3) // 4 # 1-4 -> 1, 5-8 -> 2, etc. tiles = spawn.get_edge_tiles(config) each_side = len(tiles) // 4 assert each_side > 4*teams_per_sides, 'Map too small for teams' sides = [tiles[i*each_side:(i+1)*each_side] for i in range(4)] team_spawn_positions = [] for side in sides: for i in range(teams_per_sides): idx = int(len(side)*(i+1)/(teams_per_sides + 1)) team_spawn_positions.append([side[idx]]) return team_spawn_positions ================================================ FILE: nmmo/lib/utils.py ================================================ # pylint: disable=all import inspect from collections import deque import hashlib import numpy as np from nmmo.entity.entity import Entity, EntityState EntityAttr = EntityState.State.attr_name_to_col class staticproperty(property): def __get__(self, cls, owner): return self.fget.__get__(None, owner)() class classproperty(object): def __init__(self, f): self.f = f def __get__(self, obj, owner): return self.f(owner) class Iterable(type): def __iter__(cls): queue = deque(cls.__dict__.items()) while len(queue) > 0: name, attr = queue.popleft() if type(name) != tuple: name = tuple([name]) if not inspect.isclass(attr): continue yield name, attr def values(cls): return [e[1] for e in cls] class StaticIterable(type): def __iter__(cls): stack = list(cls.__dict__.items()) stack.reverse() for name, attr in stack: if name == '__module__': continue if name.startswith('__'): break yield name, attr class NameComparable(type): def __hash__(self): return hash(self.__name__) def __eq__(self, other): return self.__name__ == other.__name__ def __ne__(self, other): return self.__name__ != other.__name__ def __lt__(self, other): return self.__name__ < other.__name__ def __le__(self, other): return self.__name__ <= other.__name__ def __gt__(self, other): return self.__name__ > other.__name__ def __ge__(self, other): return self.__name__ >= other.__name__ class IterableNameComparable(Iterable, NameComparable): pass def linf(pos1, pos2): # pos could be a single (r,c) or a vector of (r,c)s diff = np.abs(np.array(pos1) - np.array(pos2)) return np.max(diff, axis=-1) def linf_single(pos1, pos2): # pos is a single (r,c) to avoid uneccessary function calls return max(abs(pos1[0]-pos2[0]), abs(pos1[1]-pos2[1])) #Bounds checker def in_bounds(r, c, shape, border=0): R, C = shape return ( r > border and c > border and r < R - border and c < C - border ) def l1_map(size): # l1 distance from the center tile (size//2, size//2) x = np.abs(np.arange(size) - size//2) X, Y = np.meshgrid(x, x) data = np.stack((X, Y), -1) return np.max(abs(data), -1) def get_hash_embedding(func, embed_dim): # NOTE: This is a hacky way to get a hash embedding for a function # TODO: Can we get more meaningful embedding? coding LLMs are good but huge func_src = inspect.getsource(func) hash_object = hashlib.sha256(func_src.encode()) hex_digest = hash_object.hexdigest() # Convert the hexadecimal hash to a numpy array with float16 data type hash_bytes = bytes.fromhex(hex_digest) hash_array = np.frombuffer(hash_bytes, dtype=np.float16) hash_array = np.nan_to_num(hash_array, nan=1, posinf=1, neginf=1) hash_array = np.log(abs(hash_array.astype(float))) hash_array -= hash_array.mean() hash_array /= hash_array.std() embedding = np.zeros(embed_dim, dtype=np.float16) embedding[:len(hash_array)] = hash_array return embedding def identify_closest_target(entity): realm = entity.realm radius = realm.config.PLAYER_VISION_RADIUS visible_entities = Entity.Query.window( realm.datastore, entity.pos[0], entity.pos[1], radius) dist = linf(visible_entities[:,EntityAttr["row"]:EntityAttr["col"]+1], entity.pos) entity_ids = visible_entities[:,EntityAttr["id"]] # Filter out the entities that are not attackable flt_idx = visible_entities[:,EntityAttr["npc_type"]] >= 0 # no immortal (-1) if entity.config.NPC_SYSTEM_ENABLED and not entity.config.NPC_ALLOW_ATTACK_OTHER_NPCS: flt_idx &= entity_ids > 0 dist = dist[flt_idx] entity_ids = entity_ids[flt_idx] # TODO: this could be made smarter/faster, or perhaps consider health if len(dist) > 1: closest_idx = np.argmin(dist) return realm.entity(entity_ids[closest_idx]) if len(dist) == 1: return realm.entity(entity_ids[0]) return None ================================================ FILE: nmmo/lib/vec_noise.py ================================================ import numpy as np # The noise2() was ported from https://github.com/zbenjamin/vec_noise # https://github.com/zbenjamin/vec_noise/blob/master/_noise.h#L13 GRAD3 = np.array([ [1,1,0], [-1,1,0], [1,-1,0], [-1,-1,0], [1,0,1], [-1,0,1], [1,0,-1], [-1,0,-1], [0,1,1], [0,-1,1], [0,1,-1], [0,-1,-1], [1,0,-1], [-1,0,-1], [0,-1,1], [0,1,1] ]) # https://github.com/zbenjamin/vec_noise/blob/master/_noise.h#L31 PERM = np.array([ 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 ], dtype=np.int32) PERM = np.concatenate((PERM, PERM)) # 2D simplex skew factors F2 = 0.5 * (np.sqrt(3.0) - 1.0) G2 = (3.0 - np.sqrt(3.0)) / 6.0 # https://github.com/zbenjamin/vec_noise/blob/master/_simplex.c#L46 def snoise2(x, y): """Generate 2D simplex noise for given coordinates.""" s = (x + y) * F2 i = np.floor(x + s).astype(int) j = np.floor(y + s).astype(int) t = (i + j) * G2 x0 = x - (i - t) y0 = y - (j - t) # Determine which simplex we're in i1 = (x0 > y0).astype(int) j1 = 1 - i1 x1 = x0 - i1 + G2 y1 = y0 - j1 + G2 x2 = x0 - 1 + 2 * G2 y2 = y0 - 1 + 2 * G2 # Hash coordinates of the three simplex corners ii = i & 255 jj = j & 255 gi0 = PERM[ii + PERM[jj]] % 12 gi1 = PERM[ii + i1 + PERM[jj + j1]] % 12 gi2 = PERM[ii + 1 + PERM[jj + 1]] % 12 # Calculate contribution from three corners t0 = 0.5 - x0**2 - y0**2 t1 = 0.5 - x1**2 - y1**2 t2 = 0.5 - x2**2 - y2**2 mask0 = (t0 >= 0).astype(float) mask1 = (t1 >= 0).astype(float) mask2 = (t2 >= 0).astype(float) n0 = mask0 * t0**4 * (GRAD3[gi0, 0] * x0 + GRAD3[gi0, 1] * y0) n1 = mask1 * t1**4 * (GRAD3[gi1, 0] * x1 + GRAD3[gi1, 1] * y1) n2 = mask2 * t2**4 * (GRAD3[gi2, 0] * x2 + GRAD3[gi2, 1] * y2) # Sum up and scale the result return 70 * (n0 + n1 + n2) ================================================ FILE: nmmo/minigames/__init__.py ================================================ from .center_race import RacetoCenter, ProgressTowardCenter from .king_hill import KingoftheHill from .sandwich import Sandwich from .comm_together import CommTogether from .radio_raid import RadioRaid AVAILABLE_GAMES = [RacetoCenter, KingoftheHill, Sandwich, CommTogether, RadioRaid] ================================================ FILE: nmmo/minigames/center_race.py ================================================ # pylint: disable=invalid-name, duplicate-code, unused-argument import time from nmmo.core.game_api import Game from nmmo.task import task_api from nmmo.task.base_predicates import ProgressTowardCenter from nmmo.lib import utils class RacetoCenter(Game): required_systems = ["TERRAIN", "RESOURCE"] def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self._map_size = 40 # determines the difficulty self.adaptive_difficulty = True self.num_game_won = 1 # at the same map size, threshold to increase the difficulty self.step_size = 8 self.num_player_resurrect = 0 # NOTE: This is a hacky way to get a hash embedding for a function # TODO: Can we get more meaningful embedding? coding LLMs are good but huge self.task_embedding = utils.get_hash_embedding(ProgressTowardCenter, self.config.TASK_EMBED_DIM) @property def map_size(self): return self._map_size def set_map_size(self, map_size): self._map_size = map_size def is_compatible(self): return self.config.are_systems_enabled(self.required_systems) def reset(self, np_random, map_dict, tasks=None): assert self.map_size >= self.config.PLAYER_N//4,\ f"self.map_size({self.map_size}) must be >= {self.config.PLAYER_N//4}" map_dict["mark_center"] = True # mark the center tile super().reset(np_random, map_dict) self.history[-1]["map_size"] = self.map_size self.num_player_resurrect = 0 def _set_config(self): self.config.reset() self.config.toggle_systems(self.required_systems) self.config.set_for_episode("ALLOW_MOVE_INTO_OCCUPIED_TILE", False) # Regenerate the map from fractal to have less obstacles self.config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) self.config.set_for_episode("TERRAIN_WATER", 0.05) self.config.set_for_episode("TERRAIN_FOILAGE", 0.95) # prop of stone tiles: 0.05 self.config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", True) # Activate death fog self.config.set_for_episode("DEATH_FOG_ONSET", None) # 32 # self.config.set_for_episode("DEATH_FOG_SPEED", 1/6) # # Only the center tile is safe # self.config.set_for_episode("DEATH_FOG_FINAL_SIZE", 0) self._determine_difficulty() # sets the map_size self.config.set_for_episode("MAP_CENTER", self.map_size) def _determine_difficulty(self): # Determine the difficulty (the map size) based on the previous results if self.adaptive_difficulty and self.history \ and self.history[-1]["result"]: # the last game was won last_results = [r["result"] for r in self.history if r["map_size"] == self.map_size] if sum(last_results) >= self.num_game_won \ and self.map_size <= self.config.original["MAP_CENTER"] - self.step_size: self._map_size += self.step_size def _set_realm(self, map_dict): # NOTE: this game respawns dead players at the edge, so setting delete_dead_entity=False self.realm.reset(self._np_random, map_dict, delete_dead_player=False) def _define_tasks(self): return task_api.make_same_task(ProgressTowardCenter, self.config.POSSIBLE_AGENTS, task_kwargs={"embedding": self.task_embedding}) def _process_dead_players(self, terminated, dead_players): # Respawn dead players at the edge for player in dead_players.values(): player.resurrect(freeze_duration=10, health_prop=1, edge_spawn=True) self.num_player_resurrect += 1 @property def winning_score(self): if self._winners: time_limit = self.config.HORIZON return (time_limit - self.realm.tick) / time_limit # speed bonus # No one reached the center return 0.0 def _check_winners(self, terminated): return self._who_completed_task() @staticmethod def test(env, horizon=30, seed=0): game = RacetoCenter(env) env.reset(game=game, seed=seed) # Check configs config = env.config assert config.are_systems_enabled(game.required_systems) assert config.COMBAT_SYSTEM_ENABLED is False assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False start_time = time.time() for _ in range(horizon): _, r, terminated, _, _ = env.step({}) print(f"Time taken: {time.time() - start_time:.3f} s") # pylint: disable=bad-builtin # Test if the difficulty increases org_map_size = game.map_size for result in [False]*7 + [True]*game.num_game_won: game.history.append({"result": result, "map_size": game.map_size}) game._determine_difficulty() # pylint: disable=protected-access assert game.map_size == (org_map_size + game.step_size) # Check if returns of resurrect/frozen players are correct for agent_id, player in env._dead_this_tick.items(): # pylint: disable=protected-access assert player.alive, "Resurrected players should be alive" assert player.status.frozen, "Resurrected players should be frozen" assert player.my_task.progress == 0, "Resurrected players should have 0 progress" assert terminated[agent_id], "Resurrected players should be done = True" assert r[agent_id] == -1, "Resurrected players should have -1 reward" if __name__ == "__main__": import nmmo test_config = nmmo.config.Default() # Medium, AllGameSystems test_env = nmmo.Env(test_config) RacetoCenter.test(test_env) # 0.85 s # performance test from tests.testhelpers import profile_env_step test_tasks = task_api.make_same_task(ProgressTowardCenter, test_env.possible_agents) profile_env_step(tasks=test_tasks) # env._compute_rewards(): 1.9577480710031523 ================================================ FILE: nmmo/minigames/comm_together.py ================================================ # pylint: disable=duplicate-code, invalid-name, unused-argument import time from nmmo.core.game_api import TeamBattle from nmmo.task import task_spec from nmmo.task.base_predicates import AllMembersWithinRange from nmmo.lib import utils, team_helper def seek_task(within_dist): return task_spec.TaskSpec( eval_fn=AllMembersWithinRange, eval_fn_kwargs={"dist": within_dist}, reward_to="team") class CommTogether(TeamBattle): _required_systems = ["TERRAIN", "COMMUNICATION", "COMBAT"] def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) # NOTE: all should fit in a 8x8 square, in which all can see each other self.team_within_dist = 7 # gather all team members within this distance self._map_size = 128 # determines the difficulty self._spawn_immunity = 128 # so that agents can attack each other later self.adaptive_difficulty = False self.num_game_won = 1 # at the same map size, threshold to increase the difficulty self.step_size = 8 self._grass_map = False self.num_player_resurrect = 0 # NOTE: This is a hacky way to get a hash embedding for a function # TODO: Can we get more meaningful embedding? coding LLMs are good but heavy self.task_embedding = utils.get_hash_embedding(seek_task, self.config.TASK_EMBED_DIM) @property def required_systems(self): return self._required_systems @property def map_size(self): return self._map_size def set_map_size(self, map_size): self._map_size = map_size def set_spawn_immunity(self, spawn_immunity): self._spawn_immunity = spawn_immunity def set_grass_map(self, grass_map): self._grass_map = grass_map def is_compatible(self): return self.config.are_systems_enabled(self.required_systems) def reset(self, np_random, map_dict, tasks=None): super().reset(np_random, map_dict) self.history[-1]["map_size"] = self.map_size self._grass_map = False # reset to default self.num_player_resurrect = 0 def _set_config(self): self.config.reset() self.config.toggle_systems(self.required_systems) self.config.set_for_episode("ALLOW_MOVE_INTO_OCCUPIED_TILE", False) # Regenerate the map from fractal to have less obstacles self.config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) self.config.set_for_episode("TERRAIN_WATER", 0.1) self.config.set_for_episode("TERRAIN_FOILAGE", 0.9) self.config.set_for_episode("TERRAIN_RESET_TO_GRASS", self._grass_map) # NO death fog self.config.set_for_episode("DEATH_FOG_ONSET", None) # Enable +10 hp per tick, so that getting hit once doesn't damage the agent self.config.set_for_episode("PLAYER_HEALTH_INCREMENT", 10) self._determine_difficulty() # sets the map size self.config.set_for_episode("MAP_CENTER", self.map_size) self.config.set_for_episode("COMBAT_SPAWN_IMMUNITY", self._spawn_immunity) def _determine_difficulty(self): # Determine the difficulty (the map size) based on the previous results if self.adaptive_difficulty and self.history \ and self.history[-1]["result"]: # the last game was won last_results = [r["result"] for r in self.history if r["map_size"] == self.map_size] if sum(last_results) >= self.num_game_won: self._map_size = min(self.map_size + self.step_size, self.config.original["MAP_CENTER"]) # # Decrease the spawn immunity, to increase attack window # if self._spawn_immunity > self.history[-1]["winning_tick"]: # next_immunity = (self._spawn_immunity + self.history[-1]["winning_tick"]) / 2 # self._spawn_immunity = max(next_immunity, 64) # 64 is the minimum def _set_realm(self, map_dict): # NOTE: this game respawns dead players at the edge, so setting delete_dead_entity=False self.realm.reset(self._np_random, map_dict, delete_dead_player=False) def _define_tasks(self): spec_list = [seek_task(self.team_within_dist)] * len(self.teams) return task_spec.make_task_from_spec(self.teams, spec_list) def _process_dead_players(self, terminated, dead_players): # Respawn dead players at a random location for player in dead_players.values(): player.resurrect(freeze_duration=30, health_prop=1, edge_spawn=False) self.num_player_resurrect += 1 def _check_winners(self, terminated): # No winner game is possible return self._who_completed_task() @property def winning_score(self): if self._winners: time_limit = self.config.HORIZON speed_bonus = (time_limit - self.realm.tick) / time_limit return speed_bonus return 0.0 @staticmethod def test(env, horizon=30, seed=0): # pylint: disable=protected-access game = CommTogether(env) env.reset(game=game, seed=seed) # Check configs config = env.config assert config.are_systems_enabled(game.required_systems) assert config.DEATH_FOG_ONSET is None assert config.ITEM_SYSTEM_ENABLED is False assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False start_time = time.time() for _ in range(horizon): env.step({}) print(f"Time taken: {time.time() - start_time:.3f} s") # pylint: disable=bad-builtin # These should run without errors game.history.append({"result": False, "map_size": 0, "winning_tick": 512}) game._determine_difficulty() game.history.append({"result": True, "winners": None, "map_size": 0, "winning_tick": 512}) game._determine_difficulty() # Test if the difficulty changes org_map_size = game.map_size for result in [False]*7 + [True]*game.num_game_won: game.history.append({"result": result, "map_size": game.map_size, "winning_tick": 128}) game._determine_difficulty() if game.adaptive_difficulty: assert game.map_size == (org_map_size + game.step_size) if __name__ == "__main__": import nmmo test_config = nmmo.config.Default() # Medium, AllGameSystems teams = team_helper.make_teams(test_config, num_teams=7) test_config.set("TEAMS", teams) test_env = nmmo.Env(test_config) CommTogether.test(test_env) # 0.65 s # performance test from tests.testhelpers import profile_env_step test_tasks = task_spec.make_task_from_spec(teams, [seek_task(5)]*len(teams)) profile_env_step(tasks=test_tasks) # env._compute_rewards(): 0.27938533399719745 ================================================ FILE: nmmo/minigames/king_hill.py ================================================ # pylint: disable=invalid-name, duplicate-code, unused-argument import time from nmmo.core.game_api import TeamBattle from nmmo.task import task_spec, base_predicates from nmmo.lib import utils, team_helper def seize_task(dur_to_win): return task_spec.TaskSpec( eval_fn=base_predicates.SeizeCenter, eval_fn_kwargs={"num_ticks": dur_to_win}, reward_to="team") class KingoftheHill(TeamBattle): required_systems = ["TERRAIN", "COMBAT", "RESOURCE", "COMMUNICATION"] def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self._seize_duration = 10 # determines the difficulty self.dur_step_size = 10 self.max_seize_duration = 200 self.adaptive_difficulty = True self.num_game_won = 2 # at the same duration, threshold to increase the difficulty self.map_size = 40 self.score_scaler = .5 # NOTE: This is a hacky way to get a hash embedding for a function # TODO: Can we get more meaningful embedding? coding LLMs are good but huge self.task_embedding = utils.get_hash_embedding(seize_task, self.config.TASK_EMBED_DIM) @property def seize_duration(self): return self._seize_duration def set_seize_duration(self, seize_duration): self._seize_duration = seize_duration def is_compatible(self): return self.config.are_systems_enabled(self.required_systems) def reset(self, np_random, map_dict, tasks=None): super().reset(np_random, map_dict) self.history[-1]["map_size"] = self.map_size self.history[-1]["seize_duration"] = self.seize_duration def _set_config(self): self.config.reset() self.config.toggle_systems(self.required_systems) self.config.set_for_episode("MAP_CENTER", self.map_size) self.config.set_for_episode("ALLOW_MOVE_INTO_OCCUPIED_TILE", False) # Regenerate the map from fractal to have less obstacles self.config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) self.config.set_for_episode("TERRAIN_WATER", 0.05) self.config.set_for_episode("TERRAIN_FOILAGE", 0.95) # prop of stone tiles: 0.05 self.config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", True) # Activate death fog self.config.set_for_episode("DEATH_FOG_ONSET", 32) self.config.set_for_episode("DEATH_FOG_SPEED", 1/16) self.config.set_for_episode("DEATH_FOG_FINAL_SIZE", 5) self._determine_difficulty() # sets the seize duration def _determine_difficulty(self): # Determine the difficulty (the seize duration) based on the previous results if self.adaptive_difficulty and self.history \ and self.history[-1]["result"]: # the last game was won last_results = [r["result"] for r in self.history if r["seize_duration"] == self.seize_duration] if sum(last_results) >= self.num_game_won: self._seize_duration = min(self.seize_duration + self.dur_step_size, self.max_seize_duration) def _set_realm(self, map_dict): self.realm.reset(self._np_random, map_dict, custom_spawn=True, seize_targets=["center"]) # team spawn requires custom spawning team_loader = team_helper.TeamLoader(self.config, self._np_random) self.realm.players.spawn(team_loader) def _define_tasks(self): spec_list = [seize_task(self.seize_duration)] * len(self.teams) return task_spec.make_task_from_spec(self.teams, spec_list) @property def winning_score(self): if self._winners: time_limit = self.config.HORIZON speed_bonus = (time_limit - self.realm.tick) / time_limit alive_bonus = sum(1.0 for agent_id in self._winners if agent_id in self.realm.players)\ / len(self._winners) return (speed_bonus + alive_bonus) / 2 # set max to 1.0 # No one succeeded return 0.0 def _check_winners(self, terminated): assert self.config.TEAMS is not None, "Team battle mode requires TEAMS to be defined" winners = self._who_completed_task() if winners is not None: return winners if len(self.realm.seize_status) == 0: return None seize_results = list(self.realm.seize_status.values()) # Time's up, and a team has seized the center if self.realm.tick == self.config.HORIZON: winners = [] # Declare the latest seizing agent as the winner for agent_id, _ in seize_results: for task in self.tasks: if agent_id in task.assignee: winners += task.assignee return winners or None # Only one team remains and they have seized the center current_teams = self._check_remaining_teams() if len(current_teams) == 1: winning_team = list(current_teams.keys())[0] team_members = self.config.TEAMS[winning_team] for agent_id, _ in seize_results: # Check if the agent is in the winning team if agent_id in team_members: return team_members # No team has seized the center return None @staticmethod def test(env, horizon=30, seed=0): game = KingoftheHill(env) env.reset(game=game, seed=seed) # Check configs config = env.config assert config.are_systems_enabled(game.required_systems) assert config.TERRAIN_SYSTEM_ENABLED is True assert config.RESOURCE_SYSTEM_ENABLED is True assert config.COMBAT_SYSTEM_ENABLED is True assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False assert config.DEATH_FOG_ONSET == 32 assert env.realm.map.seize_targets == [(config.MAP_SIZE//2, config.MAP_SIZE//2)] start_time = time.time() for _ in range(horizon): env.step({}) print(f"Time taken: {time.time() - start_time:.3f} s") # pylint: disable=bad-builtin # Test if the difficulty increases org_seize_dur = game.seize_duration for result in [False]*7 + [True]*game.num_game_won: game.history.append({"result": result, "seize_duration": game.seize_duration}) game._determine_difficulty() # pylint: disable=protected-access assert game.seize_duration == (org_seize_dur + game.dur_step_size) if __name__ == "__main__": import nmmo test_config = nmmo.config.Default() # Medium, AllGameSystems test_config.set("TEAMS", team_helper.make_teams(test_config, num_teams=7)) test_env = nmmo.Env(test_config) KingoftheHill.test(test_env) # 0.59 s # performance test from tests.testhelpers import profile_env_step teams = test_config.TEAMS test_tasks = task_spec.make_task_from_spec(teams, [seize_task(30)]*len(teams)) profile_env_step(tasks=test_tasks) # env._compute_rewards(): 0.24291237899888074 ================================================ FILE: nmmo/minigames/radio_raid.py ================================================ # pylint: disable=duplicate-code, invalid-name, unused-argument import time from nmmo.core.game_api import TeamBattle from nmmo.task import task_spec from nmmo.task.base_predicates import DefeatEntity from nmmo.lib import utils, team_helper def hunt_task(num_npc): return task_spec.TaskSpec( eval_fn=DefeatEntity, eval_fn_kwargs={"agent_type": "npc", "level": 0, "num_agent": num_npc}, reward_to="team") class RadioRaid(TeamBattle): required_systems = ["TERRAIN", "COMBAT", "COMMUNICATION", "NPC"] num_teams = 8 def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self._goal_num_npc = 5 # determines the difficulty self.adaptive_difficulty = True self.num_game_won = 2 # at the same map size, threshold to increase the difficulty self.step_size = 5 self.quad_centers = None self._grass_map = False # npc danger: 0=all npc are passive, 1=all npc are aggressive self._npc_danger = 0 # increase by .1 per wave self._danger_step_size = .1 self._spawn_center_crit = 0.4 # if danger is less than crit, spawn at center self.npc_wave_num = 10 # number of npc to spawn per wave self._last_wave_tick = 0 self.npc_spawn_crit = 3 self.npc_spawn_radius = 5 self.max_wave_interval = 20 # These will probably affect the difficulty self.map_size = 48 self.spawn_immunity = self.config.HORIZON # NOTE: This is a hacky way to get a hash embedding for a function # TODO: Can we get more meaningful embedding? coding LLMs are good but heavy self.task_embedding = utils.get_hash_embedding(hunt_task, self.config.TASK_EMBED_DIM) @property def teams(self): team_size = self.config.PLAYER_N // self.num_teams teams = {i: list(range((i-1)*team_size+1, i*team_size+1)) for i in range(1, self.num_teams)} teams[self.num_teams] = \ list(range((self.num_teams-1)*team_size+1, self.config.PLAYER_N+1)) return teams @property def goal_num_npc(self): return self._goal_num_npc def set_goal_num_npc(self, goal_num_npc): self._goal_num_npc = goal_num_npc def set_grass_map(self, grass_map): self._grass_map = grass_map def is_compatible(self): return self.config.are_systems_enabled(self.required_systems) def reset(self, np_random, map_dict, tasks=None): super().reset(np_random, map_dict) self.history[-1]["goal_num_npc"] = self.goal_num_npc self._npc_danger = 0 self._last_wave_tick = 0 def _set_config(self): self.config.reset() self.config.toggle_systems(self.required_systems) self.config.set_for_episode("MAP_CENTER", self.map_size) self.config.set_for_episode("COMBAT_SPAWN_IMMUNITY", self.spawn_immunity) self.config.set_for_episode("ALLOW_MOVE_INTO_OCCUPIED_TILE", False) self.config.set_for_episode("TEAMS", self.teams) self.config.set_for_episode("NPC_DEFAULT_REFILL_DEAD_NPCS", False) # Regenerate the map from fractal to have less obstacles self.config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) self.config.set_for_episode("TERRAIN_WATER", 0.1) self.config.set_for_episode("TERRAIN_FOILAGE", 0.95) self.config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", False) self.config.set_for_episode("TERRAIN_RESET_TO_GRASS", self._grass_map) # NO death fog self.config.set_for_episode("DEATH_FOG_ONSET", None) # Enable +1 hp per tick -- restore health by eat/drink self.config.set_for_episode("PLAYER_HEALTH_INCREMENT", 1) # Make NPCs more aggressive self.config.set_for_episode("NPC_SPAWN_NEUTRAL", 0.3) self.config.set_for_episode("NPC_SPAWN_AGGRESSIVE", 0.8) self._determine_difficulty() # sets the goal_num_npc def _determine_difficulty(self): # Determine the difficulty (the map size) based on the previous results if self.adaptive_difficulty and self.history \ and self.history[-1]["result"]: # the last game was won last_results = [r["result"] for r in self.history if r["goal_num_npc"] == self.goal_num_npc] if sum(last_results) >= self.num_game_won: self._goal_num_npc = self._goal_num_npc + self.step_size def _set_realm(self, map_dict): self.realm.reset(self._np_random, map_dict, custom_spawn=True) # team spawn requires custom spawning team_loader = team_helper.TeamLoader(self.config, self._np_random) self.realm.players.spawn(team_loader) # from each team, pick 4 agents and place on each quad center as recons self.quad_centers = list(self.realm.map.quad_centers.values()) for members in self.teams.values(): recons = self._np_random.choice(members, size=4, replace=False) for idx, agent_id in enumerate(recons): self.realm.players[agent_id].make_recon(new_pos=self.quad_centers[idx]) def _define_tasks(self): spec_list = [hunt_task(self.goal_num_npc)] * len(self.teams) return task_spec.make_task_from_spec(self.teams, spec_list) def _process_dead_npcs(self, dead_npcs): npc_manager = self.realm.npcs diff_player_npc = (self.realm.num_players - self.num_teams*4) - len(npc_manager) # Spawn more NPCs if there are more players than NPCs # If the gap is large, spawn in waves # If the gap is small, spawn in small batches if diff_player_npc >= 0 and (len(npc_manager) <= self.npc_spawn_crit or \ self.realm.tick - self._last_wave_tick > self.max_wave_interval): if self._npc_danger < self._spawn_center_crit: spawn_pos = self.realm.map.center_coord else: spawn_pos = self._np_random.choice(self.quad_centers) r_min, r_max = spawn_pos[0] - self.npc_spawn_radius, spawn_pos[0] + self.npc_spawn_radius c_min, c_max = spawn_pos[1] - self.npc_spawn_radius, spawn_pos[1] + self.npc_spawn_radius npc_manager.area_spawn(r_min, r_max, c_min, c_max, self.npc_wave_num, lambda r, c: npc_manager.spawn_npc(r, c, danger=self._npc_danger)) self._npc_danger += min(self._danger_step_size, 1) # max danger = 1 self._last_wave_tick = self.realm.tick def _check_winners(self, terminated): # No winner game is possible return self._who_completed_task() @property def is_over(self): return self.winners is not None or self.realm.tick >= self.config.HORIZON or \ self.realm.num_players <= (self.num_teams*4) # 4 immortal recons per team @property def winning_score(self): if self._winners: time_limit = self.config.HORIZON speed_bonus = (time_limit - self.realm.tick) / time_limit alive_bonus = sum(1.0 for agent_id in self._winners if agent_id in self.realm.players)\ / len(self._winners) return (speed_bonus + alive_bonus) / 2 # set max to 1.0 return 0.0 @staticmethod def test(env, horizon=30, seed=0): game = RadioRaid(env) env.reset(game=game, seed=seed) # Check configs config = env.config assert config.are_systems_enabled(game.required_systems) assert config.COMBAT_SYSTEM_ENABLED is True assert config.RESOURCE_SYSTEM_ENABLED is False assert config.COMMUNICATION_SYSTEM_ENABLED is True assert config.ITEM_SYSTEM_ENABLED is False assert config.DEATH_FOG_ONSET is None assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False assert config.NPC_SYSTEM_ENABLED is True assert config.NPC_DEFAULT_REFILL_DEAD_NPCS is False start_time = time.time() for _ in range(horizon): env.step({}) print(f"Time taken: {time.time() - start_time:.3f} s") # pylint: disable=bad-builtin # pylint: disable=protected-access # These should run without errors game.history.append({"result": False, "goal_num_npc": game.goal_num_npc}) game._determine_difficulty() # Test if the difficulty changes org_goal_npc = game.goal_num_npc for result in [False]*7 + [True]*game.num_game_won: game.history.append({"result": result, "goal_num_npc": game.goal_num_npc}) game._determine_difficulty() # pylint: disable=protected-access assert game.goal_num_npc == (org_goal_npc + game.step_size) if __name__ == "__main__": import nmmo test_config = nmmo.config.Default() # Medium, AllGameSystems test_env = nmmo.Env(test_config) RadioRaid.test(test_env) # 0.60 s # performance test from tests.testhelpers import profile_env_step test_tasks = task_spec.make_task_from_spec(test_config.TEAMS, [hunt_task(30)]*len(test_config.TEAMS)) profile_env_step(tasks=test_tasks) # env._compute_rewards(): 0.17201571099940338 ================================================ FILE: nmmo/minigames/sandwich.py ================================================ import time import numpy as np from nmmo.core.game_api import TeamBattle, team_survival_task from nmmo.task import task_spec from nmmo.lib import team_helper def secure_order(pos, radius=5): return {"secure": {"position": pos, "radius": radius}} class Sandwich(TeamBattle): required_systems = ["TERRAIN", "COMBAT", "NPC", "COMMUNICATION"] num_teams = 8 def __init__(self, env, sampling_weight=None): super().__init__(env, sampling_weight) self.map_size = 80 self._inner_npc_num = 2 # determines the difficulty self._outer_npc_num = None # these npcs rally to the center self.npc_step_size = 2 self.adaptive_difficulty = True self.num_game_won = 2 # at the same duration, threshold to increase the difficulty self.max_npc_num = self.config.PLAYER_N // self.num_teams self.survival_crit = 500 # to win, agents must survive this long self._grass_map = False @property def teams(self): team_size = self.config.PLAYER_N // self.num_teams teams = {i: list(range((i-1)*team_size+1, i*team_size+1)) for i in range(1, self.num_teams)} teams[self.num_teams] = \ list(range((self.num_teams-1)*team_size+1, self.config.PLAYER_N+1)) return teams @property def inner_npc_num(self): return self._inner_npc_num def set_inner_npc_num(self, inner_npc_num): self._inner_npc_num = inner_npc_num @property def outer_npc_num(self): return self._outer_npc_num or min(self._inner_npc_num*self.num_teams, self.map_size*2) def set_outer_npc_num(self, outer_npc_num): self._outer_npc_num = outer_npc_num def set_grass_map(self, grass_map): self._grass_map = grass_map def is_compatible(self): return self.config.are_systems_enabled(self.required_systems) def reset(self, np_random, map_dict, tasks=None): super().reset(np_random, map_dict) self.history[-1]["inner_npc_num"] = self.inner_npc_num self.history[-1]["outer_npc_num"] = self.outer_npc_num self._grass_map = False # reset to default def _set_config(self): self.config.reset() self.config.toggle_systems(self.required_systems) self.config.set_for_episode("TEAMS", self.teams) self.config.set_for_episode("ALLOW_MOVE_INTO_OCCUPIED_TILE", False) self.config.set_for_episode("NPC_DEFAULT_REFILL_DEAD_NPCS", False) # Make the map small self.config.set_for_episode("MAP_CENTER", self.map_size) # Regenerate the map from fractal to have less obstacles self.config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) self.config.set_for_episode("TERRAIN_WATER", 0.1) self.config.set_for_episode("TERRAIN_FOILAGE", 0.9) self.config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", False) self.config.set_for_episode("TERRAIN_RESET_TO_GRASS", self._grass_map) # Activate death fog from the onset self.config.set_for_episode("DEATH_FOG_ONSET", 1) self.config.set_for_episode("DEATH_FOG_SPEED", 1/10) self.config.set_for_episode("DEATH_FOG_FINAL_SIZE", 5) # Enable +1 hp per tick self.config.set_for_episode("PLAYER_HEALTH_INCREMENT", 1) self._determine_difficulty() # sets the seize duration def _determine_difficulty(self): # Determine the difficulty based on the previous results if self.adaptive_difficulty and self.history \ and self.history[-1]["result"]: # the last game was won last_results = [r["result"] for r in self.history if r["inner_npc_num"] == self.inner_npc_num] if sum(last_results) >= self.num_game_won: # Increase the npc num, when there were only few npcs left at the end self._inner_npc_num += self.npc_step_size self._inner_npc_num = min(self._inner_npc_num, self.max_npc_num) def _generate_spawn_locs(self): center = self.config.MAP_SIZE // 2 radius = self.map_size // 4 angles = np.linspace(0, 2*np.pi, self.num_teams, endpoint=False) return [(center + int(radius*np.cos(a)), center + int(radius*np.sin(a))) for a in angles] def _set_realm(self, map_dict): self.realm.reset(self._np_random, map_dict, custom_spawn=True) # team spawn requires custom spawning spawn_locs = self._generate_spawn_locs() team_loader = team_helper.TeamLoader(self.config, self._np_random, spawn_locs) self.realm.players.spawn(team_loader) # spawn NPCs npc_manager = self.realm.npcs center = self.config.MAP_SIZE // 2 offset = self.config.MAP_CENTER // 8 for i in range(self.num_teams): r, c = spawn_locs[i] if r < center: r_min, r_max = center - offset, center - 1 else: r_min, r_max = center + 1, center + offset if c < center: c_min, c_max = center - offset, center - 1 else: c_min, c_max = center + 1, center + offset # pylint: disable=cell-var-from-loop npc_manager.area_spawn(r_min, r_max, c_min, c_max, self.inner_npc_num, lambda r, c: npc_manager.spawn_npc( r, c, name=f"NPC{i+1}", order={"rally": spawn_locs[i]})) npc_manager.edge_spawn(self.outer_npc_num, lambda r, c: npc_manager.spawn_npc( r, c, name="NPC5", order={"rally": (center,center)})) def _process_dead_npcs(self, dead_npcs): npc_manager = self.realm.npcs target_num = min(self.realm.num_players, self.inner_npc_num) // 2 if len(npc_manager) < target_num: center = self.config.MAP_SIZE // 2 offset = self.config.MAP_CENTER // 6 r_min = c_min = center - offset r_max = c_max = center + offset num_spawn = target_num - len(npc_manager) npc_manager.area_spawn(r_min, r_max, c_min, c_max, num_spawn, lambda r, c: npc_manager.spawn_npc( r, c, name="NPC5", order={"rally": (center,center)})) @property def winning_score(self): if self._winners: time_limit = self.config.HORIZON speed_bonus = (time_limit - self.realm.tick) / time_limit return speed_bonus # set max to 1.0 # No one succeeded return 0.0 def _check_winners(self, terminated): # Basic survival criteria if self.realm.tick < self.survival_crit: return None return super()._check_winners(terminated) @staticmethod def test(env, horizon=30, seed=0): game = Sandwich(env) env.reset(game=game, seed=seed) # Check configs config = env.config assert config.are_systems_enabled(game.required_systems) assert config.TERRAIN_SYSTEM_ENABLED is True assert config.RESOURCE_SYSTEM_ENABLED is False assert config.COMBAT_SYSTEM_ENABLED is True assert config.NPC_SYSTEM_ENABLED is True assert config.NPC_DEFAULT_REFILL_DEAD_NPCS is False assert config.EQUIPMENT_SYSTEM_ENABLED is False # equipment is used to set npc stats assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False start_time = time.time() for _ in range(horizon): env.step({}) print(f"Time taken: {time.time() - start_time:.3f} s") # pylint: disable=bad-builtin # Test if the difficulty increases org_inner_npc_num = game.inner_npc_num for result in [False]*7 + [True]*game.num_game_won: game.history.append( {"result": result, "inner_npc_num": game.inner_npc_num}) game._determine_difficulty() # pylint: disable=protected-access assert game.inner_npc_num == (org_inner_npc_num + game.npc_step_size) if __name__ == "__main__": import nmmo test_config = nmmo.config.Default() # Medium, AllGameSystems test_env = nmmo.Env(test_config) Sandwich.test(test_env) # 0.74 s # performance test from tests.testhelpers import profile_env_step test_tasks = task_spec.make_task_from_spec(test_config.TEAMS, [team_survival_task(30)]*len(test_config.TEAMS)) profile_env_step(tasks=test_tasks) # env._compute_rewards(): 0.1768564050034911 ================================================ FILE: nmmo/render/__init__.py ================================================ ================================================ FILE: nmmo/render/overlay.py ================================================ import numpy as np from nmmo.lib.colors import Neon from nmmo.systems import combat from .render_utils import normalize # pylint: disable=unused-argument class OverlayRegistry: def __init__(self, realm, renderer): '''Manager class for overlays Args: config: A Config object realm: An environment ''' self.initialized = False self.realm = realm self.config = realm.config self.renderer = renderer self.overlays = { #'counts': Counts, # TODO: change population to team 'skills': Skills} def init(self, *args): self.initialized = True for cmd, overlay in self.overlays.items(): self.overlays[cmd] = overlay(self.config, self.realm, self.renderer, *args) return self def step(self, cmd): '''Per-tick overlay updates Args: cmd: User command returned by the client ''' if not self.initialized: self.init() for overlay in self.overlays.values(): overlay.update() if cmd in self.overlays: self.overlays[cmd].register() class Overlay: '''Define a overlay for visualization in the client Overlays are color images of the same size as the game map. They are rendered over the environment with transparency and can be used to gain insight about agent behaviors.''' def __init__(self, config, realm, renderer, *args): ''' Args: config: A Config object realm: An environment ''' self.config = config self.realm = realm self.renderer = renderer self.size = config.MAP_SIZE self.values = np.zeros((self.size, self.size)) def update(self): '''Compute per-tick updates to this overlay. Override per overlay. Args: obs: Observation returned by the environment ''' def register(self): '''Compute the overlay and register it within realm. Override per overlay.''' class Skills(Overlay): def __init__(self, config, realm, renderer, *args): '''Indicates whether agents specialize in foraging or combat''' super().__init__(config, realm, renderer) self.num_skill = 2 self.values = np.zeros((self.size, self.size, self.num_skill)) def update(self): '''Computes a count-based exploration map by painting tiles as agents walk over them''' for agent in self.realm.players.values(): r, c = agent.pos skill_lvl = (agent.skills.food.level.val + agent.skills.water.level.val)/2.0 combat_lvl = combat.level(agent.skills) if skill_lvl == 10 and combat_lvl == 3: continue self.values[r, c, 0] = skill_lvl self.values[r, c, 1] = combat_lvl def register(self): values = np.zeros((self.size, self.size, self.num_skill)) for idx in range(self.num_skill): ary = self.values[:, :, idx] vals = ary[ary != 0] mean = np.mean(vals) std = np.std(vals) if std == 0: std = 1 values[:, :, idx] = (ary - mean) / std values[ary == 0] = 0 colors = np.array([Neon.BLUE.rgb, Neon.BLOOD.rgb]) colorized = np.zeros((self.size, self.size, 3)) amax = np.argmax(values, -1) for idx in range(self.num_skill): colorized[amax == idx] = colors[idx] / 255 colorized[values[:, :, idx] == 0] = 0 self.renderer.register(colorized) # CHECK ME: this was based on population, so disabling it for now # We may want this back for the team-level analysis class Counts(Overlay): def __init__(self, config, realm, renderer, *args): super().__init__(config, realm, renderer) self.values = np.zeros((self.size, self.size, config.PLAYER_POLICIES)) def update(self): '''Computes a count-based exploration map by painting tiles as agents walk over them''' for ent_id, agent in self.realm.players.items(): r, c = agent.pos self.values[r, c][ent_id] += 1 def register(self): colors = self.realm.players.palette.colors colors = np.array([colors[pop].rgb for pop in range(self.config.PLAYER_POLICIES)]) colorized = self.values[:, :, :, None] * colors / 255 colorized = np.sum(colorized, -2) count_sum = np.sum(self.values[:, :], -1) data = normalize(count_sum)[..., None] count_sum[count_sum==0] = 1 colorized = colorized * data / count_sum[..., None] self.renderer.register(colorized) ================================================ FILE: nmmo/render/render_client.py ================================================ from __future__ import annotations import numpy as np from nmmo.render.overlay import OverlayRegistry from nmmo.render.render_utils import patch_packet # Render is external to the game # NOTE: WebsocketRenderer has been renamed to DummyRenderer class DummyRenderer: def __init__(self, realm=None) -> None: self._client = None # websocket.Application(realm) self.overlay_pos = [256, 256] self._realm = realm self.overlay = None self.registry = OverlayRegistry(realm, renderer=self) if realm else None self.packet = None def set_realm(self, realm) -> None: self._realm = realm self.registry = OverlayRegistry(realm, renderer=self) if realm else None def render_packet(self, packet) -> None: packet = { 'pos': self.overlay_pos, 'wilderness': 0, # obsolete, but maintained for compatibility **packet } self.overlay_pos, _ = self._client.update(packet) def render_realm(self) -> None: assert self._realm is not None, 'This function requires a realm' assert self._realm.tick is not None, 'render before reset' packet = { 'config': self._realm.config, 'pos': self.overlay_pos, 'wilderness': 0, **self._realm.packet() } # TODO: a hack to make the client work packet = patch_packet(packet, self._realm) if self.overlay is not None: packet['overlay'] = self.overlay self.overlay = None # save the packet for investigation self.packet = packet # pass the packet to renderer pos, cmd = None, None # self._client.update(self.packet) # NOTE: copy pasted from nmmo/render/websocket.py # def update(self, packet): # self.tick += 1 # uptime = np.round(self.tickRate*self.tick, 1) # delta = time.time() - self.time # print('Wall Clock: ', str(delta)[:5], 'Uptime: ', uptime, ', Tick: ', self.tick) # delta = self.tickRate - delta # if delta > 0: # time.sleep(delta) # self.time = time.time() # for client in self.clients: # client.sendUpdate(packet) # if client.pos is not None: # self.pos = client.pos # self.cmd = client.cmd # return self.pos, self.cmd self.overlay_pos = pos self.registry.step(cmd) def register(self, overlay: np.ndarray) -> None: '''Register an overlay to be sent to the client The intended use of this function is: User types overlay -> client sends cmd to server -> server computes overlay update -> register(overlay) -> overlay is sent to client -> overlay rendered Args: overlay: A map-sized (self.size) array of floating point values overlay must be a numpy array of dimension (*(env.size), 3) ''' self.overlay = overlay.tolist() ================================================ FILE: nmmo/render/render_utils.py ================================================ import numpy as np from scipy import signal from nmmo.lib.colors import Neon # NOTE: added to fix json.dumps() cannot serialize numpy objects # pylint: disable=inconsistent-return-statements def np_encoder(obj): if isinstance(obj, np.generic): return obj.item() def normalize(ary: np.ndarray, norm_std=2): R, C = ary.shape preprocessed = np.zeros_like(ary) nonzero = ary[ary!= 0] mean = np.mean(nonzero) std = np.std(nonzero) if std == 0: std = 1 for r in range(R): for c in range(C): val = ary[r, c] if val != 0: val = (val - mean) / (norm_std * std) val = np.clip(val+1, 0, 2)/2 preprocessed[r, c] = val return preprocessed def clip(ary: np.ndarray): R, C = ary.shape preprocessed = np.zeros_like(ary) nonzero = ary[ary!= 0] mmin = np.min(nonzero) mmag = np.max(nonzero) - mmin for r in range(R): for c in range(C): val = ary[r, c] val = (val - mmin) / mmag preprocessed[r, c] = val return preprocessed def make_two_tone(ary, norm_std=2, preprocess='norm', invert=False, periods=1): if preprocess == 'norm': ary = normalize(ary, norm_std) elif preprocess == 'clip': ary = clip(ary) # if preprocess not in ['norm', 'clip'], assume no preprocessing R, C = ary.shape colorized = np.zeros((R, C, 3)) if periods != 1: ary = np.abs(signal.sawtooth(periods*3.14159*ary)) if invert: colorized[:, :, 0] = ary colorized[:, :, 1] = 1-ary else: colorized[:, :, 0] = 1-ary colorized[:, :, 1] = ary colorized *= (ary != 0)[:, :, None] return colorized # TODO: this is a hack to make the client work # by adding color, population, self to the packet # integrating with team helper could make this neat def patch_packet(packet, realm): for ent_id in packet['player']: packet['player'][ent_id]['base']['color'] = Neon.GREEN.packet() # EntityAttr: population was changed to npc_type packet['player'][ent_id]['base']['population'] = 0 # old code: nmmo.Serialized.Entity.Self, no longer being used packet['player'][ent_id]['base']['self'] = 1 npc_colors = { 1: Neon.YELLOW.packet(), # passive npcs 2: Neon.MAGENTA.packet(), # neutral npcs 3: Neon.BLOOD.packet() } # aggressive npcs for ent_id in packet['npc']: npc = realm.npcs.corporeal[ent_id] packet['npc'][ent_id]['base']['color'] = npc_colors[int(npc.npc_type.val)] packet['npc'][ent_id]['base']['population'] = -int(npc.npc_type.val) # note negative packet['npc'][ent_id]['base']['self'] = 1 return packet ================================================ FILE: nmmo/systems/__init__.py ================================================ from .skill import Skill ================================================ FILE: nmmo/systems/combat.py ================================================ #Various utilities for managing combat, including hit/damage import numpy as np from nmmo.systems import skill as Skill from nmmo.lib.event_code import EventCode def level(skills): return max(e.level.val for e in skills.skills) def damage_multiplier(config, skill, targ): skills = [targ.skills.melee, targ.skills.range, targ.skills.mage] exp = [s.exp for s in skills] if max(exp) == min(exp): return 1.0 idx = np.argmax([exp]) targ = skills[idx] if isinstance(skill, targ.weakness): return config.COMBAT_WEAKNESS_MULTIPLIER return 1.0 # pylint: disable=unnecessary-lambda-assignment def attack(realm, attacker, target, skill_fn): config = attacker.config skill = skill_fn(attacker) skill_type = type(skill) skill_name = skill_type.__name__ # Per-style offense/defense level_damage = 0 if skill_type == Skill.Melee: base_damage = config.COMBAT_MELEE_DAMAGE if config.PROGRESSION_SYSTEM_ENABLED: base_damage = config.PROGRESSION_MELEE_BASE_DAMAGE level_damage = config.PROGRESSION_MELEE_LEVEL_DAMAGE offense_fn = lambda e: e.melee_attack defense_fn = lambda e: e.melee_defense elif skill_type == Skill.Range: base_damage = config.COMBAT_RANGE_DAMAGE if config.PROGRESSION_SYSTEM_ENABLED: base_damage = config.PROGRESSION_RANGE_BASE_DAMAGE level_damage = config.PROGRESSION_RANGE_LEVEL_DAMAGE offense_fn = lambda e: e.range_attack defense_fn = lambda e: e.range_defense elif skill_type == Skill.Mage: base_damage = config.COMBAT_MAGE_DAMAGE if config.PROGRESSION_SYSTEM_ENABLED: base_damage = config.PROGRESSION_MAGE_BASE_DAMAGE level_damage = config.PROGRESSION_MAGE_LEVEL_DAMAGE offense_fn = lambda e: e.mage_attack defense_fn = lambda e: e.mage_defense elif __debug__: assert False, 'Attack skill must be Melee, Range, or Mage' # Compute modifiers multiplier = damage_multiplier(config, skill, target) # NOTE: skill offense and defense are only for agents, NOT npcs skill_offense = base_damage if attacker.is_player: skill_offense += level_damage * skill.level.val if attacker.is_npc and config.EQUIPMENT_SYSTEM_ENABLED: # NOTE: In this case, npc off/def is set only with equipment. Revisit this. skill_offense = 0 if config.PROGRESSION_SYSTEM_ENABLED and target.is_player: skill_defense = config.PROGRESSION_BASE_DEFENSE + \ config.PROGRESSION_LEVEL_DEFENSE*level(target.skills) else: skill_defense = 0 if config.EQUIPMENT_SYSTEM_ENABLED: equipment_offense = attacker.equipment.total(offense_fn) equipment_defense = target.equipment.total(defense_fn) # after tallying ammo damage, consume ammo (i.e., fire) when the skill type matches ammunition = attacker.equipment.ammunition.item if ammunition is not None and getattr(ammunition, skill_name.lower() + '_attack').val > 0: ammunition.fire(attacker) else: equipment_offense = 0 equipment_defense = 0 # Total damage calculation offense = skill_offense + equipment_offense defense = skill_defense + equipment_defense min_damage_prop = config.COMBAT_MINIMUM_DAMAGE_PROPORTION damage = config.COMBAT_DAMAGE_FORMULA(offense, defense, multiplier, min_damage_prop) damage = max(int(damage), 0) if attacker.is_player: realm.event_log.record(EventCode.SCORE_HIT, attacker, target=target, combat_style=skill_type, damage=damage) attacker.apply_damage(damage, skill.__class__.__name__.lower()) target.receive_damage(attacker, damage) return damage def danger(config, pos): border = config.MAP_BORDER center = config.MAP_CENTER r, c = pos #Distance from border r_dist = min(r - border, center + border - r - 1) c_dist = min(c - border, center + border - c - 1) dist = min(r_dist, c_dist) norm = 2 * dist / center return norm def spawn(config, dnger, np_random): border = config.MAP_BORDER center = config.MAP_CENTER mid = center // 2 dist = dnger * center / 2 max_offset = mid - dist offset = mid + border + np_random.integers(-max_offset, max_offset) rng = np_random.random() if rng < 0.25: r = border + dist c = offset elif rng < 0.5: r = border + center - dist - 1 c = offset elif rng < 0.75: c = border + dist r = offset else: c = border + center - dist - 1 r = offset if __debug__: assert dnger == danger(config, (r,c)), 'Agent spawned at incorrect radius' r = int(r) c = int(c) return r, c ================================================ FILE: nmmo/systems/droptable.py ================================================ class Fixed(): def __init__(self, item): self.item = item def roll(self, realm, level): return [self.item(realm, level)] class Drop: def __init__(self, item, prob): self.item = item self.prob = prob def roll(self, realm, level): # TODO: do not access realm._np_random directly # related to skill.py, all harvest skills # pylint: disable=protected-access if realm._np_random.random() < self.prob: return self.item(realm, level) return None class Standard: def __init__(self): self.drops = [] def add(self, item, prob=1.0): self.drops += [Drop(item, prob)] def roll(self, realm, level): ret = [] for e in self.drops: drop = e.roll(realm, level) if drop is not None: ret += [drop] return ret class Empty(Standard): def roll(self, realm, level): return [] class Ammunition(Standard): def __init__(self, item): super().__init__() self.item = item def roll(self, realm, level): return [self.item(realm, level)] class Consumable(Standard): def __init__(self, item): super().__init__() self.item = item def roll(self, realm, level): return [self.item(realm, level)] ================================================ FILE: nmmo/systems/exchange.py ================================================ from __future__ import annotations from collections import deque import math from typing import Dict from nmmo.systems.item import Item, Stack from nmmo.lib.event_code import EventCode """ The Exchange class is a simulation of an in-game item exchange. It has several methods that allow players to list items for sale, buy items, and remove expired listings. The _list_item() method is used to add a new item to the exchange, and the unlist_item() method is used to remove an item from the exchange. The step() method is used to regularly check and remove expired listings. The sell() method allows a player to sell an item, and the buy() method allows a player to purchase an item. The packet property returns a dictionary that contains information about the items currently being sold on the exchange, such as the maximum and minimum price, the average price, and the total supply of the items. """ class ItemListing: def __init__(self, item: Item, seller, price: int, tick: int): self.item = item self.seller = seller self.price = price self.tick = tick class Exchange: def __init__(self, realm): self._listings_queue: deque[(int, int)] = deque() # (item_id, tick) self._item_listings: Dict[int, ItemListing] = {} self._realm = realm self._config = realm.config def reset(self): self._listings_queue.clear() self._item_listings.clear() def _list_item(self, item: Item, seller, price: int, tick: int): item.listed_price.update(price) self._item_listings[item.id.val] = ItemListing(item, seller, price, tick) self._listings_queue.append((item.id.val, tick)) def unlist_item(self, item: Item): if item.id.val in self._item_listings: self._unlist_item(item.id.val) def _unlist_item(self, item_id: int): item = self._item_listings.pop(item_id).item item.listed_price.update(0) def step(self): """ Remove expired listings from the exchange's listings queue and item listings dictionary. The method starts by checking the oldest listing in the listings queue using a while loop. If the current tick minus the listing tick is less than or equal to the EXCHANGE_LISTING_DURATION in the realm's configuration, the method breaks out of the loop as the oldest listing has not expired. If the oldest listing has expired, the method removes it from the listings queue and the item listings dictionary. It then checks if the actual listing still exists and that it is indeed expired. If it does exist and is expired, it calls the _unlist_item method to remove the listing and update the item's listed price. The process repeats until all expired listings are removed from the queue and dictionary. """ if self._config.EXCHANGE_SYSTEM_ENABLED is False: return current_tick = self._realm.tick # Remove expired listings while self._listings_queue: (item_id, listing_tick) = self._listings_queue[0] if current_tick - listing_tick <= self._config.EXCHANGE_LISTING_DURATION: # Oldest listing has not expired break # Remove expired listing from queue self._listings_queue.popleft() # The actual listing might have been refreshed and is newer than the queue record. # Or it might have already been removed. listing = self._item_listings.get(item_id) if listing is not None and \ current_tick - listing.tick > self._config.EXCHANGE_LISTING_DURATION: self._unlist_item(item_id) def sell(self, seller, item: Item, price: int, tick: int): assert isinstance( item, object), f'{item} for sale is not an Item instance' assert item in seller.inventory, f'{item} for sale is not in {seller} inventory' assert item.quantity.val > 0, f'{item} for sale has quantity {item.quantity.val}' assert item.listed_price.val == 0, 'Item is already listed' assert item.equipped.val == 0, 'Item has been equiped so cannot be listed' assert price > 0, 'Price must be larger than 0' self._list_item(item, seller, price, tick) self._realm.event_log.record(EventCode.LIST_ITEM, seller, item=item, price=price) def buy(self, buyer, item: Item): assert item.quantity.val > 0, f'{item} purchase has quantity {item.quantity.val}' assert item.equipped.val == 0, 'Listed item must not be equipped' assert buyer.gold.val >= item.listed_price.val, 'Buyer does not have enough gold' assert buyer.ent_id != item.owner_id.val, 'One cannot buy their own items' if not buyer.inventory.space: if isinstance(item, Stack): if not buyer.inventory.has_stack(item.signature): # no ammo stack with the same signature, so cannot buy return else: # no space, and item is not ammo stack, so cannot buy return # item is not in the listing (perhaps bought by other) if item.id.val not in self._item_listings: return listing = self._item_listings[item.id.val] price = item.listed_price.val self.unlist_item(item) listing.seller.inventory.remove(item) buyer.inventory.receive(item) buyer.gold.decrement(price) listing.seller.gold.increment(price) self._realm.event_log.record(EventCode.BUY_ITEM, buyer, item=item, price=price) self._realm.event_log.record(EventCode.EARN_GOLD, listing.seller, amount=price) @property def packet(self): packet = {} for listing in self._item_listings.values(): item = listing.item key = f'{item.__class__.__name__}_{item.level.val}' max_price = max(packet.get(key, {}).get('max_price', -math.inf), listing.price) min_price = min(packet.get(key, {}).get('min_price', math.inf), listing.price) supply = packet.get(key, {}).get('supply', 0) + item.quantity.val packet[key] = { 'max_price': max_price, 'min_price': min_price, 'price': (max_price + min_price) / 2, 'supply': supply } return packet ================================================ FILE: nmmo/systems/inventory.py ================================================ from typing import Dict, Tuple from ordered_set import OrderedSet from nmmo.systems import item as Item class EquipmentSlot: def __init__(self) -> None: self.item = None def equip(self, item: Item.Item) -> None: self.item = item def unequip(self) -> None: if self.item: self.item.equipped.update(0) self.item = None class Equipment: def __init__(self): self.hat = EquipmentSlot() self.top = EquipmentSlot() self.bottom = EquipmentSlot() self.held = EquipmentSlot() self.ammunition = EquipmentSlot() def total(self, lambda_getter): items = [lambda_getter(e).val for e in self] if not items: return 0 return sum(items) def __iter__(self): for slot in [self.hat, self.top, self.bottom, self.held, self.ammunition]: if slot.item is not None: yield slot.item def conditional_packet(self, packet, slot_name: str, slot: EquipmentSlot): if slot.item: packet[slot_name] = slot.item.packet @property def item_level(self): return self.total(lambda e: e.level) @property def melee_attack(self): return self.total(lambda e: e.melee_attack) @property def range_attack(self): return self.total(lambda e: e.range_attack) @property def mage_attack(self): return self.total(lambda e: e.mage_attack) @property def melee_defense(self): return self.total(lambda e: e.melee_defense) @property def range_defense(self): return self.total(lambda e: e.range_defense) @property def mage_defense(self): return self.total(lambda e: e.mage_defense) @property def packet(self): packet = {} self.conditional_packet(packet, 'hat', self.hat) self.conditional_packet(packet, 'top', self.top) self.conditional_packet(packet, 'bottom', self.bottom) self.conditional_packet(packet, 'held', self.held) self.conditional_packet(packet, 'ammunition', self.ammunition) # pylint: disable=R0801 # Similar lines here and in npc.py packet['item_level'] = self.item_level packet['melee_attack'] = self.melee_attack packet['range_attack'] = self.range_attack packet['mage_attack'] = self.mage_attack packet['melee_defense'] = self.melee_defense packet['range_defense'] = self.range_defense packet['mage_defense'] = self.mage_defense return packet class Inventory: def __init__(self, realm, entity): config = realm.config self.realm = realm self.entity = entity self.config = config self.equipment = Equipment() self.capacity = 0 if config.ITEM_SYSTEM_ENABLED and entity.is_player: self.capacity = config.ITEM_INVENTORY_CAPACITY self._item_stacks: Dict[Tuple, Item.Stack] = {} self.items: OrderedSet[Item.Item] = OrderedSet([]) # critical for correct functioning @property def space(self): return self.capacity - len(self.items) def has_stack(self, signature: Tuple) -> bool: return signature in self._item_stacks def packet(self): item_packet = [] if self.config.ITEM_SYSTEM_ENABLED: item_packet = [e.packet for e in self.items] return { 'items': item_packet, 'equipment': self.equipment.packet} def __iter__(self): for item in self.items: yield item def receive(self, item: Item.Item) -> bool: # Return True if the item is received assert isinstance(item, Item.Item), f'{item} received is not an Item instance' assert item not in self.items, f'{item} object received already in inventory' assert not item.equipped.val, f'Received equipped item {item}' assert not item.listed_price.val, f'Received listed item {item}' assert item.quantity.val, f'Received empty item {item}' if isinstance(item, Item.Stack): signature = item.signature if self.has_stack(signature): stack = self._item_stacks[signature] assert item.level.val == stack.level.val, f'{item} stack level mismatch' stack.quantity.increment(item.quantity.val) # destroy the original item instance after the transfer is complete item.destroy() return False if not self.space: # if no space thus cannot receive, just destroy the item item.destroy() return False self._item_stacks[signature] = item if not self.space: # if no space thus cannot receive, just destroy the item item.destroy() return False item.owner_id.update(self.entity.id.val) self.items.add(item) return True # pylint: disable=protected-access def remove(self, item, quantity=None): assert isinstance(item, Item.Item), f'{item} removing item is not an Item instance' assert item in self.items, f'No item {item} to remove' if isinstance(item, Item.Equipment) and item.equipped.val: item.unequip(item._slot(self.entity)) if isinstance(item, Item.Stack): signature = item.signature assert self.has_stack(item.signature), f'{item} stack to remove not in inventory' stack = self._item_stacks[signature] if quantity is None or stack.quantity.val == quantity: self._remove(stack) del self._item_stacks[signature] return assert 0 < quantity <= stack.quantity.val, \ f'Invalid remove {quantity} x {item} ({stack.quantity.val} available)' stack.quantity.val -= quantity return self._remove(item) def _remove(self, item): self.realm.exchange.unlist_item(item) item.owner_id.update(0) self.items.remove(item) ================================================ FILE: nmmo/systems/item.py ================================================ from __future__ import annotations import math from abc import ABC from types import SimpleNamespace from typing import Dict from nmmo.datastore.serialized import SerializedState from nmmo.lib.colors import Tier from nmmo.lib.event_code import EventCode # pylint: disable=no-member ItemState = SerializedState.subclass("Item", [ "id", "type_id", "owner_id", "level", "capacity", "quantity", "melee_attack", "range_attack", "mage_attack", "melee_defense", "range_defense", "mage_defense", "health_restore", "resource_restore", "equipped", # Market "listed_price", ]) # TODO: These limits should be defined in the config. ItemState.Limits = lambda config: { "id": (0, math.inf), "type_id": (0, 99), "owner_id": (-math.inf, math.inf), "level": (0, 99), "capacity": (0, 99), "quantity": (0, math.inf), # NOTE: Ammunitions can be stacked infinitely "melee_attack": (0, 100), "range_attack": (0, 100), "mage_attack": (0, 100), "melee_defense": (0, 100), "range_defense": (0, 100), "mage_defense": (0, 100), "health_restore": (0, 100), "resource_restore": (0, 100), "equipped": (0, 1), "listed_price": (0, math.inf), } ItemState.Query = SimpleNamespace( table=lambda ds: ds.table("Item").where_neq( ItemState.State.attr_name_to_col["id"], 0), by_id=lambda ds, id: ds.table("Item").where_eq( ItemState.State.attr_name_to_col["id"], id), owned_by = lambda ds, id: ds.table("Item").where_eq( ItemState.State.attr_name_to_col["owner_id"], id), for_sale = lambda ds: ds.table("Item").where_neq( ItemState.State.attr_name_to_col["listed_price"], 0), ) class Item(ItemState): ITEM_TYPE_ID = None _item_type_id_to_class: Dict[int, type] = {} @staticmethod def register(item_type): assert item_type.ITEM_TYPE_ID is not None if item_type.ITEM_TYPE_ID not in Item._item_type_id_to_class: Item._item_type_id_to_class[item_type.ITEM_TYPE_ID] = item_type @staticmethod def item_class(type_id: int): return Item._item_type_id_to_class[type_id] def __init__(self, realm, level, capacity=0, melee_attack=0, range_attack=0, mage_attack=0, melee_defense=0, range_defense=0, mage_defense=0, health_restore=0, resource_restore=0): super().__init__(realm.datastore, ItemState.Limits(realm.config)) self.realm = realm self.config = realm.config Item.register(self.__class__) self.id.update(self.datastore_record.id) self.type_id.update(self.ITEM_TYPE_ID) self.level.update(level) self.capacity.update(capacity) # every item instance is created individually, i.e., quantity=1 self.quantity.update(1) self.melee_attack.update(melee_attack) self.range_attack.update(range_attack) self.mage_attack.update(mage_attack) self.melee_defense.update(melee_defense) self.range_defense.update(range_defense) self.mage_defense.update(mage_defense) self.health_restore.update(health_restore) self.resource_restore.update(resource_restore) realm.items[self.id.val] = self def destroy(self): # NOTE: we may want to track the item lifecycle and # and see how many high-level items are wasted if self.config.EXCHANGE_SYSTEM_ENABLED: self.realm.exchange.unlist_item(self) if self.owner_id.val in self.realm.players: self.realm.players[self.owner_id.val].inventory.remove(self) self.realm.items.pop(self.id.val, None) self.datastore_record.delete() @property def packet(self): return {'item': self.__class__.__name__, 'level': self.level.val, 'capacity': self.capacity.val, 'quantity': self.quantity.val, 'melee_attack': self.melee_attack.val, 'range_attack': self.range_attack.val, 'mage_attack': self.mage_attack.val, 'melee_defense': self.melee_defense.val, 'range_defense': self.range_defense.val, 'mage_defense': self.mage_defense.val, 'health_restore': self.health_restore.val, 'resource_restore': self.resource_restore.val, } def _level(self, entity): # this is for armors, ration, and potion # weapons and tools must override this with specific skills return entity.level def level_gt(self, entity): return self.level.val > self._level(entity) def use(self, entity) -> bool: raise NotImplementedError class Stack: @property def signature(self): return (self.type_id.val, self.level.val) class Equipment(Item): @property def packet(self): packet = {'color': self.color.packet()} return {**packet, **super().packet} @property def color(self): if self.level == 0: return Tier.BLACK if self.level < 10: return Tier.WOOD if self.level < 20: return Tier.BRONZE if self.level < 40: return Tier.SILVER if self.level < 60: return Tier.GOLD if self.level < 80: return Tier.PLATINUM return Tier.DIAMOND def unequip(self, equip_slot): assert self.equipped.val == 1 self.equipped.update(0) equip_slot.unequip() def equip(self, entity, equip_slot): assert self.equipped.val == 0 if self._level(entity) < self.level.val: return self.equipped.update(1) equip_slot.equip(self) def _slot(self, entity): raise NotImplementedError def use(self, entity): assert self in entity.inventory, "Item is not in entity's inventory" assert self.listed_price == 0, "Listed item cannot be used" assert self._level(entity) >= self.level.val, "Entity's level is not sufficient to use the item" if self.equipped.val: self.unequip(self._slot(entity)) else: # always empty the slot first self._slot(entity).unequip() self.equip(entity, self._slot(entity)) self.realm.event_log.record(EventCode.EQUIP_ITEM, entity, item=self) class Armor(Equipment, ABC): def __init__(self, realm, level, **kwargs): defense = realm.config.EQUIPMENT_ARMOR_BASE_DEFENSE + \ level*realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE super().__init__(realm, level, melee_defense=defense, range_defense=defense, mage_defense=defense, **kwargs) class Hat(Armor): ITEM_TYPE_ID = 2 def _slot(self, entity): return entity.inventory.equipment.hat class Top(Armor): ITEM_TYPE_ID = 3 def _slot(self, entity): return entity.inventory.equipment.top class Bottom(Armor): ITEM_TYPE_ID = 4 def _slot(self, entity): return entity.inventory.equipment.bottom class Weapon(Equipment): def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.attack = ( realm.config.EQUIPMENT_WEAPON_BASE_DAMAGE + level*realm.config.EQUIPMENT_WEAPON_LEVEL_DAMAGE) def _slot(self, entity): return entity.inventory.equipment.held class Spear(Weapon): ITEM_TYPE_ID = 5 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.melee_attack.update(self.attack) def _level(self, entity): return entity.skills.melee.level.val class Bow(Weapon): ITEM_TYPE_ID = 6 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.range_attack.update(self.attack) def _level(self, entity): return entity.skills.range.level.val class Wand(Weapon): ITEM_TYPE_ID = 7 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.mage_attack.update(self.attack) def _level(self, entity): return entity.skills.mage.level.val class Tool(Equipment): def __init__(self, realm, level, **kwargs): defense = realm.config.EQUIPMENT_TOOL_BASE_DEFENSE + \ level*realm.config.EQUIPMENT_TOOL_LEVEL_DEFENSE super().__init__(realm, level, melee_defense=defense, range_defense=defense, mage_defense=defense, **kwargs) def _slot(self, entity): return entity.inventory.equipment.held class Rod(Tool): ITEM_TYPE_ID = 8 def _level(self, entity): return entity.skills.fishing.level.val class Gloves(Tool): ITEM_TYPE_ID = 9 def _level(self, entity): return entity.skills.herbalism.level.val class Pickaxe(Tool): ITEM_TYPE_ID = 10 def _level(self, entity): return entity.skills.prospecting.level.val class Axe(Tool): ITEM_TYPE_ID = 11 def _level(self, entity): return entity.skills.carving.level.val class Chisel(Tool): ITEM_TYPE_ID = 12 def _level(self, entity): return entity.skills.alchemy.level.val class Ammunition(Equipment, Stack): def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.attack = ( realm.config.EQUIPMENT_AMMUNITION_BASE_DAMAGE + level*realm.config.EQUIPMENT_AMMUNITION_LEVEL_DAMAGE) def _slot(self, entity): return entity.inventory.equipment.ammunition def fire(self, entity) -> int: assert self.equipped.val > 0, 'Ammunition not equipped' assert self.quantity.val > 0, 'Used ammunition with 0 quantity' self.quantity.decrement() if self.quantity.val == 0: entity.inventory.remove(self) # delete this empty item instance from the datastore self.destroy() self.realm.event_log.record(EventCode.FIRE_AMMO, entity, item=self) return self.damage class Whetstone(Ammunition): ITEM_TYPE_ID = 13 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.melee_attack.update(self.attack) def _level(self, entity): return entity.skills.melee.level.val @property def damage(self): return self.melee_attack.val class Arrow(Ammunition): ITEM_TYPE_ID = 14 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.range_attack.update(self.attack) def _level(self, entity): return entity.skills.range.level.val @property def damage(self): return self.range_attack.val class Runes(Ammunition): ITEM_TYPE_ID = 15 def __init__(self, realm, level, **kwargs): super().__init__(realm, level, **kwargs) self.mage_attack.update(self.attack) def _level(self, entity): return entity.skills.mage.level.val @property def damage(self): return self.mage_attack.val # NOTE: Each consumable item (ration, potion) cannot be stacked, # so each item takes 1 inventory space class Consumable(Item): def use(self, entity) -> bool: assert self in entity.inventory, "Item is not in entity's inventory" assert self.listed_price == 0, "Listed item cannot be used" assert self._level(entity) >= self.level.val, "Entity's level is not sufficient to use the item" self.realm.event_log.record(EventCode.CONSUME_ITEM, entity, item=self) self._apply_effects(entity) entity.inventory.remove(self) self.destroy() return True class Ration(Consumable): ITEM_TYPE_ID = 16 def __init__(self, realm, level, **kwargs): restore = 0 if realm.config.PROFESSION_SYSTEM_ENABLED: restore = realm.config.PROFESSION_CONSUMABLE_RESTORE(level) super().__init__(realm, level, resource_restore=restore, **kwargs) def _apply_effects(self, entity): entity.resources.food.increment(self.resource_restore.val) entity.resources.water.increment(self.resource_restore.val) class Potion(Consumable): ITEM_TYPE_ID = 17 def __init__(self, realm, level, **kwargs): restore = 0 if realm.config.PROFESSION_SYSTEM_ENABLED: restore = realm.config.PROFESSION_CONSUMABLE_RESTORE(level) super().__init__(realm, level, health_restore=restore, **kwargs) def _apply_effects(self, entity): entity.resources.health.increment(self.health_restore.val) entity.poultice_consumed += 1 entity.poultice_level_consumed = max( entity.poultice_level_consumed, self.level.val) # Item groupings ARMOR = [Hat, Top, Bottom] WEAPON = [Spear, Bow, Wand] TOOL = [Rod, Gloves, Pickaxe, Axe, Chisel] AMMUNITION = [Whetstone, Arrow, Runes] CONSUMABLE = [Ration, Potion] ALL_ITEM = ARMOR + WEAPON + TOOL + AMMUNITION + CONSUMABLE ================================================ FILE: nmmo/systems/skill.py ================================================ from __future__ import annotations import abc import numpy as np from ordered_set import OrderedSet from nmmo.lib import material from nmmo.systems import combat from nmmo.lib.event_code import EventCode ### Infrastructure ### class ExperienceCalculator: def __init__(self, config): if not config.PROGRESSION_SYSTEM_ENABLED: return self.config = config self.exp_threshold = np.array(config.PROGRESSION_EXP_THRESHOLD) assert len(self.exp_threshold) >= config.PROGRESSION_LEVEL_MAX,\ "PROGRESSION_LEVEL_BY_EXP must have at least PROGRESSION_LEVEL_MAX entries" self.max_exp = self.exp_threshold[self.config.PROGRESSION_LEVEL_MAX - 1] def exp_at_level(self, level): level = min(max(level, self.config.PROGRESSION_BASE_LEVEL), self.config.PROGRESSION_LEVEL_MAX) return int(self.exp_threshold[level - 1]) def level_at_exp(self, exp): if exp >= self.max_exp: return self.config.PROGRESSION_LEVEL_MAX return np.argmin(exp >= self.exp_threshold) class SkillGroup: def __init__(self, realm, entity): self.config = realm.config self.realm = realm self.entity = entity self.experience_calculator = ExperienceCalculator(self.config) self.skills = OrderedSet() # critical for determinism def update(self): for skill in self.skills: skill.update() def packet(self): data = {} for skill in self.skills: data[skill.__class__.__name__.lower()] = skill.packet() return data class Skill(abc.ABC): def __init__(self, skill_group: SkillGroup): self.realm = skill_group.realm self.config = skill_group.config self.entity = skill_group.entity self.experience_calculator = skill_group.experience_calculator self.skill_group = skill_group skill_group.skills.add(self) def packet(self): data = {} data['exp'] = self.exp.val data['level'] = self.level.val return data def add_xp(self, xp): self.exp.increment(xp) new_level = int(self.experience_calculator.level_at_exp(self.exp.val)) if new_level > self.level.val: self.level.update(new_level) self.realm.event_log.record(EventCode.LEVEL_UP, self.entity, skill=self, level=new_level) def set_experience_by_level(self, level): self.exp.update(self.experience_calculator.level_at_exp(level)) self.level.update(int(level)) @property def level(self): raise NotImplementedError(f"Skill {self.__class__.__name__} "\ "does not implement 'level' property") @property def exp(self): raise NotImplementedError(f"Skill {self.__class__.__name__} "\ "does not implement 'exp' property") ### Skill Bases ### class CombatSkill(Skill): def update(self): pass class NonCombatSkill(Skill): def __init__(self, skill_group: SkillGroup): super().__init__(skill_group) self._dummy_value = DummyValue() # for water and food @property def level(self): return self._dummy_value @property def exp(self): return self._dummy_value class HarvestSkill(NonCombatSkill): def process_drops(self, matl, drop_table): if not self.config.ITEM_SYSTEM_ENABLED: return entity = self.entity # harvest without tool will only yield level-1 item even with high skill level # for example, fishing level=5 without rod will only yield level-1 ration level = 1 tool = entity.equipment.held if matl.tool is not None and isinstance(tool.item, matl.tool): level = min(1+tool.item.level.val, self.config.PROGRESSION_LEVEL_MAX) #TODO: double-check drop table quantity for drop in drop_table.roll(self.realm, level): assert drop.level.val == level, 'Drop level does not match roll specification' if entity.inventory.space: entity.inventory.receive(drop) self.realm.event_log.record(EventCode.HARVEST_ITEM, entity, item=drop) else: drop.destroy() # this was the source of the item leak def harvest(self, matl, deplete=True): entity = self.entity realm = self.realm r, c = entity.pos if realm.map.tiles[r, c].state != matl: return False drop_table = realm.map.harvest(r, c, deplete) if drop_table: self.process_drops(matl, drop_table) return drop_table def harvest_adjacent(self, matl, deplete=True): entity = self.entity realm = self.realm r, c = entity.pos drop_table = None if realm.map.tiles[r-1, c].state == matl: drop_table = realm.map.harvest(r-1, c, deplete) if realm.map.tiles[r+1, c].state == matl: drop_table = realm.map.harvest(r+1, c, deplete) if realm.map.tiles[r, c-1].state == matl: drop_table = realm.map.harvest(r, c-1, deplete) if realm.map.tiles[r, c+1].state == matl: drop_table = realm.map.harvest(r, c+1, deplete) if drop_table: self.process_drops(matl, drop_table) return drop_table class AmmunitionSkill(HarvestSkill): def process_drops(self, matl, drop_table): super().process_drops(matl, drop_table) if self.config.PROGRESSION_SYSTEM_ENABLED: self.add_xp(self.config.PROGRESSION_AMMUNITION_XP_SCALE) class ConsumableSkill(HarvestSkill): def process_drops(self, matl, drop_table): super().process_drops(matl, drop_table) if self.config.PROGRESSION_SYSTEM_ENABLED: self.add_xp(self.config.PROGRESSION_CONSUMABLE_XP_SCALE) ### Skill groups ### class Basic(SkillGroup): def __init__(self, realm, entity): super().__init__(realm, entity) self.water = Water(self) self.food = Food(self) @property def basic_level(self): return 0.5 * (self.water.level + self.food.level) class Harvest(SkillGroup): def __init__(self, realm, entity): super().__init__(realm, entity) self.fishing = Fishing(self) self.herbalism = Herbalism(self) self.prospecting = Prospecting(self) self.carving = Carving(self) self.alchemy = Alchemy(self) @property def harvest_level(self): return max(self.fishing.level, self.herbalism.level, self.prospecting.level, self.carving.level, self.alchemy.level) class Combat(SkillGroup): def __init__(self, realm, entity): super().__init__(realm, entity) self.melee = Melee(self) self.range = Range(self) self.mage = Mage(self) def packet(self): data = super().packet() data['level'] = combat.level(self) return data @property def combat_level(self): return max(self.melee.level, self.range.level, self.mage.level) def apply_damage(self, style): if self.config.PROGRESSION_SYSTEM_ENABLED: skill = self.__dict__[style] skill.add_xp(self.config.PROGRESSION_COMBAT_XP_SCALE) def receive_damage(self, dmg): pass class Skills(Basic, Harvest, Combat): pass ### Combat Skills ### class Melee(CombatSkill): SKILL_ID = 1 @property def level(self): return self.entity.melee_level @property def exp(self): return self.entity.melee_exp class Range(CombatSkill): SKILL_ID = 2 @property def level(self): return self.entity.range_level @property def exp(self): return self.entity.range_exp class Mage(CombatSkill): SKILL_ID = 3 @property def level(self): return self.entity.mage_level @property def exp(self): return self.entity.mage_exp Melee.weakness = Mage Range.weakness = Melee Mage.weakness = Range ### Basic/Harvest Skills ### class DummyValue: def __init__(self, val=0): self.val = val def update(self, val): self.val = val class Water(HarvestSkill): def update(self): config = self.config if not config.RESOURCE_SYSTEM_ENABLED: return if config.IMMORTAL or self.entity.immortal: return depletion = config.RESOURCE_DEPLETION_RATE water = self.entity.resources.water water.decrement(depletion) if not self.harvest_adjacent(material.Water, deplete=False): return restore = np.floor(config.RESOURCE_BASE * config.RESOURCE_HARVEST_RESTORE_FRACTION) water.increment(restore) self.realm.event_log.record(EventCode.DRINK_WATER, self.entity) class Food(HarvestSkill): def update(self): config = self.config if not config.RESOURCE_SYSTEM_ENABLED: return if config.IMMORTAL or self.entity.immortal: return depletion = config.RESOURCE_DEPLETION_RATE food = self.entity.resources.food food.decrement(depletion) if not self.harvest(material.Foilage): return restore = np.floor(config.RESOURCE_BASE * config.RESOURCE_HARVEST_RESTORE_FRACTION) food.increment(restore) self.realm.event_log.record(EventCode.EAT_FOOD, self.entity) class Fishing(ConsumableSkill): SKILL_ID = 4 @property def level(self): return self.entity.fishing_level @property def exp(self): return self.entity.fishing_exp def update(self): self.harvest_adjacent(material.Fish) class Herbalism(ConsumableSkill): SKILL_ID = 5 @property def level(self): return self.entity.herbalism_level @property def exp(self): return self.entity.herbalism_exp def update(self): self.harvest(material.Herb) class Prospecting(AmmunitionSkill): SKILL_ID = 6 @property def level(self): return self.entity.prospecting_level @property def exp(self): return self.entity.prospecting_exp def update(self): self.harvest(material.Ore) class Carving(AmmunitionSkill): SKILL_ID = 7 @property def level(self): return self.entity.carving_level @property def exp(self): return self.entity.carving_exp def update(self,): self.harvest(material.Tree) class Alchemy(AmmunitionSkill): SKILL_ID = 8 @property def level(self): return self.entity.alchemy_level @property def exp(self): return self.entity.alchemy_exp def update(self): self.harvest(material.Crystal) # Skill groupings COMBAT_SKILL = [Melee, Range, Mage] HARVEST_SKILL = [Fishing, Herbalism, Prospecting, Carving, Alchemy] ================================================ FILE: nmmo/task/__init__.py ================================================ from .game_state import * from .predicate_api import * from .task_api import * ================================================ FILE: nmmo/task/base_predicates.py ================================================ #pylint: disable=invalid-name, unused-argument, no-value-for-parameter from __future__ import annotations from typing import Iterable import numpy as np from numpy import count_nonzero as count from nmmo.task.group import Group from nmmo.task.game_state import GameState from nmmo.systems import skill as nmmo_skill from nmmo.systems.skill import Skill from nmmo.systems.item import Item from nmmo.lib.material import Material from nmmo.lib import utils def norm(progress): return max(min(progress, 1.0), 0.0) def Success(gs: GameState, subject: Group): ''' Returns True. For debugging. ''' return True def TickGE(gs: GameState, subject: Group, num_tick: int = None): """True if the current tick is greater than or equal to the specified num_tick. Is progress counter. """ if num_tick is None: num_tick = gs.config.HORIZON return norm(gs.current_tick / num_tick) def CanSeeTile(gs: GameState, subject: Group, tile_type: type[Material]): """ True if any agent in subject can see a tile of tile_type """ return any(tile_type.index in t for t in subject.obs.tile.material_id) def StayAlive(gs: GameState, subject: Group): """True if all subjects are alive. """ return count(subject.health > 0) == len(subject) def AllDead(gs: GameState, subject: Group): """True if all subjects are dead. """ return norm(1.0 - count(subject.health) / len(subject)) def CheckAgentStatus(gs: GameState, subject: Group, target: Iterable[int], status: str): """Check if target agents are alive or dead using the game status""" if isinstance(target, int): target = [target] num_agents = len(target) num_alive = sum(1 for agent in target if agent in gs.alive_agents) if status == 'alive': return num_alive / num_agents if status == 'dead': return (num_agents - num_alive) / num_agents # invalid status return 0.0 def OccupyTile(gs: GameState, subject: Group, row: int, col: int): """True if any subject agent is on the desginated tile. """ return np.any((subject.row == row) & (subject.col == col)) def CanSeeAgent(gs: GameState, subject: Group, target: int): """True if obj_agent is present in the subjects' entities obs. """ return any(target in e.ids for e in subject.obs.entities) def CanSeeGroup(gs: GameState, subject: Group, target: Iterable[int]): """ Returns True if subject can see any of target """ if target is None: return False return any(CanSeeAgent(gs, subject, agent) for agent in target) def DistanceTraveled(gs: GameState, subject: Group, dist: int): """True if the summed l-inf distance between each agent's current pos and spawn pos is greater than or equal to the specified _dist. """ if not any(subject.health > 0): return False r = subject.row c = subject.col dists = utils.linf(list(zip(r,c)),[gs.spawn_pos[id_] for id_ in subject.entity.id]) return norm(dists.sum() / dist) def AttainSkill(gs: GameState, subject: Group, skill: type[Skill], level: int, num_agent: int): """True if the number of agents having skill level GE level is greather than or equal to num_agent """ if level <= 1: return 1.0 skill_level = getattr(subject,skill.__name__.lower() + '_level') - 1 # base level is 1 return norm(sum(skill_level) / (num_agent * (level-1))) def GainExperience(gs: GameState, subject: Group, skill: type[Skill], experience: int, num_agent: int): """True if the experience gained for the skill is greater than or equal to experience.""" skill_exp = getattr(subject,skill.__name__.lower() + '_exp') return norm(sum(skill_exp) / (experience*num_agent)) def CountEvent(gs: GameState, subject: Group, event: str, N: int): """True if the number of events occured in subject corresponding to event >= N """ return norm(len(getattr(subject.event, event)) / N) def ScoreHit(gs: GameState, subject: Group, combat_style: type[Skill], N: int): """True if the number of hits scored in style combat_style >= count """ hits = subject.event.SCORE_HIT.combat_style == combat_style.SKILL_ID return norm(count(hits) / N) def DefeatEntity(gs: GameState, subject: Group, agent_type: str, level: int, num_agent: int): """True if the number of agents (agent_type, >= level) defeated is greater than or equal to num_agent """ # NOTE: there is no way to tell if an agent is a teammate or an enemy # so agents can get rewarded for killing their own teammates defeated_type = subject.event.PLAYER_KILL.target_ent > 0 if agent_type == 'player' \ else subject.event.PLAYER_KILL.target_ent < 0 defeated = defeated_type & (subject.event.PLAYER_KILL.level >= level) if num_agent > 0: return norm(count(defeated) / num_agent) return 1.0 def HoardGold(gs: GameState, subject: Group, amount: int): """True iff the summed gold of all teammate is greater than or equal to amount. """ return norm(subject.gold.sum() / amount) def EarnGold(gs: GameState, subject: Group, amount: int): """ True if the total amount of gold earned is greater than or equal to amount. """ gold = subject.event.EARN_GOLD.gold.sum() + subject.event.LOOT_GOLD.gold.sum() return norm(gold / amount) def SpendGold(gs: GameState, subject: Group, amount: int): """ True if the total amount of gold spent is greater than or equal to amount. """ return norm(subject.event.BUY_ITEM.gold.sum() / amount) def MakeProfit(gs: GameState, subject: Group, amount: int): """ True if the total amount of gold earned-spent is greater than or equal to amount. """ profits = subject.event.EARN_GOLD.gold.sum() + subject.event.LOOT_GOLD.gold.sum() costs = subject.event.BUY_ITEM.gold.sum() return norm((profits-costs) / amount) def InventorySpaceGE(gs: GameState, subject: Group, space: int): """True if the inventory space of every subjects is greater than or equal to the space. Otherwise false. """ max_space = gs.config.ITEM_INVENTORY_CAPACITY return all(max_space - inv.len >= space for inv in subject.obs.inventory) def OwnItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if the number of items owned (_item_type, >= level) is greater than or equal to quantity. """ owned = (subject.item.type_id == item.ITEM_TYPE_ID) & \ (subject.item.level >= level) return norm(sum(subject.item.quantity[owned]) / quantity) def EquipItem(gs: GameState, subject: Group, item: type[Item], level: int, num_agent: int): """True if the number of agents that equip the item (_item_type, >=_level) is greater than or equal to _num_agent. """ equipped = (subject.item.type_id == item.ITEM_TYPE_ID) & \ (subject.item.level >= level) & \ (subject.item.equipped > 0) if num_agent > 0: return norm(count(equipped) / num_agent) return 1.0 def FullyArmed(gs: GameState, subject: Group, combat_style: type[Skill], level: int, num_agent: int): """True if the number of fully equipped agents is greater than or equal to _num_agent Otherwise false. To determine fully equipped, we look at hat, top, bottom, weapon, ammo, respectively, and see whether these are equipped and has level greater than or equal to _level. """ WEAPON_IDS = { nmmo_skill.Melee: {'weapon':5, 'ammo':13}, # Spear, Whetstone nmmo_skill.Range: {'weapon':6, 'ammo':14}, # Bow, Arrow nmmo_skill.Mage: {'weapon':7, 'ammo':15} # Wand, Runes } item_ids = { 'hat':2, 'top':3, 'bottom':4 } item_ids.update(WEAPON_IDS[combat_style]) lvl_flt = (subject.item.level >= level) & \ (subject.item.equipped > 0) type_flt = np.isin(subject.item.type_id,list(item_ids.values())) _, equipment_numbers = np.unique(subject.item.owner_id[lvl_flt & type_flt], return_counts=True) if num_agent > 0: return norm((equipment_numbers >= len(item_ids.items())).sum() / num_agent) return 1.0 def ConsumeItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if total quantity consumed of item type above level is >= quantity """ type_flt = subject.event.CONSUME_ITEM.type == item.ITEM_TYPE_ID lvl_flt = subject.event.CONSUME_ITEM.level >= level return norm(subject.event.CONSUME_ITEM.number[type_flt & lvl_flt].sum() / quantity) def HarvestItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if total quantity harvested of item type above level is >= quantity """ type_flt = subject.event.HARVEST_ITEM.type == item.ITEM_TYPE_ID lvl_flt = subject.event.HARVEST_ITEM.level >= level return norm(subject.event.HARVEST_ITEM.number[type_flt & lvl_flt].sum() / quantity) def FireAmmo(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if total quantity consumed of item type above level is >= quantity """ type_flt = subject.event.FIRE_AMMO.type == item.ITEM_TYPE_ID lvl_flt = subject.event.FIRE_AMMO.level >= level return norm(subject.event.FIRE_AMMO.number[type_flt & lvl_flt].sum() / quantity) def ListItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if total quantity listed of item type above level is >= quantity """ type_flt = subject.event.LIST_ITEM.type == item.ITEM_TYPE_ID lvl_flt = subject.event.LIST_ITEM.level >= level return norm(subject.event.LIST_ITEM.number[type_flt & lvl_flt].sum() / quantity) def BuyItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int): """True if total quantity purchased of item type above level is >= quantity """ type_flt = subject.event.BUY_ITEM.type == item.ITEM_TYPE_ID lvl_flt = subject.event.BUY_ITEM.level >= level return norm(subject.event.BUY_ITEM.number[type_flt & lvl_flt].sum() / quantity) ############################################################################################ # Below are used for the mini games, so these need to be fast def ProgressTowardCenter(gs, subject): if not any(a in gs.alive_agents for a in subject.agents): # subject should be alive return 0.0 center = gs.config.MAP_SIZE // 2 max_dist = center - gs.config.MAP_BORDER r = subject.row c = subject.col # distance to the center tile, so dist = 0 when subject is on the center tile if len(r) == 1: dists = utils.linf_single((r[0], c[0]), (center, center)) else: coords = np.hstack([r, c]) # NOTE: subject can be multiple agents (e.g., team), so taking the minimum dists = np.min(utils.linf(coords, (center, center))) return 1.0 - dists/max_dist def AllMembersWithinRange(gs: GameState, subject: Group, dist: int): """True if the max l-inf distance of teammates is less than or equal to dist """ if dist < 0 or \ not any(a in gs.alive_agents for a in subject.agents): # subject should be alive return 0.0 max_dist = gs.config.MAP_CENTER r = subject.row c = subject.col current_dist = max(r.max()-r.min(), c.max()-c.min()) if current_dist <= dist: return 1.0 # progress bonus, which takes account of the overall distribution max_dist_score = (max_dist - current_dist) / (max_dist - dist) r_sd_score = dist / max(3*np.std(r), dist) # becomes 1 if 3*std(r) < dist c_sd_score = dist / max(3*np.std(c), dist) # becomes 1 if 3*std(c) < dist return (max_dist_score + r_sd_score + c_sd_score) / 3.0 def SeizeTile(gs: GameState, subject: Group, row: int, col: int, num_ticks: int, progress_bonus = 0.4, seize_bonus = 0.3): if not any(subject.health > 0): # subject should be alive return 0.0 target_tile = (row, col) # When the subject seizes the target tile if target_tile in gs.seize_status and gs.seize_status[target_tile][0] in subject.agents: seize_duration = gs.current_tick - gs.seize_status[target_tile][1] hold_bonus = (1.0 - progress_bonus - seize_bonus) * seize_duration/num_ticks return norm(progress_bonus + seize_bonus + hold_bonus) # motivate agents to seize the target tile #max_dist = utils.linf_single(target_tile, gs.spawn_pos[subject.agents[0]]) max_dist = gs.config.MAP_CENTER // 2 # does not have to be precise r = subject.row c = subject.col # distance to the center tile, so dist = 0 when subject is on the center tile if len(r) == 1: dists = utils.linf_single((r[0], c[0]), target_tile) else: coords = np.hstack([r.reshape(-1,1), c.reshape(-1,1)]) # NOTE: subject can be multiple agents (e.g., team), so taking the minimum dists = np.min(utils.linf(coords, target_tile)) return norm(progress_bonus * (1.0 - dists/max_dist)) def SeizeCenter(gs: GameState, subject: Group, num_ticks: int, progress_bonus = 0.3): row = col = gs.config.MAP_SIZE // 2 # center tile return SeizeTile(gs, subject, row, col, num_ticks, progress_bonus) def SeizeQuadCenter(gs: GameState, subject: Group, num_ticks: int, quadrant: str, progress_bonus = 0.3): center = gs.config.MAP_SIZE // 2 half_dist = gs.config.MAP_CENTER // 4 if quadrant == "first": row = col = center + half_dist elif quadrant == "second": row, col = center - half_dist, center + half_dist elif quadrant == "third": row = col = center - half_dist elif quadrant == "fourth": row, col = center + half_dist, center - half_dist else: raise ValueError(f"Invalid quadrant {quadrant}") return SeizeTile(gs, subject, row, col, num_ticks, progress_bonus) def ProtectLeader(gs, subject, target_protect: int, target_destroy: Iterable[int]): """target_destory is not used for reward, but used as info for the reward wrapper""" # Failed to protect the leader if target_protect not in gs.alive_agents: return 0 # Reward each tick the target is alive return gs.current_tick / gs.config.HORIZON ================================================ FILE: nmmo/task/game_state.py ================================================ from __future__ import annotations from typing import Dict, Iterable, Tuple, MutableMapping, Set, List from dataclasses import dataclass, field from copy import deepcopy from collections import defaultdict import weakref from abc import ABC, abstractmethod import functools import numpy as np from nmmo.core.config import Config from nmmo.core.realm import Realm from nmmo.core.observation import Observation from nmmo.task.group import Group from nmmo.entity.entity import EntityState from nmmo.lib.event_log import EventState, ATTACK_COL_MAP, ITEM_COL_MAP, LEVEL_COL_MAP from nmmo.lib.event_code import EventCode from nmmo.systems.item import ItemState from nmmo.core.tile import TileState EntityAttr = EntityState.State.attr_name_to_col EntityAttrKeys = EntityAttr.keys() EventAttr = EventState.State.attr_name_to_col ItemAttr = ItemState.State.attr_name_to_col TileAttr = TileState.State.attr_name_to_col EventAttr.update(ITEM_COL_MAP) EventAttr.update(ATTACK_COL_MAP) EventAttr.update(LEVEL_COL_MAP) @dataclass(frozen=True) # make gs read-only, except cache_result class GameState: current_tick: int config: Config spawn_pos: Dict[int, Tuple[int, int]] # ent_id: (row, col) of all spawned agents alive_agents: Set[int] # of alive agents' ent_id (for convenience) env_obs: Dict[int, Observation] # env passes the obs of only alive agents entity_data: np.ndarray # a copied, whole Entity ds table entity_index: Dict[int, Iterable] # precomputed index for where_in_1d item_data: np.ndarray # a copied, whole Item ds table item_index: Dict[int, Iterable] event_data: np.ndarray # a copied, whole Event log table event_index: Dict[int, Iterable] # status of the seize target tiles (row, col) -> (ent_id, tick) seize_status: Dict[Tuple[int, int], Tuple[int, int]] cache_result: MutableMapping # cache for general memoization _group_view: List[GroupView] = field(default_factory=list) # cache for GroupView # add helper functions below @functools.lru_cache def entity_or_none(self, ent_id): if ent_id not in self.entity_index: return None return EntityState.parse_array(self.entity_data[self.entity_index[ent_id]][0]) def where_in_id(self, data_type, subject: Iterable[int]): k = (data_type, subject) if k in self.cache_result: return self.cache_result[k] if data_type == 'entity': flt_idx = [row for sbj in subject for row in self.entity_index.get(sbj,[])] self.cache_result[k] = self.entity_data[flt_idx] if data_type == 'item': flt_idx = [row for sbj in subject for row in self.item_index.get(sbj,[])] self.cache_result[k] = self.item_data[flt_idx] if data_type == 'event': flt_idx = [row for sbj in subject for row in self.event_index.get(sbj,[])] self.cache_result[k] = self.event_data[flt_idx] if data_type in ['entity', 'item', 'event']: return self.cache_result[k] raise ValueError("data_type must be in entity, item, event") def get_subject_view(self, subject: Group): new_group_view = GroupView(self, subject) self._group_view.append(new_group_view) return new_group_view def clear_cache(self): # clear the cache, so that this object can be garbage collected self.entity_or_none.cache_clear() # pylint: disable=no-member self.cache_result.clear() self.alive_agents.clear() while self._group_view: weakref.ref(self._group_view.pop()) # clear the cache # Wrapper around an iterable datastore class CachedProperty: def __init__(self, func): self.func = func # Allows the instance keys to be garbage collected # when they are no longer referenced elsewhere self.cache = weakref.WeakKeyDictionary() def __get__(self, instance, owner): if instance is None: return self if instance not in self.cache: self.cache[instance] = self.func(instance) return self.cache[instance] class ArrayView(ABC): def __init__(self, mapping, name: str, gs: GameState, subject: Group, arr: np.ndarray): self._mapping = mapping self._name = name self._gs = gs self._subject = subject self._hash = hash(subject) ^ hash(name) self._arr = arr self._cache = self._gs.cache_result def __len__(self): return len(self._arr) @abstractmethod def get_attribute(self, attr) -> np.ndarray: raise NotImplementedError def __getattr__(self, attr) -> np.ndarray: k = (self._hash, attr) if k in self._cache: return self._cache[k] v = object.__getattribute__(self, 'get_attribute')(attr) self._cache[k] = v return v class ItemView(ArrayView): def __init__(self, gs: GameState, subject: Group, arr: np.ndarray): super().__init__(ItemAttr, 'item', gs, subject, arr) self._mapping = ItemAttr def get_attribute(self, attr) -> np.ndarray: return self._arr[:, self._mapping[attr]] class EntityView(ArrayView): def __init__(self, gs: GameState, subject: Group, arr: np.ndarray): super().__init__(EntityAttr, 'entity', gs, subject, arr) def get_attribute(self, attr) -> np.ndarray: return self._arr[:, self._mapping[attr]] class EventView(ArrayView): def __init__(self, gs: GameState, subject: Group, arr: np.ndarray): super().__init__(EventAttr, 'event', gs, subject, arr) def get_attribute(self, attr) -> np.ndarray: assert hasattr(EventCode, attr), 'Invalid event code' arr = self._arr[np.in1d(self._arr[:, EventAttr['event']], getattr(EventCode, attr))] return EventCodeView(attr, self._gs, self._subject, arr) class TileView(ArrayView): def __init__(self, gs: GameState, subject: Group, arr: np.ndarray): super().__init__(TileAttr, 'tile', gs, subject, arr) def get_attribute(self, attr) -> np.ndarray: return [o[:, self._mapping[attr]] for o in self._arr] class EventCodeView(ArrayView): def __init__(self, name: str, gs: GameState, subject: Group, arr: np.ndarray): super().__init__(EventAttr, name, gs, subject, arr) def get_attribute(self, attr) -> np.ndarray: return self._arr[:, self._mapping[attr]] # Group class GroupObsView: def __init__(self, gs: GameState, subject: Group): self._gs = gs valid_agents = filter(lambda eid: eid in gs.env_obs,subject.agents) self._obs = [gs.env_obs[ent_id] for ent_id in valid_agents] self._subject = subject @CachedProperty def tile(self): return TileView(self._gs, self._subject, [o.tiles for o in self._obs]) def __getattr__(self, attr): return [getattr(o, attr) for o in self._obs] class GroupView: def __init__(self, gs: GameState, subject: Group): self._gs = gs self._subject = subject self._subject_hash = hash(subject) @CachedProperty def obs(self): return GroupObsView(self._gs, self._subject) @CachedProperty def _sbj_ent(self): return self._gs.where_in_id('entity', self._subject.agents) @CachedProperty def entity(self): return EntityView(self._gs, self._subject, self._sbj_ent) @CachedProperty def _sbj_item(self): return self._gs.where_in_id('item', self._subject.agents) @CachedProperty def item(self): return ItemView(self._gs, self._subject, self._sbj_item) @CachedProperty def _sbj_event(self): return self._gs.where_in_id('event', self._subject.agents) @CachedProperty def event(self): return EventView(self._gs, self._subject, self._sbj_event) def __getattribute__(self, attr): if attr in {'_gs','_subject','_sbj_ent','_sbj_item', 'entity','item','event','obs', '_subject_hash'}: return object.__getattribute__(self, attr) # Cached optimization k = (self._subject_hash, attr) cache = self._gs.cache_result if k in cache: return cache[k] try: # Get property if attr in EntityAttrKeys: v = getattr(self.entity, attr) else: v = object.__getattribute__(self, attr) cache[k] = v return v except AttributeError: # View behavior return object.__getattribute__(self._gs, attr) class GameStateGenerator: def __init__(self, realm: Realm, config: Config): self.config = deepcopy(config) self.spawn_pos: Dict[int, Tuple[int, int]] = {} for ent_id, ent in realm.players.items(): self.spawn_pos.update( {ent_id: ent.pos} ) def generate(self, realm: Realm, env_obs: Dict[int, Observation]) -> GameState: # copy the datastore, by running astype entity_all = EntityState.Query.table(realm.datastore).copy() alive_agents = entity_all[:, EntityAttr["id"]] alive_agents = set(alive_agents[alive_agents > 0]) item_data = ItemState.Query.table(realm.datastore).copy() event_data = EventState.Query.table(realm.datastore).copy() return GameState( current_tick = realm.tick, config = self.config, spawn_pos = self.spawn_pos, alive_agents = alive_agents, env_obs = env_obs, entity_data = entity_all, entity_index = precompute_index(entity_all, EntityAttr["id"]), item_data = item_data, item_index = precompute_index(item_data, ItemAttr["owner_id"]), event_data = event_data, event_index = precompute_index(event_data, EventAttr['ent_id']), seize_status = realm.seize_status, cache_result = {} ) def precompute_index(table, id_col): index = defaultdict() for row, id_ in enumerate(table[:,id_col]): if id_ in index: index[id_].append(row) else: index[id_] = [row] return index ================================================ FILE: nmmo/task/group.py ================================================ from __future__ import annotations from typing import Dict, Union, Iterable, TYPE_CHECKING from collections import OrderedDict from collections.abc import Set, Sequence import weakref if TYPE_CHECKING: from nmmo.task.game_state import GameState, GroupView class Group(Sequence, Set): ''' An immutable, ordered, unique group of agents involved in a task ''' def __init__(self, agents: Union(Iterable[int], int), name: str=None): if isinstance(agents, int): agents = (agents,) assert len(agents) > 0, "Team must have at least one agent" self.name = name if name else f"Agent({','.join([str(e) for e in agents])})" # Remove duplicates self._agents = tuple(OrderedDict.fromkeys(sorted(agents)).keys()) if not isinstance(self._agents,tuple): self._agents = (self._agents,) self._sd: GroupView = None self._gs: GameState = None self._hash = hash(self._agents) @property def agents(self): return self._agents def union(self, o: Group): return Group(self._agents + o.agents) def intersection(self, o: Group): return Group(set(self._agents).intersection(set(o.agents))) def __eq__(self, o): return self._agents == o def __len__(self): return len(self._agents) def __hash__(self): return self._hash def __getitem__(self, key): if len(self) == 1 and key == 0: return self return Group((self._agents[key],), f"{self.name}.{key}") def __contains__(self, key): if isinstance(key, int): return key in self.agents return Sequence.__contains__(self, key) def __str__(self) -> str: return str(self._agents) def __int__(self) -> int: assert len(self._agents) == 1, "Group is not a singleton" return int(self._agents[0]) def __copy__(self): return self def __deepcopy__(self, memo): return Group(self.agents, self.name) def description(self) -> Dict: return { "type": "Group", "name": self.name, "agents": self._agents } def clear_prev_state(self) -> None: if self._gs is not None: self._gs.clear_cache() # prevent memory leak self._gs = None if self._sd is not None: weakref.ref(self._sd) # prevent memory leak self._sd = None def update(self, gs: GameState) -> None: self.clear_prev_state() self._gs = gs self._sd = gs.get_subject_view(self) def __getattr__(self, attr): return self._sd.__getattribute__(attr) def union(*groups: Group) -> Group: """ Performs a big union over groups """ agents = [] for group in groups: for agent in group.agents: agents.append(agent) return Group(agents) def complement(group: Group, universe: Group) -> Group: """ Returns the complement of group in universe """ agents = [] for agent in universe.agents: if not agent in group: agents.append(agent) return Group(agents) ================================================ FILE: nmmo/task/predicate_api.py ================================================ from __future__ import annotations from typing import Callable, List, Optional, Union, Iterable, Type, TYPE_CHECKING from types import FunctionType from abc import ABC, abstractmethod import inspect from numbers import Real from nmmo.core.config import Config from nmmo.task.group import Group, union from nmmo.task.game_state import GameState if TYPE_CHECKING: from nmmo.task.task_api import Task class InvalidPredicateDefinition(Exception): pass class Predicate(ABC): """ A mapping from a game state to bounded [0, 1] float """ def __init__(self, subject: Group, *args, **kwargs): self.name = self._make_name(self.__class__.__name__, args, kwargs) self._groups: List[Group] = [x for x in list(args) + list(kwargs.values()) if isinstance(x, Group)] self._groups.append(subject) self._args = args self._kwargs = kwargs self._config = None self._subject = subject def __call__(self, gs: GameState) -> float: """ Calculates score Params: gs: GameState Returns: progress: float bounded between [0, 1], 1 is considered to be true """ # Update views for group in self._groups: group.update(gs) # Calculate score cache = gs.cache_result if self.name in cache: progress = cache[self.name] else: progress = max(min(float(self._evaluate(gs)),1.0),0.0) cache[self.name] = progress return progress def close(self): # To prevent memory leak, clear all refs to old game state for group in self._groups: group.clear_prev_state() @abstractmethod def _evaluate(self, gs: GameState) -> float: """ A mapping from a game state to the desirability/progress of that state. __call__() will cap its value to [0, 1] """ raise NotImplementedError def _make_name(self, class_name, args, kwargs) -> str: name = [class_name] + \ list(map(arg_to_string, args)) + \ [f"{arg_to_string(key)}:{arg_to_string(arg)}" for key, arg in kwargs.items()] name = "("+'_'.join(name).replace(' ', '')+")" return name def __str__(self): return self.name @abstractmethod def get_source_code(self) -> str: """ Returns the actual source code how the game state/progress evaluation is done. """ raise NotImplementedError @abstractmethod def get_signature(self) -> List: """ Returns the signature of the game state/progress evaluation function. """ raise NotImplementedError @property def args(self): return self._args @property def kwargs(self): return self._kwargs @property def subject(self): return self._subject def create_task(self, task_cls: Optional[Type[Task]]=None, assignee: Union[Iterable[int], int]=None, **kwargs) -> Task: """ Creates a task from this predicate""" if task_cls is None: from nmmo.task.task_api import Task task_cls = Task if assignee is None: # the new task is assigned to this predicate's subject assignee = self._subject.agents return task_cls(eval_fn=self, assignee=assignee, **kwargs) def __and__(self, other): return AND(self, other) def __or__(self, other): return OR(self, other) def __invert__(self): return NOT(self) def __add__(self, other): return ADD(self, other) def __radd__(self, other): return ADD(self, other) def __sub__(self, other): return SUB(self, other) def __rsub__(self, other): return SUB(self, other) def __mul__(self, other): return MUL(self, other) def __rmul__(self, other): return MUL(self, other) # _make_name helper functions def arg_to_string(arg): if isinstance(arg, (type, FunctionType)): # class or function return arg.__name__ if arg is None: return 'Any' return str(arg) ################################################ def make_predicate(fn: Callable) -> Type[Predicate]: """ Syntactic sugar API for defining predicates from function """ signature = inspect.signature(fn) for i, param in enumerate(signature.parameters.values()): if i == 0 and param.name != 'gs': raise InvalidPredicateDefinition('First parameter must be gs: GameState') if i == 1 and (param.name != 'subject'): raise InvalidPredicateDefinition("Second parameter must be subject: Group") class FunctionPredicate(Predicate): def __init__(self, *args, **kwargs) -> None: self._signature = signature super().__init__(*args, **kwargs) self._args = args self._kwargs = kwargs self.name = self._make_name(fn.__name__, args, kwargs) def _evaluate(self, gs: GameState) -> float: return float(fn(gs, *self._args, **self._kwargs)) def get_source_code(self): return inspect.getsource(fn).strip() def get_signature(self) -> List: return list(self._signature.parameters) return FunctionPredicate ################################################ class PredicateOperator(Predicate): def __init__(self, n, *predicates: Union[Predicate, Real], subject: Group=None): if not n(len(predicates)): raise InvalidPredicateDefinition(f"Need {n} arguments") predicates = list(predicates) self._subject_argument = subject if subject is None: subject = union(*[p.subject for p in filter(lambda p: isinstance(p, Predicate), predicates)]) super().__init__(subject, *predicates) for i, p in enumerate(predicates): if isinstance(p, Real): predicates[i] = lambda _,v=predicates[i] : v self._predicates = predicates def check(self, config: Config) -> bool: return all((p.check(config) if isinstance(p, Predicate) else True for p in self._predicates)) def sample(self, config: Config, cls: Type[PredicateOperator], **kwargs): subject = self._subject_argument if 'subject' not in kwargs else kwargs['subject'] predicates = [p.sample(config, **kwargs) if isinstance(p, Predicate) else p(None) for p in self._predicates] return cls(*predicates, subject=subject) def get_source_code(self) -> str: # NOTE: get_source_code() of the combined predicates returns the joined str # of each predicate's source code, which may NOT represent what the actual # predicate is doing # TODO: try to generate "the source code" that matches # what the actual instantiated predicate returns, # which perhaps should reflect the actual agent ids, etc... src_list = [] for pred in self._predicates: if isinstance(pred, Predicate): src_list.append(pred.get_source_code()) return '\n\n'.join(src_list).strip() def get_signature(self): # TODO: try to generate the correct signature return [] @property def args(self): # TODO: try to generate the correct args return [] @property def kwargs(self): # NOTE: This is incorrect implementation. kwargs of the combined predicates returns # all summed kwargs dict, which can OVERWRITE the values of duplicated keys # TODO: try to match the eval function and kwargs, which can be correctly used downstream # for pred in self._predicates: # if isinstance(pred, Predicate): # kwargs.update(pred.kwargs) return {} class OR(PredicateOperator, Predicate): def __init__(self, *predicates: Predicate, subject: Group=None): super().__init__(lambda n: n>0, *predicates, subject=subject) def _evaluate(self, gs: GameState) -> float: # using max as OR for the [0,1] float return max(p(gs) for p in self._predicates) def sample(self, config: Config, **kwargs): return super().sample(config, OR, **kwargs) class AND(PredicateOperator, Predicate): def __init__(self, *predicates: Predicate, subject: Group=None): super().__init__(lambda n: n>0, *predicates, subject=subject) def _evaluate(self, gs: GameState) -> float: # using min as AND for the [0,1] float return min(p(gs) for p in self._predicates) def sample(self, config: Config, **kwargs): return super().sample(config, AND, **kwargs) class NOT(PredicateOperator, Predicate): def __init__(self, predicate: Predicate, subject: Group=None): super().__init__(lambda n: n==1, predicate, subject=subject) def _evaluate(self, gs: GameState) -> float: return 1.0 - self._predicates[0](gs) def sample(self, config: Config, **kwargs): return super().sample(config, NOT, **kwargs) class ADD(PredicateOperator, Predicate): def __init__(self, *predicate: Union[Predicate, Real], subject: Group=None): super().__init__(lambda n: n>0, *predicate, subject=subject) def _evaluate(self, gs: GameState) -> float: return max(min(sum(p(gs) for p in self._predicates),1.0),0.0) def sample(self, config: Config, **kwargs): return super().sample(config, ADD, **kwargs) class SUB(PredicateOperator, Predicate): def __init__(self, p: Predicate, q: Union[Predicate, Real], subject: Group=None): super().__init__(lambda n: n==2, p,q, subject=subject) def _evaluate(self, gs: GameState) -> float: return max(min(self._predicates[0](gs)-self._predicates[1](gs),1.0),0.0) def sample(self, config: Config, **kwargs): return super().sample(config, SUB, **kwargs) class MUL(PredicateOperator, Predicate): def __init__(self, *predicate: Union[Predicate, Real], subject: Group=None): super().__init__(lambda n: n>0, *predicate, subject=subject) def _evaluate(self, gs: GameState) -> float: result = 1.0 for p in self._predicates: result = result * p(gs) return max(min(result,1.0),0.0) def sample(self, config: Config, **kwargs): return super().sample(config, MUL, **kwargs) ================================================ FILE: nmmo/task/task_api.py ================================================ # pylint: disable=unused-import,attribute-defined-outside-init from typing import Callable, Iterable, Dict, List, Union, Tuple, Type from types import FunctionType from abc import ABC import inspect import numpy as np from nmmo.task.group import Group from nmmo.task.game_state import GameState from nmmo.task.predicate_api import Predicate, make_predicate, arg_to_string from nmmo.task import base_predicates as bp class Task(ABC): """ A task is used to calculate rewards for agents in assignee based on the predicate and game state """ def __init__(self, eval_fn: Callable, assignee: Union[Iterable[int], int], reward_multiplier = 1.0, embedding = None, spec_name: str = None, reward_to = None, tags: List[str] = None): if isinstance(assignee, int): self._assignee = (assignee,) else: assert len(assignee) > 0, "Assignee cannot be empty" self._assignee = tuple(set(assignee)) # dedup self._eval_fn = eval_fn self._reward_multiplier = reward_multiplier self._embedding = None if embedding is None else np.array(embedding, dtype=np.float16) # These are None if not created using TaskSpec self.spec_name, self.reward_to, self.tags = spec_name, reward_to, tags self.name = self._make_name(self.__class__.__name__, eval_fn=eval_fn, assignee=self._assignee) self.reset() def reset(self): self._stop_eval = False self._last_eval_tick = None self._progress = 0.0 self._completed_tick = None self._max_progress = 0.0 self._positive_reward_count = 0 self._negative_reward_count = 0 def close(self): if self._stop_eval is False: if isinstance(self._eval_fn, Predicate): self._eval_fn.close() self._stop_eval = True @property def assignee(self) -> Tuple[int]: return self._assignee @property def completed(self) -> bool: return self._completed_tick is not None @property def progress(self) -> float: return self._progress @property def reward_multiplier(self) -> float: return self._reward_multiplier @property def reward_signal_count(self) -> int: return self._positive_reward_count + self._negative_reward_count @property def embedding(self): return self._embedding def set_embedding(self, embedding): self._embedding = embedding def _map_progress_to_reward(self, gs: GameState) -> float: """ The default reward is the diff between the old and new progress. Once the task is completed, no more reward is provided. Override this function to create a custom reward function """ if self.completed: return 0.0 new_progress = max(min(float(self._eval_fn(gs)),1.0),0.0) diff = new_progress - self._progress self._progress = new_progress if self._progress >= 1: self._completed_tick = gs.current_tick return diff def compute_rewards(self, gs: GameState) -> Tuple[Dict[int, float], Dict[int, Dict]]: """ Environment facing API Returns rewards and infos for all agents in subject """ reward = self._map_progress_to_reward(gs) * self._reward_multiplier self._last_eval_tick = gs.current_tick self._max_progress = max(self._max_progress, self._progress) self._positive_reward_count += int(reward > 0) self._negative_reward_count += int(reward < 0) rewards = {int(ent_id): reward for ent_id in self._assignee} infos = {int(ent_id): {"task_spec": self.spec_name, "reward": reward, "progress": self._progress, "completed": self.completed} for ent_id in self._assignee} # NOTE: tasks do not know whether assignee agents are alive or dead # so the Env must check it before filling in rewards and infos return rewards, infos def _make_name(self, class_name, **kwargs) -> str: name = [class_name] + \ [f"{arg_to_string(key)}:{arg_to_string(arg)}" for key, arg in kwargs.items()] name = "("+"_".join(name).replace(" ", "")+")" return name def __str__(self): return self.name @property def subject(self): if isinstance(self._eval_fn, Predicate): return self._eval_fn.subject.agents return self.assignee def get_source_code(self): if isinstance(self._eval_fn, Predicate): return self._eval_fn.get_source_code() return inspect.getsource(self._eval_fn).strip() def get_signature(self): if isinstance(self._eval_fn, Predicate): return self._eval_fn.get_signature() signature = inspect.signature(self._eval_fn) return list(signature.parameters) @property def args(self): if isinstance(self._eval_fn, Predicate): return self._eval_fn.args # the function _eval_fn must only take gs return [] @property def kwargs(self): if isinstance(self._eval_fn, Predicate): return self._eval_fn.kwargs # the function _eval_fn must only take gs return {} @property def progress_info(self): return { "task_spec_name": self.spec_name, "last_eval_tick": self._last_eval_tick, "completed": self.completed, "completed_tick": self._completed_tick, "max_progress": self._max_progress, "positive_reward_count": self._positive_reward_count, "negative_reward_count": self._negative_reward_count, "reward_signal_count": self.reward_signal_count, } class OngoingTask(Task): def _map_progress_to_reward(self, gs: GameState) -> float: """Keep returning the progress reward after the task is completed. However, this task tracks the completion status in the same manner. """ self._progress = max(min(self._eval_fn(gs)*1.0,1.0),0.0) if self._progress >= 1 and self._completed_tick is None: self._completed_tick = gs.current_tick return self._progress class HoldDurationTask(Task): def __init__(self, eval_fn: Callable, assignee: Union[Iterable[int], int], hold_duration: int, **kwargs): super().__init__(eval_fn, assignee, **kwargs) self._hold_duration = hold_duration self._reset_timer() def _reset_timer(self): self._timer = 0 self._last_success_tick = 0 def reset(self): super().reset() self._reset_timer() def _map_progress_to_reward(self, gs: GameState) -> float: # pylint: disable=attribute-defined-outside-init if self.completed: return 0.0 curr_eval = max(min(self._eval_fn(gs)*1.0,1.0),0.0) if curr_eval < 1: self._reset_timer() else: self._timer += 1 self._last_success_tick = gs.current_tick new_progress = self._timer / self._hold_duration diff = new_progress - self._progress self._progress = new_progress if self._progress >= 1 and self._completed_tick is None: self._completed_tick = gs.current_tick diff = 1.0 # give out the max reward when task is completed return diff ###################################################################### # The same task is assigned each agent in agent_list individually # with the agent as the predicate subject and task assignee def make_same_task(pred_cls: Union[Type[Predicate], Callable], agent_list: Iterable[int], pred_kwargs=None, task_cls: Type[Task]=Task, task_kwargs=None) -> List[Task]: # if a function is provided, make it a predicate class if isinstance(pred_cls, FunctionType): pred_cls = make_predicate(pred_cls) if pred_kwargs is None: pred_kwargs = {} if task_kwargs is None: task_kwargs = {} task_list = [] for agent_id in agent_list: predicate = pred_cls(Group(agent_id), **pred_kwargs) task_list.append(predicate.create_task(task_cls=task_cls, **task_kwargs)) return task_list def nmmo_default_task(agent_list: Iterable[int], test_mode=None) -> List[Task]: # (almost) no overhead in env._compute_rewards() if test_mode == "no_task": return [] # eval function on Predicate class, but does not use Group during eval if test_mode == "dummy_eval_fn": # pylint: disable=unused-argument return make_same_task(lambda gs, subject: True, agent_list, task_cls=OngoingTask) return make_same_task(bp.TickGE, agent_list) ================================================ FILE: nmmo/task/task_spec.py ================================================ import functools from dataclasses import dataclass, field from typing import Iterable, Dict, List, Union, Type from types import FunctionType from copy import deepcopy from tqdm import tqdm import numpy as np import nmmo from nmmo.task.task_api import Task, make_same_task from nmmo.task.predicate_api import Predicate, make_predicate from nmmo.task.group import Group from nmmo.task import base_predicates as bp from nmmo.lib.team_helper import TeamHelper """ task_spec eval_fn can come from the base_predicates.py or could be custom functions like above eval_fn_kwargs are the additional args that go into predicate. There are also special keys * "target" must be ["left_team", "right_team", "left_team_leader", "right_team_leader"] these str will be translated into the actual agent ids task_cls specifies the task class to be used. Default is Task. task_kwargs are the optional, additional args that go into the task. reward_to: must be in ["team", "agent"] * "team" create a single team task, in which all team members get rewarded * "agent" create a task for each agent, in which only the agent gets rewarded sampling_weight specifies the weight of the task in the curriculum sampling. Default is 1 """ REWARD_TO = ["agent", "team"] VALID_TARGET = ["left_team", "left_team_leader", "right_team", "right_team_leader", "my_team_leader", "all_foes", "all_foe_leaders"] @dataclass class TaskSpec: eval_fn: FunctionType eval_fn_kwargs: Dict task_cls: Type[Task] = Task task_kwargs: Dict = field(default_factory=dict) reward_to: str = "agent" sampling_weight: float = 1.0 embedding: np.ndarray = None predicate: Predicate = None tags: List[str] = field(default_factory=list) def __post_init__(self): if self.predicate is None: assert isinstance(self.eval_fn, FunctionType), \ "eval_fn must be a function" else: assert self.eval_fn is None, "Cannot specify both eval_fn and predicate" assert self.reward_to in REWARD_TO, \ f"reward_to must be in {REWARD_TO}" if "target" in self.eval_fn_kwargs: assert self.eval_fn_kwargs["target"] in VALID_TARGET, \ f"target must be in {VALID_TARGET}" @functools.cached_property def name(self): # pylint: disable=no-member kwargs_str = [] for key, val in self.eval_fn_kwargs.items(): val_str = str(val) if isinstance(val, type): val_str = val.__name__ kwargs_str.append(f"{key}:{val_str}_") kwargs_str = "(" + "".join(kwargs_str)[:-1] + ")" # remove the last _ pred_name = self.eval_fn.__name__ if self.predicate is None else self.predicate.name return "_".join([self.task_cls.__name__, pred_name, kwargs_str, "reward_to:" + self.reward_to]) def make_task_from_spec(assign_to: Union[Iterable[int], Dict], task_spec: List[TaskSpec]) -> List[Task]: """ Args: assign_to: either a Dict with { team_id: [agent_id]} or a List of agent ids task_spec: a list of tuples (reward_to, eval_fn, pred_fn_kwargs, task_kwargs) each tuple is assigned to the teams """ teams = assign_to if not isinstance(teams, Dict): # convert agent id list to the team dict format teams = {idx: [agent_id] for idx, agent_id in enumerate(assign_to)} team_list = list(teams.keys()) team_helper = TeamHelper(teams) # assign task spec to teams (assign_to) tasks = [] for idx in range(min(len(team_list), len(task_spec))): team_id = team_list[idx] # map local vars to spec attributes reward_to = task_spec[idx].reward_to pred_fn = task_spec[idx].eval_fn pred_fn_kwargs = deepcopy(task_spec[idx].eval_fn_kwargs) task_cls = task_spec[idx].task_cls task_kwargs = deepcopy(task_spec[idx].task_kwargs) task_kwargs["embedding"] = task_spec[idx].embedding # to pass to task_cls task_kwargs["spec_name"] = task_spec[idx].name task_kwargs["reward_to"] = task_spec[idx].reward_to task_kwargs["tags"] = task_spec[idx].tags predicate = task_spec[idx].predicate # reserve "target" for relative agent mapping target_keys = [key for key in pred_fn_kwargs.keys() if key.startswith("target")] for key in target_keys: target_keyword = pred_fn_kwargs.pop(key) assert target_keyword in VALID_TARGET, "Invalid target" # translate target to specific agent ids using team_helper target_ent = team_helper.get_target_agent(team_id, target_keyword) pred_fn_kwargs[key] = target_ent # handle some special cases and instantiate the predicate first if pred_fn is not None and isinstance(pred_fn, FunctionType): # if a function is provided as a predicate pred_cls = make_predicate(pred_fn) # TODO: should create a test for these if (pred_fn in [bp.AllDead]) or \ (pred_fn in [bp.StayAlive] and "target" in pred_fn_kwargs): # use the target as the predicate subject target_ent = pred_fn_kwargs.pop("target") # remove target predicate = pred_cls(Group(target_ent), **pred_fn_kwargs) # create the task if reward_to == "team": assignee = team_helper.teams[team_id] if predicate is None: predicate = pred_cls(Group(assignee), **pred_fn_kwargs) tasks.append(predicate.create_task(task_cls=task_cls, **task_kwargs)) else: # this branch is for the cases like AllDead, StayAlive tasks.append(predicate.create_task(assignee=assignee, task_cls=task_cls, **task_kwargs)) elif reward_to == "agent": agent_list = team_helper.teams[team_id] if predicate is None: tasks += make_same_task(pred_cls, agent_list, pred_kwargs=pred_fn_kwargs, task_cls=task_cls, task_kwargs=task_kwargs) else: # this branch is for the cases like AllDead, StayAlive tasks += [predicate.create_task(assignee=agent_id, task_cls=task_cls, **task_kwargs) for agent_id in agent_list] return tasks # pylint: disable=bare-except,cell-var-from-loop def check_task_spec(spec_list: List[TaskSpec], debug=False) -> List[Dict]: teams = {0: [1, 2, 3], 3: [4, 5], 7: [6, 7], 11: [8, 9], 14: [10, 11]} config = nmmo.config.Default() config.set("PLAYER_N", 11) config.set("TEAMS", teams) env = nmmo.Env(config) results = [] for single_spec in tqdm(spec_list): result = {"spec_name": single_spec.name} try: env.reset(make_task_fn=lambda: make_task_from_spec(teams, [single_spec])) for _ in range(3): env.step({}) result["runnable"] = True except: result["runnable"] = False if debug: raise results.append(result) return results ================================================ FILE: nmmo/version.py ================================================ __version__ = '2.1.2' ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools", "wheel", "cython", "numpy==1.23.3"] ================================================ FILE: scripted/__init__.py ================================================ ================================================ FILE: scripted/attack.py ================================================ # pylint: disable=invalid-name, unused-argument import numpy as np import nmmo from nmmo.core.observation import Observation from nmmo.entity.entity import EntityState from nmmo.lib import utils def closestTarget(config, ob: Observation): shortestDist = np.inf closestAgent = None agent = ob.agent start = (agent.row, agent.col) for target_ent in ob.entities.values: target_ent = EntityState.parse_array(target_ent) if target_ent.id == agent.id: continue dist = utils.linf_single(start, (target_ent.row, target_ent.col)) if dist < shortestDist and dist != 0: shortestDist = dist closestAgent = target_ent if closestAgent is None: return None, None return closestAgent, shortestDist def attacker(config, ob: Observation): agent = ob.agent attacker_id = agent.attacker_id if attacker_id == 0: return None, None target_ent = ob.entity(attacker_id) if target_ent is None: return None, None return target_ent,\ utils.linf_single((agent.row, agent.col), (target_ent.row, target_ent.col)) def target(config, actions, style, targetID): actions[nmmo.action.Attack] = { nmmo.action.Style: style, nmmo.action.Target: targetID} ================================================ FILE: scripted/baselines.py ================================================ # pylint: disable=invalid-name, attribute-defined-outside-init, no-member from typing import Dict from collections import defaultdict import nmmo from nmmo import material from nmmo.systems import skill import nmmo.systems.item as item_system from nmmo.lib import colors from nmmo.core import action from nmmo.core.observation import Observation from scripted import attack, move class Scripted(nmmo.Scripted): '''Template class for baseline scripted models. You may either subclass directly or mirror the __call__ function''' scripted = True color = colors.Neon.SKY def __init__(self, config, idx): ''' Args: config : A forge.blade.core.Config object or subclass object ''' super().__init__(config, idx) self.health_max = config.PLAYER_BASE_HEALTH if config.RESOURCE_SYSTEM_ENABLED: self.food_max = config.RESOURCE_BASE self.water_max = config.RESOURCE_BASE self.spawnR = None self.spawnC = None @property def policy(self): return self.__class__.__name__ @property def forage_criterion(self) -> bool: '''Return true if low on food or water''' min_level = 7 * self.config.RESOURCE_DEPLETION_RATE return self.me.food <= min_level or self.me.water <= min_level def forage(self): '''Min/max food and water using Dijkstra's algorithm''' # TODO: do not access realm._np_random directly. ALSO see below for all other uses move.forageDijkstra(self.config, self.ob, self.actions, self.food_max, self.water_max, self._np_random) def gather(self, resource): '''BFS search for a particular resource''' return move.gatherBFS(self.config, self.ob, self.actions, resource, self._np_random) def explore(self): '''Route away from spawn''' move.explore(self.config, self.ob, self.actions, self.me.row, self.me.col, self._np_random) @property def downtime(self): '''Return true if agent is not occupied with a high-priority action''' return not self.forage_criterion and self.attacker is None def evade(self): '''Target and path away from an attacker''' move.evade(self.config, self.ob, self.actions, self.attacker, self._np_random) self.target = self.attacker self.targetID = self.attackerID self.targetDist = self.attackerDist def attack(self): '''Attack the current target''' if self.target is not None: assert self.targetID is not None style = self._np_random.choice(self.style) attack.target(self.config, self.actions, style, self.targetID) def target_weak(self): # pylint: disable=inconsistent-return-statements '''Target the nearest agent if it is weak''' if self.closest is None: return False selfLevel = self.me.level targLevel = max(self.closest.melee_level, self.closest.range_level, self.closest.mage_level) if self.closest.npc_type == 1 or \ targLevel <= selfLevel <= 5 or \ selfLevel >= targLevel + 3: self.target = self.closest self.targetID = self.closestID self.targetDist = self.closestDist def scan_agents(self): '''Scan the nearby area for agents''' self.closest, self.closestDist = attack.closestTarget(self.config, self.ob) self.attacker, self.attackerDist = attack.attacker(self.config, self.ob) self.closestID = None if self.closest is not None: self.closestID = self.ob.entities.index(self.closest.id) self.attackerID = None if self.attacker is not None: self.attackerID = self.ob.entities.index(self.attacker.id) self.target = None self.targetID = None self.targetDist = None def adaptive_control_and_targeting(self, explore=True): '''Balanced foraging, evasion, and exploration''' self.scan_agents() if self.attacker is not None: self.evade() return if self.fog_criterion: self.explore() elif self.forage_criterion or not explore: self.forage() else: self.explore() self.target_weak() def process_inventory(self): if not self.config.ITEM_SYSTEM_ENABLED: return self.inventory = {} self.best_items: Dict = {} self.item_counts = defaultdict(int) self.item_levels = { item_system.Hat.ITEM_TYPE_ID: self.level, item_system.Top.ITEM_TYPE_ID: self.level, item_system.Bottom.ITEM_TYPE_ID: self.level, item_system.Spear.ITEM_TYPE_ID: self.me.melee_level, item_system.Bow.ITEM_TYPE_ID: self.me.range_level, item_system.Wand.ITEM_TYPE_ID: self.me.mage_level, item_system.Rod.ITEM_TYPE_ID: self.me.fishing_level, item_system.Gloves.ITEM_TYPE_ID: self.me.herbalism_level, item_system.Pickaxe.ITEM_TYPE_ID: self.me.prospecting_level, item_system.Axe.ITEM_TYPE_ID: self.me.carving_level, item_system.Chisel.ITEM_TYPE_ID: self.me.alchemy_level, item_system.Whetstone.ITEM_TYPE_ID: self.me.melee_level, item_system.Arrow.ITEM_TYPE_ID: self.me.range_level, item_system.Runes.ITEM_TYPE_ID: self.me.mage_level, item_system.Ration.ITEM_TYPE_ID: self.level, item_system.Potion.ITEM_TYPE_ID: self.level } for item_ary in self.ob.inventory.values: itm = item_system.ItemState.parse_array(item_ary) assert itm.quantity != 0 # Too high level to equip or use if itm.type_id in self.item_levels and itm.level > self.item_levels[itm.type_id]: continue # cannot use listed item if itm.listed_price: continue self.item_counts[itm.type_id] += itm.quantity self.inventory[itm.id] = itm # Best by default if itm.type_id not in self.best_items: self.best_items[itm.type_id] = itm best_itm = self.best_items[itm.type_id] if itm.level > best_itm.level: self.best_items[itm.type_id] = itm def upgrade_heuristic(self, current_level, upgrade_level, price): return (upgrade_level - current_level) / max(price, 1) def process_market(self): if not self.config.EXCHANGE_SYSTEM_ENABLED: return self.market = {} self.best_heuristic = {} for item_ary in self.ob.market.values: itm = item_system.ItemState.parse_array(item_ary) self.market[itm.id] = itm # Prune Unaffordable if itm.listed_price > self.me.gold: continue # Too high level to equip if itm.type_id in self.item_levels and itm.level > self.item_levels[itm.type_id] : continue #Current best item level current_level = 0 if itm.type_id in self.best_items: current_level = self.best_items[itm.type_id].level itm.heuristic = self.upgrade_heuristic(current_level, itm.level, itm.listed_price) #Always count first item if itm.type_id not in self.best_heuristic: self.best_heuristic[itm.type_id] = itm continue #Better heuristic value if itm.heuristic > self.best_heuristic[itm.type_id].heuristic: self.best_heuristic[itm.type_id] = itm def equip(self, items: set): for type_id, itm in self.best_items.items(): if type_id not in items: continue if itm.equipped or itm.listed_price: continue # InventoryItem needs where the item is (index) in the inventory self.actions[action.Use] = { action.InventoryItem: self.ob.inventory.index(itm.id)} return True def consume(self): if self.me.health <= self.health_max // 2 \ and item_system.Potion.ITEM_TYPE_ID in self.best_items: itm = self.best_items[item_system.Potion.ITEM_TYPE_ID] elif (self.me.food == 0 or self.me.water == 0) \ and item_system.Ration.ITEM_TYPE_ID in self.best_items: itm = self.best_items[item_system.Ration.ITEM_TYPE_ID] else: return if itm.listed_price: return # InventoryItem needs where the item is (index) in the inventory self.actions[action.Use] = { action.InventoryItem: self.ob.inventory.index(itm.id)} def sell(self, keep_k: dict, keep_best: set): for itm in self.inventory.values(): price = int(max(itm.level, 1)) assert itm.quantity > 0 if itm.equipped or itm.listed_price: continue if itm.type_id in keep_k: owned = self.item_counts[itm.type_id] k = keep_k[itm.type_id] if owned <= k: continue #Exists an equippable of the current class, best needs to be kept, and this is the best item if itm.type_id in self.best_items and \ itm.type_id in keep_best and \ itm.id == self.best_items[itm.type_id].id: continue self.actions[action.Sell] = { action.InventoryItem: self.ob.inventory.index(itm.id), action.Price: action.Price.index(price) } return itm def buy(self, buy_k: dict, buy_upgrade: set): if len(self.inventory) >= self.config.ITEM_INVENTORY_CAPACITY: return purchase = None best = list(self.best_heuristic.items()) self._np_random.shuffle(best) for type_id, itm in best: # Buy top k if type_id in buy_k: owned = self.item_counts[type_id] k = buy_k[type_id] if owned < k: purchase = itm # Check if item desired and upgrade elif type_id in buy_upgrade and itm.heuristic > 0: purchase = itm # Buy best heuristic upgrade if purchase: self.actions[action.Buy] = { action.MarketItem: self.ob.market.index(purchase.id)} return def exchange(self): if not self.config.EXCHANGE_SYSTEM_ENABLED: return self.process_market() self.sell(keep_k=self.supplies, keep_best=self.wishlist) self.buy(buy_k=self.supplies, buy_upgrade=self.wishlist) def use(self): self.process_inventory() if self.config.EQUIPMENT_SYSTEM_ENABLED and not self.consume(): self.equip(items=self.wishlist) def __call__(self, observation: Observation): '''Process observations and return actions''' assert self._np_random is not None, "Agent's RNG must be set." self.actions = {} self.ob = observation self.me = observation.agent # combat level self.me.level = max(self.me.melee_level, self.me.range_level, self.me.mage_level) self.skills = { skill.Melee: self.me.melee_level, skill.Range: self.me.range_level, skill.Mage: self.me.mage_level, skill.Fishing: self.me.fishing_level, skill.Herbalism: self.me.herbalism_level, skill.Prospecting: self.me.prospecting_level, skill.Carving: self.me.carving_level, skill.Alchemy: self.me.alchemy_level } # TODO(kywch): need a consistent level variables # level for using armor, rations, and potion self.level = min(1, max(self.skills.values())) if self.spawnR is None: self.spawnR = self.me.row if self.spawnC is None: self.spawnC = self.me.col # When to run from death fog in BR configs self.fog_criterion = None if self.config.DEATH_FOG_ONSET is not None: time_alive = self.me.time_alive start_running = time_alive > self.config.DEATH_FOG_ONSET - 64 run_now = time_alive % max(1, int(1 / self.config.DEATH_FOG_SPEED)) self.fog_criterion = start_running and run_now class Sleeper(Scripted): '''Do Nothing''' def __call__(self, obs): super().__call__(obs) return {} class Random(Scripted): '''Moves randomly''' def __call__(self, obs): super().__call__(obs) move.rand(self.config, self.ob, self.actions, self._np_random) return self.actions class Meander(Scripted): '''Moves randomly on safe terrain''' def __call__(self, obs): super().__call__(obs) move.meander(self.config, self.ob, self.actions, self._np_random) return self.actions class Explore(Scripted): '''Actively explores towards the center''' def __call__(self, obs): super().__call__(obs) self.explore() return self.actions class Forage(Scripted): '''Forages using Dijkstra's algorithm and actively explores''' def __call__(self, obs): super().__call__(obs) if self.forage_criterion: self.forage() else: self.explore() return self.actions class Combat(Scripted): '''Forages, fights, and explores''' def __init__(self, config, idx): super().__init__(config, idx) self.style = [action.Melee, action.Range, action.Mage] @property def supplies(self): return { item_system.Ration.ITEM_TYPE_ID: 2, item_system.Potion.ITEM_TYPE_ID: 2, self.ammo.ITEM_TYPE_ID: 10 } @property def wishlist(self): return { item_system.Hat.ITEM_TYPE_ID, item_system.Top.ITEM_TYPE_ID, item_system.Bottom.ITEM_TYPE_ID, self.weapon.ITEM_TYPE_ID, self.ammo.ITEM_TYPE_ID } def __call__(self, obs): super().__call__(obs) self.use() self.exchange() self.adaptive_control_and_targeting() self.attack() return self.actions class Gather(Scripted): '''Forages, fights, and explores''' def __init__(self, config, idx): super().__init__(config, idx) self.resource = [material.Fish, material.Herb, material.Ore, material.Tree, material.Crystal] @property def supplies(self): return { item_system.Ration.ITEM_TYPE_ID: 1, item_system.Potion.ITEM_TYPE_ID: 1 } @property def wishlist(self): return { item_system.Hat.ITEM_TYPE_ID, item_system.Top.ITEM_TYPE_ID, item_system.Bottom.ITEM_TYPE_ID, self.tool.ITEM_TYPE_ID } def __call__(self, obs): super().__call__(obs) self.use() self.exchange() if self.forage_criterion: self.forage() elif self.fog_criterion or not self.gather(self.resource): self.explore() return self.actions class Fisher(Gather): def __init__(self, config, idx): super().__init__(config, idx) if config.PROFESSION_SYSTEM_ENABLED: self.resource = [material.Fish] self.tool = item_system.Rod class Herbalist(Gather): def __init__(self, config, idx): super().__init__(config, idx) if config.PROFESSION_SYSTEM_ENABLED: self.resource = [material.Herb] self.tool = item_system.Gloves class Prospector(Gather): def __init__(self, config, idx): super().__init__(config, idx) if config.PROFESSION_SYSTEM_ENABLED: self.resource = [material.Ore] self.tool = item_system.Pickaxe class Carver(Gather): def __init__(self, config, idx): super().__init__(config, idx) if config.PROFESSION_SYSTEM_ENABLED: self.resource = [material.Tree] self.tool = item_system.Axe class Alchemist(Gather): def __init__(self, config, idx): super().__init__(config, idx) if config.PROFESSION_SYSTEM_ENABLED: self.resource = [material.Crystal] self.tool = item_system.Chisel class Melee(Combat): def __init__(self, config, idx): super().__init__(config, idx) if config.COMBAT_SYSTEM_ENABLED: self.style = [action.Melee] self.weapon = item_system.Spear self.ammo = item_system.Whetstone class Range(Combat): def __init__(self, config, idx): super().__init__(config, idx) if config.COMBAT_SYSTEM_ENABLED: self.style = [action.Range] self.weapon = item_system.Bow self.ammo = item_system.Arrow class Mage(Combat): def __init__(self, config, idx): super().__init__(config, idx) if config.COMBAT_SYSTEM_ENABLED: self.style = [action.Mage] self.weapon = item_system.Wand self.ammo = item_system.Runes ================================================ FILE: scripted/move.py ================================================ # pylint: disable=invalid-name, unused-argument import heapq import numpy as np from nmmo.core import action from nmmo.core.observation import Observation from nmmo.lib import material, astar def inSight(dr, dc, vision): return (-vision <= dr <= vision and -vision <= dc <= vision) def rand(config, ob, actions, np_random): direction = np_random.choice(action.Direction.edges) actions[action.Move] = {action.Direction: direction} def towards(direction, np_random): if direction == (-1, 0): return action.North if direction == (1, 0): return action.South if direction == (0, -1): return action.West if direction == (0, 1): return action.East return np_random.choice(action.Direction.edges) def pathfind(config, ob, actions, rr, cc, np_random): direction = aStar(config, ob, actions, rr, cc) direction = towards(direction, np_random) actions[action.Move] = {action.Direction: direction} def meander(config, ob, actions, np_random): cands = [] if ob.tile(-1, 0).material_id in material.Habitable.indices: cands.append((-1, 0)) if ob.tile(1, 0).material_id in material.Habitable.indices: cands.append((1, 0)) if ob.tile(0, -1).material_id in material.Habitable.indices: cands.append((0, -1)) if ob.tile(0, 1).material_id in material.Habitable.indices: cands.append((0, 1)) if len(cands) > 0: direction = np_random.choices(cands)[0] direction = towards(direction, np_random) actions[action.Move] = {action.Direction: direction} def explore(config, ob, actions, r, c, np_random): vision = config.PLAYER_VISION_RADIUS sz = config.MAP_SIZE centR, centC = sz//2, sz//2 vR, vC = centR-r, centC-c mmag = max(1, abs(vR), abs(vC)) rr = int(np.round(vision*vR/mmag)) cc = int(np.round(vision*vC/mmag)) pathfind(config, ob, actions, rr, cc, np_random) def evade(config, ob: Observation, actions, attacker, np_random): agent = ob.agent rr, cc = (2*agent.row - attacker.row, 2*agent.col - attacker.col) pathfind(config, ob, actions, rr, cc, np_random) def forageDijkstra(config, ob: Observation, actions, food_max, water_max, np_random, cutoff=100): vision = config.PLAYER_VISION_RADIUS agent = ob.agent food = agent.food water = agent.water best = -1000 start = (0, 0) goal = (0, 0) reward = {start: (food, water)} backtrace = {start: None} queue = [start] while queue: cutoff -= 1 if cutoff <= 0: break cur = queue.pop(0) for nxt in astar.adjacentPos(cur): if nxt in backtrace: continue if not inSight(*nxt, vision): continue tile = ob.tile(*nxt) matl = tile.material_id if not matl in material.Habitable.indices: continue food, water = reward[cur] water = max(0, water - 1) food = max(0, food - 1) if matl == material.Foilage.index: food = min(food+food_max//2, food_max) for pos in astar.adjacentPos(nxt): if not inSight(*pos, vision): continue tile = ob.tile(*pos) matl = tile.material_id if matl == material.Water.index: water = min(water+water_max//2, water_max) break reward[nxt] = (food, water) total = min(food, water) if total > best \ or (total == best and max(food, water) > max(reward[goal])): best = total goal = nxt queue.append(nxt) backtrace[nxt] = cur while goal in backtrace and backtrace[goal] != start: goal = backtrace[goal] direction = towards(goal, np_random) actions[action.Move] = {action.Direction: direction} def findResource(config, ob: Observation, resource): vision = config.PLAYER_VISION_RADIUS resource_index = resource.index for r in range(-vision, vision+1): for c in range(-vision, vision+1): tile = ob.tile(r, c) material_id = tile.material_id if material_id == resource_index: return (r, c) return False def gatherAStar(config, ob, actions, resource, np_random, cutoff=100): resource_pos = findResource(config, ob, resource) if not resource_pos: return False rr, cc = resource_pos next_pos = aStar(config, ob, actions, rr, cc, cutoff=cutoff) if not next_pos or next_pos == (0, 0): return False direction = towards(next_pos, np_random) actions[action.Move] = {action.Direction: direction} return True def gatherBFS(config, ob: Observation, actions, resource, np_random, cutoff=100): vision = config.PLAYER_VISION_RADIUS start = (0, 0) backtrace = {start: None} queue = [start] found = False while queue: cutoff -= 1 if cutoff <= 0: return False cur = queue.pop(0) for nxt in astar.adjacentPos(cur): if found: break if nxt in backtrace: continue if not inSight(*nxt, vision): continue tile = ob.tile(*nxt) matl = tile.material_id if material.Fish in resource and material.Fish.index == matl: found = nxt backtrace[nxt] = cur break if not tile.material_id in material.Habitable.indices: continue if matl in (e.index for e in resource): found = nxt backtrace[nxt] = cur break for pos in astar.adjacentPos(nxt): if not inSight(*pos, vision): continue tile = ob.tile(*pos) matl = tile.material_id if matl == material.Fish.index: backtrace[nxt] = cur break queue.append(nxt) backtrace[nxt] = cur #Ran out of tiles if not found: return False while found in backtrace and backtrace[found] != start: found = backtrace[found] direction = towards(found, np_random) actions[action.Move] = {action.Direction: direction} return True def aStar(config, ob: Observation, actions, rr, cc, cutoff=100): vision = config.PLAYER_VISION_RADIUS start = (0, 0) goal = (rr, cc) if start == goal: return (0, 0) pq = [(0, start)] backtrace = {} cost = {start: 0} closestPos = start closestHeuristic = astar.l1(start, goal) closestCost = closestHeuristic while pq: # Use approximate solution if budget exhausted cutoff -= 1 if cutoff <= 0: if goal not in backtrace: goal = closestPos break priority, cur = heapq.heappop(pq) if cur == goal: break for nxt in astar.adjacentPos(cur): if not inSight(*nxt, vision): continue tile = ob.tile(*nxt) matl = tile.material_id if not matl in material.Habitable.indices: continue #Omitted water from the original implementation. Seems key if matl in material.Impassible.indices: continue newCost = cost[cur] + 1 if nxt not in cost or newCost < cost[nxt]: cost[nxt] = newCost heuristic = astar.l1(goal, nxt) priority = newCost + heuristic # Compute approximate solution if heuristic < closestHeuristic \ or (heuristic == closestHeuristic and priority < closestCost): closestPos = nxt closestHeuristic = heuristic closestCost = priority heapq.heappush(pq, (priority, nxt)) backtrace[nxt] = cur goal = closestPos while goal in backtrace and backtrace[goal] != start: goal = backtrace[goal] return goal ================================================ FILE: setup.py ================================================ from itertools import chain from setuptools import find_packages, setup from Cython.Build import cythonize import numpy as np REPO_URL = "https://github.com/neuralmmo/environment" extra = { 'docs': [ 'sphinx==5.0.0', 'sphinx-rtd-theme==0.5.1', 'sphinxcontrib-youtube==1.0.1', 'myst-parser==1.0.0', 'sphinx-rtd-theme==0.5.1', 'sphinx-design==0.4.1', 'furo==2023.3.27', ], } extra['all'] = list(set(chain.from_iterable(extra.values()))) with open('nmmo/version.py', encoding="utf-8") as vf: ver = vf.read().split()[-1].strip("'") setup( name="nmmo", description="Neural MMO is a platform for multiagent intelligence research " + \ "inspired by Massively Multiplayer Online (MMO) role-playing games. " + \ "Documentation hosted at neuralmmo.github.io.", long_description_content_type="text/markdown", version=ver, packages=find_packages(), include_package_data=True, install_requires=[ 'cython>=3.0.0', 'numpy==1.23.3', 'scipy==1.10.0', 'pytest==7.3.0', 'pytest-benchmark==3.4.1', 'imageio>=2.27', 'ordered-set==4.1.0', 'pettingzoo==1.24.1', 'gymnasium==0.29.1', 'pylint==2.16.0', 'psutil<6', 'tqdm<5', 'py==1.11.0', 'dill<0.4', ], ext_modules = cythonize(["nmmo/lib/cython_helper.pyx"]), include_dirs=[np.get_include()], extras_require=extra, python_requires=">=3.7,<3.11", license="MIT", author="Joseph Suarez", author_email="jsuarez@mit.edu", url=REPO_URL, keywords=["Neural MMO", "MMO"], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Environment :: Console", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/action/test_ammo_use.py ================================================ import unittest import logging import numpy as np from tests.testhelpers import ScriptedTestTemplate, provide_item from nmmo.core import action from nmmo.systems import item as Item from nmmo.systems.item import ItemState RANDOM_SEED = 284 LOGFILE = None # 'tests/action/test_ammo_use.log' class TestAmmoUse(ScriptedTestTemplate): # pylint: disable=protected-access,multiple-statements,no-member @classmethod def setUpClass(cls): super().setUpClass() # config specific to the tests here if LOGFILE: # for debugging logging.basicConfig(filename=LOGFILE, level=logging.INFO) def _assert_action_targets_zero(self, gym_obs): mask = np.sum(gym_obs["ActionTargets"]["GiveGold"]["Price"]) \ + np.sum(gym_obs["ActionTargets"]["Buy"]["MarketItem"]) for atn in [action.Use, action.Give, action.Destroy, action.Sell]: mask += np.sum(gym_obs["ActionTargets"][atn.__name__]["InventoryItem"]) # If MarketItem and InventoryTarget have no-action flags, these sum up to 104 # To prevent entropy collapse, GiveGold/Price and Buy/MarketItem masks are tweaked # The Price mask is all ones, so the sum is 104 self.assertEqual(mask, 99 + 5*int(self.config.PROVIDE_NOOP_ACTION_TARGET)) def test_spawn_immunity(self): env = self._setup_env(random_seed=RANDOM_SEED) # Check spawn immunity in the action targets for ent_obs in env.obs.values(): gym_obs = ent_obs.to_gym() target_mask = gym_obs["ActionTargets"]["Attack"]["Target"][:len(ent_obs.entities.ids)] # cannot target other agents self.assertTrue(np.sum(target_mask[ent_obs.entities.ids > 0]) == 0) # Test attack during spawn immunity, which should be ignored env.step({ ent_id: { action.Attack: { action.Style: env.realm.players[ent_id].agent.style[0], action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } } for ent_id in self.ammo }) for ent_id in [1, 2, 3]: # in_combat status is set when attack is executed self.assertFalse(env.realm.players[ent_id].in_combat) def test_ammo_fire_all(self): env = self._setup_env(random_seed=RANDOM_SEED, remove_immunity=True) # First tick actions: USE (equip) level-0 ammo env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) } } for ent_id, ent_ammo in self.ammo.items() }) # check if the agents have equipped the ammo for ent_id, ent_ammo in self.ammo.items(): gym_obs = env.obs[ent_id].to_gym() inventory = env.obs[ent_id].inventory inv_idx = inventory.sig(ent_ammo, 0) self.assertEqual(1, # True ItemState.parse_array(inventory.values[inv_idx]).equipped) # check SELL InventoryItem mask -- one cannot sell equipped item mask = gym_obs["ActionTargets"]["Sell"]["InventoryItem"][:inventory.len] > 0 self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask]) # the agents must not be in combat status self.assertFalse(env.realm.players[ent_id].in_combat) # Second tick actions: ATTACK other agents using ammo # NOTE that agents 1 & 3's attack are invalid due to out-of-range env.step({ ent_id: { action.Attack: { action.Style: env.realm.players[ent_id].agent.style[0], action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } } for ent_id in self.ammo }) # check combat status: agents 2 (attacker) and 1 (target) are in combat self.assertTrue(env.realm.players[2].in_combat) self.assertTrue(env.realm.players[1].in_combat) self.assertFalse(env.realm.players[3].in_combat) # check the action masks are all 0 during combat for ent_id in [1, 2]: self._assert_action_targets_zero(env.obs[ent_id].to_gym()) # check if the ammos were consumed ammo_ids = [] for ent_id, ent_ammo in self.ammo.items(): inventory = env.obs[ent_id].inventory inv_idx = inventory.sig(ent_ammo, 0) item_info = ItemState.parse_array(inventory.values[inv_idx]) if ent_id == 2: # only agent 2's attack is valid and consume ammo self.assertEqual(self.ammo_quantity - 1, item_info.quantity) ammo_ids.append(inventory.id(inv_idx)) else: self.assertEqual(self.ammo_quantity, item_info.quantity) # Third tick actions: ATTACK again to use up all the ammo, except agent 3 # NOTE that agent 3's attack command is invalid due to out-of-range env.step({ ent_id: { action.Attack: { action.Style: env.realm.players[ent_id].agent.style[0], action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } } for ent_id in self.ammo }) # agents 1 and 2's latest_combat_tick should be updated self.assertEqual(env.realm.tick, env.realm.players[1].latest_combat_tick.val) self.assertEqual(env.realm.tick, env.realm.players[2].latest_combat_tick.val) self.assertEqual(0, env.realm.players[3].latest_combat_tick.val) # check if the ammos are depleted and the ammo slot is empty ent_id = 2 self.assertTrue(env.obs[ent_id].inventory.len == len(self.item_sig[ent_id]) - 1) self.assertTrue(env.realm.players[ent_id].inventory.equipment.ammunition.item is None) for item_id in ammo_ids: self.assertTrue(len(ItemState.Query.by_id(env.realm.datastore, item_id)) == 0) self.assertTrue(item_id not in env.realm.items) # invalid attacks for ent_id in [1, 3]: # agent 3 gathered arrow, so the item count increased #self.assertTrue(env.obs[ent_id].inventory.len == len(self.item_sig[ent_id])) self.assertTrue(env.realm.players[ent_id].inventory.equipment.ammunition.item is not None) # after 3 ticks, combat status should be cleared for _ in range(3): env.step({ 0:0 }) # put dummy actions to prevent generating scripted actions for ent_id in [1, 2, 3]: self.assertFalse(env.realm.players[ent_id].in_combat) # DONE def test_use_ammo_only_when_attack_style_match(self): env = self._setup_env(random_seed=RANDOM_SEED, remove_immunity=True) # First tick actions: USE (equip) level-0 ammo env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) } } for ent_id, ent_ammo in self.ammo.items() }) # Second tick actions: Melee attack should not consume Arrow ent_id = 2 env.step({ 2: { action.Attack: { action.Style: action.Melee, action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } }}) ent_ammo = self.ammo[ent_id] inventory = env.obs[ent_id].inventory inv_idx = inventory.sig(ent_ammo, 0) item_info = ItemState.parse_array(inventory.values[inv_idx]) # Did not consume ammo self.assertEqual(self.ammo_quantity, item_info.quantity) # DONE def test_cannot_use_listed_items(self): env = self._setup_env(random_seed=RANDOM_SEED) sell_price = 1 # provide extra whetstone to range to make its inventory full # but level-0 whetstone overlaps with the listed item ent_id = 2 provide_item(env.realm, ent_id, Item.Whetstone, level=0, quantity=3) provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=3) # provide extra whetstone to mage to make its inventory full # there will be no overlapping item ent_id = 3 provide_item(env.realm, ent_id, Item.Whetstone, level=5, quantity=3) provide_item(env.realm, ent_id, Item.Whetstone, level=7, quantity=3) # First tick actions: SELL level-0 ammo env.step({ ent_id: { action.Sell: { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0), action.Price: action.Price.index(sell_price) } } for ent_id, ent_ammo in self.ammo.items() }) # check if the ammos were listed for ent_id, ent_ammo in self.ammo.items(): gym_obs = env.obs[ent_id].to_gym() inventory = env.obs[ent_id].inventory inv_idx = inventory.sig(ent_ammo, 0) item_info = ItemState.parse_array(inventory.values[inv_idx]) # ItemState data self.assertEqual(sell_price, item_info.listed_price) # Exchange listing self.assertTrue(item_info.id in env.realm.exchange._item_listings) self.assertTrue(item_info.id in env.obs[ent_id].market.ids) # check SELL InventoryItem mask -- one cannot sell listed item mask = gym_obs["ActionTargets"]["Sell"]["InventoryItem"][:inventory.len] > 0 self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask]) # check USE InventoryItem mask -- one cannot use listed item mask = gym_obs["ActionTargets"]["Use"]["InventoryItem"][:inventory.len] > 0 self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask]) # check BUY MarketItem mask -- there should be two ammo items in the market mask = gym_obs["ActionTargets"]["Buy"]["MarketItem"][:inventory.len] > 0 # agent 1 has inventory space if ent_id == 1: self.assertTrue(sum(mask) == 2) # agent 2's inventory is full but can buy level-0 whetstone (existing ammo) if ent_id == 2: self.assertTrue(sum(mask) == 1) # agent 3's inventory is full without overlapping ammo if ent_id == 3: self.assertTrue(sum(mask) == 0) # Second tick actions: USE ammo, which should NOT happen env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) } } for ent_id, ent_ammo in self.ammo.items() }) # check if the agents have equipped the ammo for ent_id, ent_ammo in self.ammo.items(): inventory = env.obs[ent_id].inventory inv_idx = inventory.sig(ent_ammo, 0) self.assertEqual(0, # False ItemState.parse_array(inventory.values[inv_idx]).equipped) # DONE def test_receive_extra_ammo_swap(self): env = self._setup_env(random_seed=RANDOM_SEED) extra_ammo = 500 wstone_lvl0 = (Item.Whetstone, 0) wstone_lvl1 = (Item.Whetstone, 1) wstone_lvl3 = (Item.Whetstone, 3) def sig_int_tuple(sig): return (sig[0].ITEM_TYPE_ID, sig[1]) for ent_id in self.policy: # provide extra whetstone provide_item(env.realm, ent_id, Item.Whetstone, level=0, quantity=extra_ammo) provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=extra_ammo) # level up the agent 1 (Melee) to 2 env.realm.players[1].skills.melee.level.update(2) # check inventory env._compute_observations() for ent_id in self.ammo: # realm data inv_realm = { item.signature: item.quantity.val for item in env.realm.players[ent_id].inventory.items if isinstance(item, Item.Stack) } self.assertTrue( sig_int_tuple(wstone_lvl0) in inv_realm ) self.assertTrue( sig_int_tuple(wstone_lvl1) in inv_realm ) self.assertEqual( inv_realm[sig_int_tuple(wstone_lvl1)], extra_ammo ) # item datastore inv_obs = env.obs[ent_id].inventory self.assertTrue(inv_obs.sig(*wstone_lvl0) is not None) self.assertTrue(inv_obs.sig(*wstone_lvl1) is not None) self.assertEqual( extra_ammo, ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).quantity) if ent_id == 1: # if the ammo has the same signature, the quantity is added to the existing stack self.assertEqual(inv_realm[sig_int_tuple(wstone_lvl0)], extra_ammo + self.ammo_quantity ) self.assertEqual(extra_ammo + self.ammo_quantity, ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).quantity) # so there should be 1 more space self.assertEqual(inv_obs.len, self.config.ITEM_INVENTORY_CAPACITY - 1) else: # if the signature is different, it occupies a new inventory space self.assertEqual(inv_realm[sig_int_tuple(wstone_lvl0)], extra_ammo ) self.assertEqual(extra_ammo, ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).quantity) # thus the inventory is full self.assertEqual(inv_obs.len, self.config.ITEM_INVENTORY_CAPACITY) if ent_id == 1: gym_obs = env.obs[ent_id].to_gym() # check USE InventoryItem mask mask = gym_obs["ActionTargets"]["Use"]["InventoryItem"][:inv_obs.len] > 0 # level-2 melee should be able to use level-0, level-1 whetstone but not level-3 self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl0)) in inv_obs.ids[mask]) self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl1)) in inv_obs.ids[mask]) self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl3)) not in inv_obs.ids[mask]) # First tick actions: USE (equip) level-0 ammo # execute only the agent 1's action ent_id = 1 env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl0) } }}) # check if the agents have equipped the ammo 0 inv_obs = env.obs[ent_id].inventory self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 1) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 0) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0) # Second tick actions: USE (equip) level-1 ammo # this should unequip level-0 then equip level-1 ammo env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl1) } }}) # check if the agents have equipped the ammo 1 inv_obs = env.obs[ent_id].inventory self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 0) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 1) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0) # Third tick actions: USE (equip) level-3 ammo # this should ignore USE action and leave level-1 ammo equipped env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl3) } }}) # check if the agents have equipped the ammo 1 inv_obs = env.obs[ent_id].inventory self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 0) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 1) self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0) # DONE def test_use_ration_potion(self): # cannot use level-3 ration & potion due to low level # can use level-0 ration & potion to increase food/water/health env = self._setup_env(random_seed=RANDOM_SEED) # make food/water/health 20 res_dec_tick = env.config.RESOURCE_DEPLETION_RATE init_res = 20 for ent_id in self.policy: env.realm.players[ent_id].resources.food.update(init_res) env.realm.players[ent_id].resources.water.update(init_res) env.realm.players[ent_id].resources.health.update(init_res) """First tick: try to use level-3 ration & potion""" ration_lvl3 = (Item.Ration, 3) potion_lvl3 = (Item.Potion, 3) actions = {} ent_id = 1; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl3) } } ent_id = 2; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl3) } } ent_id = 3; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*potion_lvl3) } } env.step(actions) # check if the agents have used the ration & potion for ent_id in [1, 2]: # cannot use due to low level, so still in the inventory self.assertFalse( env.obs[ent_id].inventory.sig(*ration_lvl3) is None) # failed to restore food/water, so no change resources = env.realm.players[ent_id].resources self.assertEqual( resources.food.val, init_res - res_dec_tick) self.assertEqual( resources.water.val, init_res - res_dec_tick) ent_id = 3 # failed to use the item self.assertFalse( env.obs[ent_id].inventory.sig(*potion_lvl3) is None) self.assertEqual( env.realm.players[ent_id].resources.health.val, init_res) """Second tick: try to use level-0 ration & potion""" ration_lvl0 = (Item.Ration, 0) potion_lvl0 = (Item.Potion, 0) actions = {} ent_id = 1; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl0) } } ent_id = 2; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl0) } } ent_id = 3; actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*potion_lvl0) } } env.step(actions) # check if the agents have successfully used the ration & potion restore = env.config.PROFESSION_CONSUMABLE_RESTORE(0) for ent_id in [1, 2]: # items should be gone self.assertTrue( env.obs[ent_id].inventory.sig(*ration_lvl0) is None) # successfully restored food/water resources = env.realm.players[ent_id].resources self.assertEqual( resources.food.val, init_res + restore - 2*res_dec_tick) self.assertEqual( resources.water.val, init_res + restore - 2*res_dec_tick) ent_id = 3 # successfully restored health self.assertTrue( env.obs[ent_id].inventory.sig(*potion_lvl0) is None) # item gone self.assertEqual( env.realm.players[ent_id].resources.health.val, init_res + restore) # DONE if __name__ == '__main__': unittest.main() ================================================ FILE: tests/action/test_destroy_give_gold.py ================================================ import unittest import logging from tests.testhelpers import ScriptedTestTemplate, change_spawn_pos, provide_item from nmmo.core import action from nmmo.systems import item as Item from nmmo.systems.item import ItemState from scripted import baselines RANDOM_SEED = 985 LOGFILE = None # 'tests/action/test_destroy_give_gold.log' class TestDestroyGiveGold(ScriptedTestTemplate): # pylint: disable=protected-access,multiple-statements,no-member @classmethod def setUpClass(cls): super().setUpClass() # config specific to the tests here cls.config.set("PLAYERS", [baselines.Melee, baselines.Range]) cls.config.set("PLAYER_N", 6) cls.policy = { 1:'Melee', 2:'Range', 3:'Melee', 4:'Range', 5:'Melee', 6:'Range' } cls.spawn_locs = { 1:(17,17), 2:(21,21), 3:(17,17), 4:(21,21), 5:(21,21), 6:(17,17) } cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Whetstone, 4:Item.Arrow, 5:Item.Whetstone, 6:Item.Arrow } if LOGFILE: # for debugging logging.basicConfig(filename=LOGFILE, level=logging.INFO) def test_destroy(self): env = self._setup_env(random_seed=RANDOM_SEED) # check if level-0 and level-3 ammo are in the correct place for ent_id in self.policy: for idx, lvl in enumerate(self.item_level): assert self.item_sig[ent_id][idx] == (self.ammo[ent_id], lvl) # equipped items cannot be destroyed, i.e. that action will be ignored # this should be marked in the mask too """ First tick """ # First tick actions: USE (equip) level-0 ammo env.step({ ent_id: { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0]) } # level-0 ammo } for ent_id in self.policy }) # check if the agents have equipped the ammo for ent_id in self.policy: ent_obs = env.obs[ent_id] inv_idx = ent_obs.inventory.sig(*self.item_sig[ent_id][0]) # level-0 ammo self.assertEqual(1, # True ItemState.parse_array(ent_obs.inventory.values[inv_idx]).equipped) # check Destroy InventoryItem mask -- one cannot destroy equipped item for item_sig in self.item_sig[ent_id]: if item_sig == (self.ammo[ent_id], 0): # level-0 ammo self.assertFalse(self._check_inv_mask(ent_obs, action.Destroy, item_sig)) else: # other items can be destroyed self.assertTrue(self._check_inv_mask(ent_obs, action.Destroy, item_sig)) """ Second tick """ # Second tick actions: DESTROY ammo actions = {} for ent_id in self.policy: if ent_id in [1, 2]: # agent 1 & 2, destroy the level-3 ammos, which are valid actions[ent_id] = { action.Destroy: { action.InventoryItem: env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][1]) } } else: # other agents: destroy the equipped level-0 ammos, which are invalid actions[ent_id] = { action.Destroy: { action.InventoryItem: env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0]) } } env.step(actions) # check if the ammos were destroyed for ent_id in self.policy: if ent_id in [1, 2]: inv_idx = env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][1]) self.assertTrue(inv_idx is None) # valid actions, thus destroyed else: inv_idx = env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0]) self.assertTrue(inv_idx is not None) # invalid actions, thus not destroyed # DONE def test_give_tile_npc(self): # cannot give to self (should be masked) # cannot give if not on the same tile (should be masked) # cannot give to the other team member (should be masked) # cannot give to npc (should be masked) env = self._setup_env(random_seed=RANDOM_SEED) # teleport the npc -1 to agent 5's location change_spawn_pos(env.realm, -1, self.spawn_locs[5]) env._compute_observations() """ First tick actions """ actions = {} test_cond = {} # agent 1: give ammo to agent 3 (valid: the same team, same tile) test_cond[1] = { 'tgt_id': 3, 'item_sig': self.item_sig[1][0], 'ent_mask': True, 'inv_mask': True, 'valid': True } # agent 2: give ammo to agent 2 (invalid: cannot give to self) test_cond[2] = { 'tgt_id': 2, 'item_sig': self.item_sig[2][0], 'ent_mask': False, 'inv_mask': True, 'valid': False } # agent 5: give ammo to npc -1 (invalid, should be masked) test_cond[5] = { 'tgt_id': -1, 'item_sig': self.item_sig[5][0], 'ent_mask': False, 'inv_mask': True, 'valid': False } actions = self._check_assert_make_action(env, action.Give, test_cond) env.step(actions) # check the results for ent_id, cond in test_cond.items(): self.assertEqual( cond['valid'], env.obs[ent_id].inventory.sig(*cond['item_sig']) is None) if ent_id == 1: # agent 1 gave ammo stack to agent 3 tgt_inv = env.obs[cond['tgt_id']].inventory inv_idx = tgt_inv.sig(*cond['item_sig']) self.assertEqual(2 * self.ammo_quantity, ItemState.parse_array(tgt_inv.values[inv_idx]).quantity) # DONE def test_give_equipped_listed(self): # cannot give equipped items (should be masked) # cannot give listed items (should be masked) env = self._setup_env(random_seed=RANDOM_SEED) """ First tick actions """ actions = {} # agent 1: equip the ammo ent_id = 1; item_sig = self.item_sig[ent_id][0] self.assertTrue( self._check_inv_mask(env.obs[ent_id], action.Use, item_sig)) actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig) } } # agent 2: list the ammo for sale ent_id = 2; price = 5; item_sig = self.item_sig[ent_id][0] self.assertTrue( self._check_inv_mask(env.obs[ent_id], action.Sell, item_sig)) actions[ent_id] = { action.Sell: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig), action.Price: action.Price.index(price) } } env.step(actions) # Check the first tick actions # agent 1: equip the ammo ent_id = 1; item_sig = self.item_sig[ent_id][0] inv_idx = env.obs[ent_id].inventory.sig(*item_sig) self.assertEqual(1, ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).equipped) # agent 2: list the ammo for sale ent_id = 2; price = 5; item_sig = self.item_sig[ent_id][0] inv_idx = env.obs[ent_id].inventory.sig(*item_sig) self.assertEqual(price, ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).listed_price) self.assertTrue(env.obs[ent_id].inventory.id(inv_idx) in env.obs[ent_id].market.ids) """ Second tick actions """ actions = {} test_cond = {} # agent 1: give equipped ammo to agent 3 (invalid: should be masked) test_cond[1] = { 'tgt_id': 3, 'item_sig': self.item_sig[1][0], 'ent_mask': True, 'inv_mask': False, 'valid': False } # agent 2: give listed ammo to agent 4 (invalid: should be masked) test_cond[2] = { 'tgt_id': 4, 'item_sig': self.item_sig[2][0], 'ent_mask': True, 'inv_mask': False, 'valid': False } actions = self._check_assert_make_action(env, action.Give, test_cond) env.step(actions) # Check the second tick actions # check the results for ent_id, cond in test_cond.items(): self.assertEqual( cond['valid'], env.obs[ent_id].inventory.sig(*cond['item_sig']) is None) # DONE def test_give_full_inventory(self): # cannot give to an agent with the full inventory, # but it's possible if the agent has the same ammo stack env = self._setup_env(random_seed=RANDOM_SEED) # make the inventory full for agents 1, 2 extra_items = { (Item.Bottom, 0), (Item.Bottom, 3) } for ent_id in [1, 2]: for item_sig in extra_items: self.item_sig[ent_id].append(item_sig) provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1) env._compute_observations() # check if the inventory is full for ent_id in [1, 2]: self.assertEqual(env.obs[ent_id].inventory.len, env.config.ITEM_INVENTORY_CAPACITY) self.assertTrue(env.realm.players[ent_id].inventory.space == 0) """ First tick actions """ actions = {} test_cond = {} # agent 3: give ammo to agent 1 (the same ammo stack, so valid) test_cond[3] = { 'tgt_id': 1, 'item_sig': self.item_sig[3][0], 'ent_mask': True, 'inv_mask': True, 'valid': True } # agent 4: give gloves to agent 2 (not the stack, so invalid) test_cond[4] = { 'tgt_id': 2, 'item_sig': self.item_sig[4][4], 'ent_mask': True, 'inv_mask': True, 'valid': False } actions = self._check_assert_make_action(env, action.Give, test_cond) env.step(actions) # Check the first tick actions # check the results for ent_id, cond in test_cond.items(): self.assertEqual( cond['valid'], env.obs[ent_id].inventory.sig(*cond['item_sig']) is None) if ent_id == 3: # successfully gave the ammo stack to agent 1 tgt_inv = env.obs[cond['tgt_id']].inventory inv_idx = tgt_inv.sig(*cond['item_sig']) self.assertEqual(2 * self.ammo_quantity, ItemState.parse_array(tgt_inv.values[inv_idx]).quantity) # DONE def test_give_gold(self): # cannot give to an npc (should be masked) # cannot give to self (should be masked) # cannot give if not on the same tile (should be masked) env = self._setup_env(random_seed=RANDOM_SEED) # teleport the npc -1 to agent 3's location change_spawn_pos(env.realm, -1, self.spawn_locs[3]) env._compute_observations() test_cond = {} # NOTE: the below tests rely on the static execution order from 1 to N # agent 1: give gold to agent 3 (valid: same tile) test_cond[1] = { 'tgt_id': 3, 'gold': 1, 'ent_mask': True, 'ent_gold': self.init_gold-1, 'tgt_gold': self.init_gold+1 } # agent 2: give gold to agent 4 (valid: same tile) test_cond[2] = { 'tgt_id': 4, 'gold': self.init_gold, 'ent_mask': True, 'ent_gold': 0, 'tgt_gold': 2*self.init_gold } # agent 3: give gold to npc -1 (invalid: cannot give to npc) # ent_gold is self.init_gold+1 because (3) got 1 gold from (1) test_cond[3] = { 'tgt_id': -1, 'gold': 1, 'ent_mask': False, 'ent_gold': self.init_gold+1, 'tgt_gold': self.init_gold } # agent 4: give -1 gold to 2 (invalid: cannot give minus gold) # ent_gold is 2*self.init_gold because (4) got 5 gold from (2) # tgt_gold is 0 because (2) gave all gold to (4) test_cond[4] = { 'tgt_id': 2, 'gold': -1, 'ent_mask': True, 'ent_gold': 2*self.init_gold, 'tgt_gold': 0 } actions = self._check_assert_make_action(env, action.GiveGold, test_cond) env.step(actions) # check the results for ent_id, cond in test_cond.items(): self.assertEqual(cond['ent_gold'], env.realm.players[ent_id].gold.val) if cond['tgt_id'] > 0: self.assertEqual(cond['tgt_gold'], env.realm.players[cond['tgt_id']].gold.val) # DONE if __name__ == '__main__': unittest.main() ================================================ FILE: tests/action/test_monkey_action.py ================================================ import unittest import random from tqdm import tqdm import numpy as np from tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv import nmmo # 30 seems to be enough to test variety of agent actions TEST_HORIZON = 30 RANDOM_SEED = random.randint(0, 1000000) def make_random_actions(config, ent_obs): assert 'ActionTargets' in ent_obs, 'ActionTargets is not provided in the obs' actions = {} # atn, arg, val for atn in sorted(nmmo.Action.edges(config)): actions[atn] = {} for arg in sorted(atn.edges, reverse=True): # intentionally doing wrong mask = ent_obs["ActionTargets"][atn.__name__][arg.__name__] actions[atn][arg] = 0 if np.any(mask): actions[atn][arg] += int(np.random.choice(np.where(mask)[0])) return actions # CHECK ME: this would be nice to include in the env._validate_actions() def filter_item_actions(actions, use_str_key=False): # when there are multiple actions on the same item, select one flt_atns = {} inventory_atn = {} # key: inventory idx, val: action for atn in actions: if atn in [nmmo.action.Use, nmmo.action.Sell, nmmo.action.Give, nmmo.action.Destroy]: for arg, val in actions[atn].items(): if arg == nmmo.action.InventoryItem: if val not in inventory_atn: inventory_atn[val] = [( atn, actions[atn] )] else: inventory_atn[val].append(( atn, actions[atn] )) else: flt_atns[atn] = actions[atn] # randomly select one action for each inventory item for atns in inventory_atn.values(): if len(atns) > 1: picked = random.choice(atns) flt_atns[picked[0]] = picked[1] else: flt_atns[atns[0][0]] = atns[0][1] # convert action keys to str if use_str_key: str_atns = {} for atn, args in flt_atns.items(): str_atns[atn.__name__] = {} for arg, val in args.items(): str_atns[atn.__name__][arg.__name__] = val flt_atns = str_atns return flt_atns class TestMonkeyAction(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = ScriptedAgentTestConfig() cls.config.PROVIDE_ACTION_TARGETS = True @staticmethod # NOTE: this can also be used for sweeping random seeds def rollout_with_seed(config, seed, use_str_key=False): env = ScriptedAgentTestEnv(config) obs, _ = env.reset(seed=seed) for _ in tqdm(range(TEST_HORIZON)): # sample random actions for each player actions = {} for ent_id in env.realm.players: ent_atns = make_random_actions(config, obs[ent_id]) actions[ent_id] = filter_item_actions(ent_atns, use_str_key) obs, _, _, _, _ = env.step(actions) def test_monkey_action(self): try: self.rollout_with_seed(self.config, RANDOM_SEED) except: # pylint: disable=bare-except assert False, f"Monkey action failed. seed: {RANDOM_SEED}" def test_monkey_action_with_str_key(self): self.rollout_with_seed(self.config, RANDOM_SEED, use_str_key=True) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/action/test_sell_buy.py ================================================ import unittest import logging from tests.testhelpers import ScriptedTestTemplate, provide_item from nmmo.core import action from nmmo.systems import item as Item from nmmo.systems.item import ItemState from scripted import baselines RANDOM_SEED = 985 LOGFILE = None # 'tests/action/test_sell_buy.log' class TestSellBuy(ScriptedTestTemplate): # pylint: disable=protected-access,multiple-statements,unsubscriptable-object,no-member @classmethod def setUpClass(cls): super().setUpClass() # config specific to the tests here cls.config.set("PLAYERS", [baselines.Melee, baselines.Range]) cls.config.set("PLAYER_N", 6) cls.policy = { 1:'Melee', 2:'Range', 3:'Melee', 4:'Range', 5:'Melee', 6:'Range' } cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Whetstone, 4:Item.Arrow, 5:Item.Whetstone, 6:Item.Arrow } if LOGFILE: # for debugging logging.basicConfig(filename=LOGFILE, level=logging.INFO) def test_sell_buy(self): # cannot list an item with 0 price --> impossible to do this # cannot list an equipped item for sale (should be masked) # cannot buy an item with the full inventory, # but it's possible if the agent has the same ammo stack # cannot buy its own item (should be masked) # cannot buy an item if gold is not enough (should be masked) # cannot list an already listed item for sale (should be masked) env = self._setup_env(random_seed=RANDOM_SEED) # make the inventory full for agents 1, 2 extra_items = { (Item.Bottom, 0), (Item.Bottom, 3) } for ent_id in [1, 2]: for item_sig in extra_items: self.item_sig[ent_id].append(item_sig) provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1) env._compute_observations() # check if the inventory is full for ent_id in [1, 2]: self.assertEqual(env.obs[ent_id].inventory.len, env.config.ITEM_INVENTORY_CAPACITY) self.assertTrue(env.realm.players[ent_id].inventory.space == 0) """ First tick actions """ # cannot list an item with 0 price actions = {} # agent 1-2: equip the ammo for ent_id in [1, 2]: item_sig = self.item_sig[ent_id][0] self.assertTrue( self._check_inv_mask(env.obs[ent_id], action.Use, item_sig)) actions[ent_id] = { action.Use: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig) } } # agent 4: list the ammo for sale with price 0 # the zero in action.Price is deserialized into Discrete_1, so it's valid ent_id = 4; price = 0; item_sig = self.item_sig[ent_id][0] actions[ent_id] = { action.Sell: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig), action.Price: action.Price.edges[price] } } env.step(actions) # Check the first tick actions # agent 1-2: the ammo equipped, thus should be masked for sale for ent_id in [1, 2]: item_sig = self.item_sig[ent_id][0] inv_idx = env.obs[ent_id].inventory.sig(*item_sig) self.assertEqual(1, # equipped = true ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).equipped) self.assertFalse( # not allowed to list self._check_inv_mask(env.obs[ent_id], action.Sell, item_sig)) """ Second tick actions """ # listing the level-0 ammo with different prices # cannot list an equipped item for sale (should be masked) listing_price = { 1:1, 2:5, 3:15, 5:2 } # gold for ent_id, price in listing_price.items(): item_sig = self.item_sig[ent_id][0] actions[ent_id] = { action.Sell: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig), action.Price: action.Price.edges[price-1] } } env.step(actions) # Check the second tick actions # agent 1-2: the ammo equipped, thus not listed for sale # agent 3-5's ammos listed for sale for ent_id, price in listing_price.items(): item_id = env.obs[ent_id].inventory.id(0) if ent_id in [1, 2]: # failed to list for sale self.assertFalse(item_id in env.obs[ent_id].market.ids) # not listed self.assertEqual(0, ItemState.parse_array(env.obs[ent_id].inventory.values[0]).listed_price) else: # should succeed to list for sale self.assertTrue(item_id in env.obs[ent_id].market.ids) # listed self.assertEqual(price, # sale price set ItemState.parse_array(env.obs[ent_id].inventory.values[0]).listed_price) # should not buy mine self.assertFalse( self._check_mkt_mask(env.obs[ent_id], item_id)) # should not list the same item twice self.assertFalse( self._check_inv_mask(env.obs[ent_id], action.Sell, self.item_sig[ent_id][0])) """ Third tick actions """ # cannot buy an item with the full inventory, # but it's possible if the agent has the same ammo stack # cannot buy its own item (should be masked) # cannot buy an item if gold is not enough (should be masked) # cannot list an already listed item for sale (should be masked) test_cond = {} # agent 1: buy agent 5's ammo (valid: 1 has the same ammo stack) # although 1's inventory is full, this action is valid agent5_ammo = env.obs[5].inventory.id(0) test_cond[1] = { 'item_id': agent5_ammo, 'mkt_mask': True } # agent 2: buy agent 5's ammo (invalid: full space and no same stack) test_cond[2] = { 'item_id': agent5_ammo, 'mkt_mask': False } # agent 4: cannot buy its own item (invalid) test_cond[4] = { 'item_id': env.obs[4].inventory.id(0), 'mkt_mask': False } # agent 5: cannot buy agent 3's ammo (invalid: not enought gold) test_cond[5] = { 'item_id': env.obs[3].inventory.id(0), 'mkt_mask': False } actions = self._check_assert_make_action(env, action.Buy, test_cond) # agent 3: list an already listed item for sale (try different price) ent_id = 3; item_sig = self.item_sig[ent_id][0] actions[ent_id] = { action.Sell: { action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig), action.Price: action.Price.edges[7] } } # try to set different price env.step(actions) # Check the third tick actions # agent 1: buy agent 5's ammo (valid: 1 has the same ammo stack) # agent 5's ammo should be gone seller_id = 5; buyer_id = 1 self.assertFalse( agent5_ammo in env.obs[seller_id].inventory.ids) self.assertEqual( env.realm.players[seller_id].gold.val, # gold transfer self.init_gold + listing_price[seller_id]) self.assertEqual(2 * self.ammo_quantity, # ammo transfer ItemState.parse_array(env.obs[buyer_id].inventory.values[0]).quantity) self.assertEqual( env.realm.players[buyer_id].gold.val, # gold transfer self.init_gold - listing_price[seller_id]) # agent 2-4: invalid buy, no exchange, thus the same money for ent_id in [2, 3, 4]: self.assertEqual( env.realm.players[ent_id].gold.val, self.init_gold) # DONE if __name__ == '__main__': unittest.main() ================================================ FILE: tests/conftest.py ================================================ #pylint: disable=unused-argument import logging logging.basicConfig(level=logging.INFO, stream=None) def pytest_benchmark_scale_unit(config, unit, benchmarks, best, worst, sort): if unit == 'seconds': prefix = 'millisec' scale = 1000 elif unit == 'operations': prefix = '' scale = 1 else: raise RuntimeError(f"Unexpected measurement unit {unit}") return prefix, scale ================================================ FILE: tests/core/test_config.py ================================================ import unittest import nmmo import nmmo.core.config as cfg class Config(cfg.Config, cfg.Terrain, cfg.Combat): pass class TestConfig(unittest.TestCase): def test_config_attr_set_episode(self): config = nmmo.config.Default() self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, True) config.set_for_episode("RESOURCE_SYSTEM_ENABLED", False) self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, False) config.reset() self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, True) def test_cannot_change_immutable_attr(self): config = Config() with self.assertRaises(AssertionError): config.set_for_episode("PLAYER_N", 100) def test_cannot_change_obs_attr(self): config = Config() with self.assertRaises(AssertionError): config.set_for_episode("PLAYER_N_OBS", 50) def test_cannot_use_noninit_system(self): config = Config() with self.assertRaises(AssertionError): config.set_for_episode("ITEM_SYSTEM_ENABLED", True) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_cython_masks.py ================================================ # pylint: disable=protected-access,bad-builtin import unittest from timeit import timeit from copy import deepcopy #import random import numpy as np import nmmo from tests.testhelpers import ScriptedAgentTestConfig RANDOM_SEED = 3333 # random.randint(0, 10000) PERF_TEST = True class TestCythonMasks(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = ScriptedAgentTestConfig() cls.config.set("USE_CYTHON", True) cls.config.set("COMBAT_SPAWN_IMMUNITY", 5) cls.env = nmmo.Env(cls.config, RANDOM_SEED) cls.env.reset() for _ in range(7): cls.env.step({}) cls.move_mask = cls.env._dummy_obs["ActionTargets"]["Move"] cls.attack_mask = cls.env._dummy_obs["ActionTargets"]["Attack"] def test_move_mask(self): obs = self.env.obs for agent_id in self.env.realm.players: np_masks = deepcopy(self.move_mask) cy_masks = deepcopy(self.move_mask) obs[agent_id]._make_move_mask(np_masks, use_cython=False) obs[agent_id]._make_move_mask(cy_masks, use_cython=True) self.assertTrue(np.array_equal(np_masks["Direction"], cy_masks["Direction"])) if PERF_TEST: print('---test_move_mask---') print('numpy:', timeit( lambda: [obs[agent_id]._make_move_mask(np_masks, use_cython=False) for agent_id in self.env.realm.players], number=1000, globals=globals())) print('cython:', timeit( lambda: [obs[agent_id]._make_move_mask(cy_masks, use_cython=True) for agent_id in self.env.realm.players], number=1000, globals=globals())) def test_attack_mask(self): obs = self.env.obs for agent_id in self.env.realm.players: np_masks = deepcopy(self.attack_mask) cy_masks = deepcopy(self.attack_mask) obs[agent_id]._make_attack_mask(np_masks, use_cython=False) obs[agent_id]._make_attack_mask(cy_masks, use_cython=True) self.assertTrue(np.array_equal(np_masks["Target"], cy_masks["Target"])) if PERF_TEST: print('---test_attack_mask---') print('numpy:', timeit( lambda: [obs[agent_id]._make_attack_mask(np_masks, use_cython=False) for agent_id in self.env.realm.players], number=1000, globals=globals())) print('cython:', timeit( lambda: [obs[agent_id]._make_attack_mask(cy_masks, use_cython=True) for agent_id in self.env.realm.players], number=1000, globals=globals())) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_entity.py ================================================ import unittest import numpy as np import nmmo from nmmo.entity.entity import Entity, EntityState from nmmo.datastore.numpy_datastore import NumpyDatastore from scripted.baselines import Random class MockRealm: def __init__(self): self.config = nmmo.config.Default() self.config.PLAYERS = range(100) self.datastore = NumpyDatastore() self.datastore.register_object_type("Entity", EntityState.State.num_attributes) self._np_random = np.random # pylint: disable=no-member class TestEntity(unittest.TestCase): def test_entity(self): realm = MockRealm() entity_id = 123 entity = Entity(realm, (10,20), entity_id, "name") self.assertEqual(entity.id.val, entity_id) self.assertEqual(entity.row.val, 10) self.assertEqual(entity.col.val, 20) self.assertEqual(entity.damage.val, 0) self.assertEqual(entity.time_alive.val, 0) self.assertEqual(entity.freeze.val, 0) self.assertEqual(entity.item_level.val, 0) self.assertEqual(entity.attacker_id.val, 0) self.assertEqual(entity.message.val, 0) self.assertEqual(entity.gold.val, 0) self.assertEqual(entity.health.val, realm.config.PLAYER_BASE_HEALTH) self.assertEqual(entity.food.val, realm.config.RESOURCE_BASE) self.assertEqual(entity.water.val, realm.config.RESOURCE_BASE) self.assertEqual(entity.melee_level.val, 0) self.assertEqual(entity.range_level.val, 0) self.assertEqual(entity.mage_level.val, 0) self.assertEqual(entity.fishing_level.val, 0) self.assertEqual(entity.herbalism_level.val, 0) self.assertEqual(entity.prospecting_level.val, 0) self.assertEqual(entity.carving_level.val, 0) self.assertEqual(entity.alchemy_level.val, 0) def test_query_by_ids(self): realm = MockRealm() entity_id = 123 entity = Entity(realm, (10,20), entity_id, "name") entities = EntityState.Query.by_ids(realm.datastore, [entity_id]) self.assertEqual(len(entities), 1) self.assertEqual(entities[0][Entity.State.attr_name_to_col["id"]], entity_id) self.assertEqual(entities[0][Entity.State.attr_name_to_col["row"]], 10) self.assertEqual(entities[0][Entity.State.attr_name_to_col["col"]], 20) entity.food.update(11) e_row = EntityState.Query.by_id(realm.datastore, entity_id) self.assertEqual(e_row[Entity.State.attr_name_to_col["food"]], 11) def test_recon_resurrect(self): config = nmmo.config.Default() config.set("PLAYERS", [Random]) env = nmmo.Env(config) env.reset() # set player 1 to be a recon # Recons are immortal and cannot act (move) player1 = env.realm.players[1] player1.make_recon() spawn_pos = player1.pos for _ in range(50): # long enough to starve to death env.step({}) self.assertEqual(player1.pos, spawn_pos) self.assertEqual(player1.health.val, config.PLAYER_BASE_HEALTH) # resurrect player1 player1.health.update(0) self.assertEqual(player1.alive, False) env.step({}) player1.resurrect(health_prop=0.5, freeze_duration=10) self.assertEqual(player1.health.val, 50) self.assertEqual(player1.freeze.val, 10) self.assertEqual(player1.message.val, 0) self.assertEqual(player1.npc_type, -1) # immortal flag self.assertEqual(player1.my_task.progress, 0) # task progress should be reset # pylint:disable=protected-access self.assertEqual(player1._make_mortal_tick, env.realm.tick + 10) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_env.py ================================================ import unittest from typing import List import random import numpy as np from tqdm import tqdm import nmmo from nmmo.core.realm import Realm from nmmo.core.tile import TileState from nmmo.entity.entity import Entity, EntityState from nmmo.systems.item import ItemState from scripted import baselines # Allow private access for testing # pylint: disable=protected-access # 30 seems to be enough to test variety of agent actions TEST_HORIZON = 30 RANDOM_SEED = random.randint(0, 10000) class Config(nmmo.config.Small, nmmo.config.AllGameSystems): PLAYERS = [ baselines.Fisher, baselines.Herbalist, baselines.Prospector, baselines.Carver, baselines.Alchemist, baselines.Melee, baselines.Range, baselines.Mage] class TestEnv(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = Config() cls.env = nmmo.Env(cls.config, RANDOM_SEED) def test_action_space(self): action_space = self.env.action_space(0) atn_str_keys = set(atn.__name__ for atn in nmmo.Action.edges(self.config)) self.assertSetEqual( set(action_space.keys()), atn_str_keys) def test_observations(self): obs, _ = self.env.reset() self.assertEqual(obs.keys(), self.env.realm.players.keys()) for _ in tqdm(range(TEST_HORIZON)): entity_locations = [ [ev.row.val, ev.col.val, e] for e, ev in self.env.realm.players.entities.items() ] + [ [ev.row.val, ev.col.val, e] for e, ev in self.env.realm.npcs.entities.items() ] for player_id, player_obs in obs.items(): if player_id in self.env.realm.players: # alive agents self._validate_tiles(player_obs, self.env.realm) self._validate_entitites( player_id, player_obs, self.env.realm, entity_locations) self._validate_inventory(player_id, player_obs, self.env.realm) self._validate_market(player_obs, self.env.realm) else: # the obs of dead agents are dummy, all zeros self.assertEqual(np.sum(player_obs["Tile"]), 0) self.assertEqual(np.sum(player_obs["Entity"]), 0) self.assertEqual(np.sum(player_obs["Inventory"]), 0) self.assertEqual(np.sum(player_obs["Market"]), 0) self.assertEqual(np.sum(player_obs["ActionTargets"]["Move"]["Direction"]), 1) # no-op self.assertEqual(np.sum(player_obs["ActionTargets"]["Attack"]["Style"]), 3) # all ones obs, rewards, terminated, truncated, infos = self.env.step({}) # make sure dead agents return proper dones=True, dummy obs, and -1 reward self.assertEqual(len(self.env.agents), len(self.env.realm.players)) # NOTE: the below is no longer true when mini games resurrect dead players # self.assertEqual(len(self.env.possible_agents), # len(self.env.realm.players) + len(self.env._dead_agents)) for agent_id in self.env.agents: self.assertTrue(agent_id in obs) self.assertTrue(agent_id in rewards) self.assertTrue(agent_id in terminated) self.assertTrue(agent_id in truncated) self.assertTrue(agent_id in infos) for dead_id in self.env._dead_this_tick: self.assertEqual(rewards[dead_id], -1) self.assertTrue(terminated[dead_id]) # check dead and alive entity_all = EntityState.Query.table(self.env.realm.datastore) alive_agents = entity_all[:, Entity.State.attr_name_to_col["id"]] alive_agents = set(alive_agents[alive_agents > 0]) for agent_id in alive_agents: self.assertTrue(agent_id in self.env.realm.players) def _validate_tiles(self, obs, realm: Realm): for tile_obs in obs["Tile"]: tile_obs = TileState.parse_array(tile_obs) tile = realm.map.tiles[(int(tile_obs.row), int(tile_obs.col))] for key, val in tile_obs.__dict__.items(): if val != getattr(tile, key).val: self.assertEqual(val, getattr(tile, key).val, f"Mismatch for {key} in tile {tile_obs.row}, {tile_obs.col}") def _validate_entitites(self, player_id, obs, realm: Realm, entity_locations: List[List[int]]): observed_entities = set() for entity_obs in obs["Entity"]: entity_obs = EntityState.parse_array(entity_obs) if entity_obs.id == 0: continue entity: Entity = realm.entity(entity_obs.id) observed_entities.add(entity.ent_id) for key, val in entity_obs.__dict__.items(): if getattr(entity, key) is None: raise ValueError(f"Entity {entity} has no attribute {key}") self.assertEqual(val, getattr(entity, key).val, f"Mismatch for {key} in entity {entity_obs.id}") # Make sure that we see entities IFF they are in our vision radius row = realm.players.entities[player_id].row.val col = realm.players.entities[player_id].col.val vision = realm.config.PLAYER_VISION_RADIUS visible_entities = { e for r, c, e in entity_locations if row - vision <= r <= row + vision and col - vision <= c <= col + vision } self.assertSetEqual(visible_entities, observed_entities, f"Mismatch between observed: {observed_entities} " \ f"and visible {visible_entities} for player {player_id}, "\ f" step {self.env.realm.tick}") def _validate_inventory(self, player_id, obs, realm: Realm): self._validate_items( {i.id.val: i for i in realm.players[player_id].inventory.items}, obs["Inventory"] ) def _validate_market(self, obs, realm: Realm): self._validate_items( {i.item.id.val: i.item for i in realm.exchange._item_listings.values()}, obs["Market"] ) def _validate_items(self, items_dict, item_obs): item_obs = item_obs[item_obs[:,0] != 0] if len(items_dict) != len(item_obs): assert len(items_dict) == len(item_obs),\ f"Mismatch in number of items. Random seed: {RANDOM_SEED}" for item_ob in item_obs: item_ob = ItemState.parse_array(item_ob) item = items_dict[item_ob.id] for key, val in item_ob.__dict__.items(): self.assertEqual(val, getattr(item, key).val, f"Mismatch for {key} in item {item_ob.id}: {val} != {getattr(item, key).val}") def test_clean_item_after_reset(self): # use the separate env new_env = nmmo.Env(self.config, RANDOM_SEED) # reset the environment after running new_env.reset() for _ in tqdm(range(TEST_HORIZON)): new_env.step({}) new_env.reset() # items are referenced in the realm.items, which must be empty self.assertTrue(len(new_env.realm.items) == 0) self.assertTrue(len(new_env.realm.exchange._item_listings) == 0) self.assertTrue(len(new_env.realm.exchange._listings_queue) == 0) # item state table must be empty after reset self.assertTrue(ItemState.State.table(new_env.realm.datastore).is_empty()) def test_truncated(self): test_horizon = 25 config = Config() config.set("HORIZON", test_horizon) env = nmmo.Env(config, RANDOM_SEED) obs, _ = env.reset() for _ in tqdm(range(test_horizon)): obs, _, terminated, truncated, _ = env.step({}) for agent_id in obs: alive = agent_id in env.realm.players self.assertEqual(terminated[agent_id], not alive) # Test that the last step is truncated self.assertEqual(truncated[agent_id], alive and env.realm.tick >= test_horizon) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_game_api.py ================================================ # pylint: disable=protected-access import unittest import nmmo from nmmo.core.game_api import AgentTraining, TeamTraining, TeamBattle from nmmo.lib.team_helper import TeamHelper NUM_TEAMS = 16 TEAM_SIZE = 8 class TeamConfig(nmmo.config.Small, nmmo.config.AllGameSystems): PLAYER_N = NUM_TEAMS * TEAM_SIZE TEAMS = {"Team" + str(i+1): [i*TEAM_SIZE+j+1 for j in range(TEAM_SIZE)] for i in range(NUM_TEAMS)} CURRICULUM_FILE_PATH = "tests/task/sample_curriculum.pkl" class TestGameApi(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = TeamConfig() cls.env = nmmo.Env(cls.config) def test_num_agents_in_teams(self): # raise error if PLAYER_N is not equal to the number of agents in TEAMS config = TeamConfig() config.set("PLAYER_N", 127) env = nmmo.Env(config) self.assertRaises(AssertionError, lambda: TeamTraining(env)) def test_agent_training_game(self): game = AgentTraining(self.env) self.env.reset(game=game) # this should use the DefaultGame setup self.assertTrue(isinstance(self.env.game, AgentTraining)) for task in self.env.tasks: self.assertEqual(task.reward_to, "agent") # all tasks are for agents # every agent is assigned a task self.assertEqual(len(self.env.possible_agents), len(self.env.tasks)) # for the training tasks, the task assignee and subject should be the same for task in self.env.tasks: self.assertEqual(task.assignee, task.subject) # winners should be none when not determined self.assertEqual(self.env.game.winners, None) self.assertEqual(self.env.game.is_over, False) # make agent 1 a winner by destroying all other agents for agent_id in self.env.possible_agents[1:]: self.env.realm.players[agent_id].resources.health.update(0) self.env.step({}) self.assertEqual(self.env.game.winners, [1]) # when there are winners, the game is over self.assertEqual(self.env.game.is_over, True) def test_team_training_game_spawn(self): # when TEAMS is set, the possible agents should include all agents team_helper = TeamHelper(self.config.TEAMS) self.assertListEqual(self.env.possible_agents, list(team_helper.team_and_position_for_agent.keys())) game = TeamTraining(self.env) self.env.reset(game=game) for task in self.env.tasks: self.assertEqual(task.reward_to, "team") # all tasks are for teams # agents in the same team should spawn together team_locs = {} for team_id, team_members in self.env.config.TEAMS.items(): team_locs[team_id] = self.env.realm.players[team_members[0]].pos for agent_id in team_members: self.assertEqual(team_locs[team_id], self.env.realm.players[agent_id].pos) # teams should be apart from each other for team_a in self.config.TEAMS.keys(): for team_b in self.config.TEAMS.keys(): if team_a != team_b: self.assertNotEqual(team_locs[team_a], team_locs[team_b]) def test_team_battle_mode(self): game = TeamBattle(self.env) self.env.reset(game=game) env = self.env # battle mode: all teams share the same task task_spec_name = env.tasks[0].spec_name for task in env.tasks: self.assertEqual(task.reward_to, "team") # all tasks are for teams self.assertEqual(task.spec_name, task_spec_name) # all tasks are the same in competition # set the first team to win winner_team = "Team1" for team_id, members in env.config.TEAMS.items(): if team_id != winner_team: for agent_id in members: env.realm.players[agent_id].resources.health.update(0) env.step({}) self.assertEqual(env.game.winners, env.config.TEAMS[winner_team]) def test_competition_winner_task_completed(self): game = TeamBattle(self.env) self.env.reset(game=game) # The first two tasks get completed winners = [] for task in self.env.tasks[:2]: task._completed_tick = 1 self.assertEqual(task.completed, True) winners += task.assignee self.env.step({}) self.assertEqual(self.env.game.winners, winners) def test_game_via_config(self): config = TeamConfig() config.set("GAME_PACKS", [(AgentTraining, 1), (TeamTraining, 1), (TeamBattle, 1)]) env = nmmo.Env(config) env.reset() for _ in range(3): env.step({}) self.assertTrue(isinstance(env.game, game_cls) for game_cls in [AgentTraining, TeamTraining, TeamBattle]) def test_game_set_next_task(self): game = AgentTraining(self.env) tasks = game._define_tasks() # sample tasks for testing game.set_next_tasks(tasks) self.env.reset(game=game) # The tasks are successfully fed into the env for a, b in zip(tasks, self.env.tasks): self.assertIs(a, b) # The next tasks is empty self.assertIsNone(game._next_tasks) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_gym_obs_spaces.py ================================================ import unittest from copy import deepcopy import numpy as np import nmmo from nmmo.core.game_api import DefaultGame RANDOM_SEED = np.random.randint(0, 100000) class TestGymObsSpaces(unittest.TestCase): def _is_obs_valid(self, obs_spec, obs): for agent_obs in obs.values(): for key, val in agent_obs.items(): self.assertTrue(obs_spec[key].contains(val), f"Invalid obs format -- key: {key}, val: {val}") def _test_gym_obs_space(self, env): obs_spec = env.observation_space(1) obs, _, _, _, _ = env.step({}) self._is_obs_valid(obs_spec, obs) for agent_obs in obs.values(): if "ActionTargets" in agent_obs: val = agent_obs["ActionTargets"] for atn in nmmo.Action.edges(env.config): if atn.enabled(env.config): for arg in atn.edges: # pylint: disable=not-an-iterable mask_spec = obs_spec["ActionTargets"][atn.__name__][arg.__name__] mask_val = val[atn.__name__][arg.__name__] self.assertTrue(mask_spec.contains(mask_val), "Invalid obs format -- " + \ f"key: {atn.__name__}/{arg.__name__}, val: {mask_val}") return obs def test_env_without_noop(self): config = nmmo.config.Default() config.set("PROVIDE_NOOP_ACTION_TARGET", False) env = nmmo.Env(config) env.reset(seed=1) for _ in range(3): env.step({}) self._test_gym_obs_space(env) def test_env_with_noop(self): config = nmmo.config.Default() config.set("PROVIDE_NOOP_ACTION_TARGET", True) env = nmmo.Env(config) env.reset(seed=1) for _ in range(3): env.step({}) self._test_gym_obs_space(env) def test_env_with_fogmap(self): config = nmmo.config.Default() config.set("PROVIDE_DEATH_FOG_OBS", True) env = nmmo.Env(config) env.reset(seed=1) for _ in range(3): env.step({}) self._test_gym_obs_space(env) def test_system_disable(self): class CustomGame(DefaultGame): def _set_config(self): self.config.reset() self.config.set_for_episode("COMBAT_SYSTEM_ENABLED", False) self.config.set_for_episode("ITEM_SYSTEM_ENABLED", False) self.config.set_for_episode("EXCHANGE_SYSTEM_ENABLED", False) self.config.set_for_episode("COMMUNICATION_SYSTEM_ENABLED", False) config = nmmo.config.Default() env = nmmo.Env(config) # test the default game env.reset() for _ in range(3): env.step({}) self._test_gym_obs_space(env) org_obs_spec = deepcopy(env.observation_space(1)) # test the custom game game = CustomGame(env) env.reset(game=game, seed=RANDOM_SEED) for _ in range(3): env.step({}) new_obs = self._test_gym_obs_space(env) # obs format must match between episodes self._is_obs_valid(org_obs_spec, new_obs) # check if the combat system is disabled for agent_obs in new_obs.values(): self.assertEqual(sum(agent_obs["ActionTargets"]["Attack"]["Target"]), int(config.PROVIDE_NOOP_ACTION_TARGET), f"Incorrect gym obs. seed: {RANDOM_SEED}") if __name__ == "__main__": unittest.main() ================================================ FILE: tests/core/test_map_generation.py ================================================ # pylint: disable=protected-access import unittest import os import shutil import numpy as np import nmmo from nmmo.lib import material class TestMapGeneration(unittest.TestCase): def test_insufficient_maps(self): config = nmmo.config.Small() config.set("PATH_MAPS", "maps/test_map_gen") config.set("MAP_N", 20) # clear the directory path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS) shutil.rmtree(path_maps, ignore_errors=True) # this generates 20 maps nmmo.Env(config) # test if MAP_FORCE_GENERATION can be overriden, when the maps are insufficient config2 = nmmo.config.Small() config2.set("PATH_MAPS", "maps/test_map_gen") # the same map dir config2.set("MAP_N", 30) config2.set("MAP_FORCE_GENERATION", False) test_env = nmmo.Env(config2) test_env.reset(map_id=config.MAP_N) # this should finish without error def test_map_preview(self): class MapConfig( nmmo.config.Small, # no fractal, grass only nmmo.config.Terrain, # water, grass, foilage, stone nmmo.config.Item, # no additional effect on the map nmmo.config.Profession, # add ore, tree, crystal, herb, fish ): PATH_MAPS = 'maps/test_preview' MAP_FORCE_GENERATION = True MAP_GENERATE_PREVIEWS = True config = MapConfig() # clear the directory path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS) shutil.rmtree(path_maps, ignore_errors=True) nmmo.Env(config) # this should finish without error def test_map_reset_from_fractal(self): class MapConfig( nmmo.config.Small, # no fractal, grass only nmmo.config.Terrain, # water, grass, foilage, stone nmmo.config.Item, # no additional effect on the map nmmo.config.Profession, # add ore, tree, crystal, herb, fish ): PATH_MAPS = 'maps/test_fractal' MAP_FORCE_GENERATION = True MAP_RESET_FROM_FRACTAL = True config = MapConfig() self.assertEqual(config.MAP_SIZE, 64) self.assertEqual(config.MAP_CENTER, 32) # clear the directory path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS) shutil.rmtree(path_maps, ignore_errors=True) test_env = nmmo.Env(config) # the fractals should be saved fractal_file = os.path.join(path_maps, config.PATH_FRACTAL_SUFFIX.format(1)) self.assertTrue(os.path.exists(fractal_file)) config = test_env.config map_size = config.MAP_SIZE np_random = test_env._np_random # Return the Grass map config.set_for_episode("TERRAIN_SYSTEM_ENABLED", False) map_dict = test_env._load_map_file() map_array = test_env.realm.map._process_map(map_dict, np_random) self.assertEqual(np.sum(map_array == material.Void.index)+\ np.sum(map_array == material.Grass.index), map_size*map_size) # NOTE: +1 to make the center tile, really the center self.assertEqual((config.MAP_CENTER+1)**2, np.sum(map_array == material.Grass.index)) # Another way to make the grass map (which can place other tiles, if want to) config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) config.set_for_episode("TERRAIN_RESET_TO_GRASS", True) config.set_for_episode("PROFESSION_SYSTEM_ENABLED", False) # harvestalbe tiles config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", False) map_dict = test_env._load_map_file() map_array = test_env.realm.map._process_map(map_dict, np_random) self.assertEqual(np.sum(map_array == material.Void.index)+\ np.sum(map_array == material.Grass.index), map_size*map_size) # NOTE: +1 to make the center tile, really the center self.assertEqual((config.MAP_CENTER+1)**2, np.sum(map_array == material.Grass.index)) # Generate from fractal, but not spawn profession tiles config.reset() config.set_for_episode("PROFESSION_SYSTEM_ENABLED", False) map_dict = test_env._load_map_file() map_array = test_env.realm.map._process_map(map_dict, np_random) self.assertEqual(np.sum(map_array == material.Void.index)+\ np.sum(map_array == material.Grass.index)+\ np.sum(map_array == material.Water.index)+\ np.sum(map_array == material.Stone.index)+\ np.sum(map_array == material.Foilage.index), map_size*map_size) # Use the saved map, but disable stone config.reset() config.set_for_episode("MAP_RESET_FROM_FRACTAL", False) config.set_for_episode("TERRAIN_DISABLE_STONE", True) map_dict = test_env._load_map_file() org_map = map_dict["map"].copy() self.assertTrue("fractal" not in map_dict) map_array = test_env.realm.map._process_map(map_dict, np_random) self.assertTrue(np.sum(org_map == material.Stone.index) > 0) self.assertTrue(np.sum(map_array == material.Stone.index) == 0) # Generate from fractal, test add-on functions config.reset() config.set_for_episode("MAP_RESET_FROM_FRACTAL", True) config.set_for_episode("PROFESSION_SYSTEM_ENABLED", True) config.set_for_episode("TERRAIN_SCATTER_EXTRA_RESOURCES", True) map_dict = test_env._load_map_file() map_array = test_env.realm.map._process_map(map_dict, np_random) # this should finish without error if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_observation_tile.py ================================================ # pylint: disable=protected-access,bad-builtin import unittest from timeit import timeit from collections import defaultdict import numpy as np import nmmo from nmmo.core.tile import TileState from nmmo.entity.entity import EntityState from nmmo.systems.item import ItemState from nmmo.lib.event_log import EventState from nmmo.core.observation import Observation from nmmo.core import action as Action from nmmo.lib import utils from tests.testhelpers import ScriptedAgentTestConfig TileAttr = TileState.State.attr_name_to_col EntityAttr = EntityState.State.attr_name_to_col ItemAttr = ItemState.State.attr_name_to_col EventAttr = EventState.State.attr_name_to_col class TestObservationTile(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = nmmo.config.Default() cls.env = nmmo.Env(cls.config) cls.env.reset(seed=1) for _ in range(3): cls.env.step({}) def test_tile_attr(self): self.assertDictEqual(TileAttr, {'row': 0, 'col': 1, 'material_id': 2}) def test_action_target_consts(self): self.assertEqual(len(Action.Style.edges), 3) self.assertEqual(len(Action.Price.edges), self.config.PRICE_N_OBS) self.assertEqual(len(Action.Token.edges), self.config.COMMUNICATION_NUM_TOKENS) def test_obs_tile_correctness(self): center = self.config.PLAYER_VISION_RADIUS tile_dim = self.config.PLAYER_VISION_DIAMETER self.env._compute_observations() obs = self.env.obs # pylint: disable=inconsistent-return-statements def correct_tile(agent_obs: Observation, r_delta, c_delta): agent = agent_obs.agent if (0 <= agent.row + r_delta < self.config.MAP_SIZE) & \ (0 <= agent.col + c_delta < self.config.MAP_SIZE): r_cond = (agent_obs.tiles[:,TileState.State.attr_name_to_col["row"]] == agent.row+r_delta) c_cond = (agent_obs.tiles[:,TileState.State.attr_name_to_col["col"]] == agent.col+c_delta) return TileState.parse_array(agent_obs.tiles[r_cond & c_cond][0]) for agent_obs in obs.values(): # check if the tile obs size self.assertEqual(len(agent_obs.tiles), self.config.MAP_N_OBS) # check if the coord conversion is correct row_map = agent_obs.tiles[:,TileAttr['row']].reshape(tile_dim,tile_dim) col_map = agent_obs.tiles[:,TileAttr['col']].reshape(tile_dim,tile_dim) mat_map = agent_obs.tiles[:,TileAttr['material_id']].reshape(tile_dim,tile_dim) agent = agent_obs.agent self.assertEqual(agent.row, row_map[center,center]) self.assertEqual(agent.col, col_map[center,center]) self.assertEqual(agent_obs.tile(0,0).material_id, mat_map[center,center]) # pylint: disable=not-an-iterable for d in Action.Direction.edges: self.assertTrue(np.array_equal(correct_tile(agent_obs, *d.delta), agent_obs.tile(*d.delta))) print('---test_correct_tile---') print('reference:', timeit(lambda: correct_tile(agent_obs, *d.delta), number=1000, globals=globals())) print('implemented:', timeit(lambda: agent_obs.tile(*d.delta), number=1000, globals=globals())) def test_env_visible_tiles_correctness(self): def correct_visible_tile(realm, agent_id): # Based on numpy datatable window query assert agent_id in realm.players, "agent_id not in the realm" agent = realm.players[agent_id] radius = realm.config.PLAYER_VISION_RADIUS return TileState.Query.window( realm.datastore, agent.row.val, agent.col.val, radius) # implemented in the env._compute_observations() def visible_tiles_by_index(realm, agent_id, tile_map): assert agent_id in realm.players, "agent_id not in the realm" agent = realm.players[agent_id] radius = realm.config.PLAYER_VISION_RADIUS return tile_map[agent.row.val-radius:agent.row.val+radius+1, agent.col.val-radius:agent.col.val+radius+1,:].reshape(225,3) # get tile map, to bypass the expensive tile window query tile_map = TileState.Query.get_map(self.env.realm.datastore, self.config.MAP_SIZE) self.env._compute_observations() obs = self.env.obs for agent_id in self.env.realm.players: self.assertTrue(np.array_equal(correct_visible_tile(self.env.realm, agent_id), obs[agent_id].tiles)) print('---test_visible_tile_window---') print('reference:', timeit(lambda: correct_visible_tile(self.env.realm, agent_id), number=1000, globals=globals())) print('implemented:', timeit(lambda: visible_tiles_by_index(self.env.realm, agent_id, tile_map), number=1000, globals=globals())) def test_make_attack_mask_within_range(self): def correct_within_range(entities, attack_range, agent_row, agent_col): entities_pos = entities[:,[EntityAttr["row"],EntityAttr["col"]]] within_range = utils.linf(entities_pos,(agent_row, agent_col)) <= attack_range return within_range # implemented in the Observation._make_attack_mask() def simple_within_range(entities, attack_range, agent_row, agent_col): return np.maximum( np.abs(entities[:,EntityAttr["row"]] - agent_row), np.abs(entities[:,EntityAttr["col"]] - agent_col) ) <= attack_range self.env._compute_observations() obs = self.env.obs attack_range = self.config.COMBAT_MELEE_REACH for agent_obs in obs.values(): entities = agent_obs.entities.values agent = agent_obs.agent self.assertTrue(np.array_equal( correct_within_range(entities, attack_range, agent.row, agent.col), simple_within_range(entities, attack_range, agent.row, agent.col))) print('---test_attack_within_range---') print('reference:', timeit( lambda: correct_within_range(entities, attack_range, agent.row, agent.col), number=1000, globals=globals())) print('implemented:', timeit( lambda: simple_within_range(entities, attack_range, agent.row, agent.col), number=1000, globals=globals())) def test_gs_where_in_1d(self): config = ScriptedAgentTestConfig() env = nmmo.Env(config) env.reset(seed=0) for _ in range(5): env.step({}) def correct_where_in_1d(event_data, subject): flt_idx = np.in1d(event_data[:, EventAttr['ent_id']], subject) return event_data[flt_idx] def where_in_1d_with_index(event_data, subject, index): flt_idx = [row for sbj in subject for row in index.get(sbj,[])] return event_data[flt_idx] event_data = EventState.Query.table(env.realm.datastore) event_index = defaultdict() for row, id_ in enumerate(event_data[:,EventAttr['ent_id']]): if id_ in event_index: event_index[id_].append(row) else: event_index[id_] = [row] # NOTE: the index-based approach returns the data in different order, # and all the operations in the task system don't use the order info def sort_event_data(event_data): keys = [event_data[:,i] for i in range(1,8)] sorted_idx = np.lexsort(keys) return event_data[sorted_idx] arr1 = sort_event_data(correct_where_in_1d(event_data, [1,2,3])) arr2 = sort_event_data(where_in_1d_with_index(event_data, [1,2,3], event_index)) self.assertTrue(np.array_equal(arr1, arr2)) print('---test_gs_where_in_1d---') print('reference:', timeit( lambda: correct_where_in_1d(event_data, [1, 2, 3]), number=1000, globals=globals())) print('implemented:', timeit( lambda: where_in_1d_with_index(event_data, [1, 2, 3], event_index), number=1000, globals=globals())) if __name__ == '__main__': unittest.main() # from tests.testhelpers import profile_env_step # profile_env_step() # config = nmmo.config.Default() # env = nmmo.Env(config) # env.reset() # for _ in range(10): # env.step({}) # obs = env._compute_observations() # NOTE: the most of performance gain in _make_move_mask comes from the improved tile # test_func = [ # '_make_move_mask()', # 0.170 -> 0.012 # '_make_attack_mask()', # 0.060 -> 0.037 # '_make_use_mask()', # 0.0036 -> # '_make_sell_mask()', # '_make_give_target_mask()', # '_make_destroy_item_mask()', # '_make_buy_mask()', # 0.022 -> 0.011 # '_make_give_gold_mask()', # '_existing_ammo_listings()', # 'agent()', # 'tile(1,-1)' # 0.020 (cache off) -> 0.012 # ] # for func in test_func: # print(func, timeit(f'obs[1].{func}', number=1000, globals=globals())) ================================================ FILE: tests/core/test_tile_property.py ================================================ import unittest import copy import nmmo from scripted.baselines import Sleeper HORIZON = 32 class TestTileProperty(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = nmmo.config.Default() cls.config.PLAYERS = [Sleeper] env = nmmo.Env(cls.config) env.reset() cls.start = copy.deepcopy(env.realm) for _ in range(HORIZON): env.step({}) cls.end = copy.deepcopy(env.realm) # Test immutable invariants assumed for certain optimizations def test_fixed_habitability_passability(self): # Used in optimization with habitability lookup table start_habitable = [tile.habitable for tile in self.start.map.tiles.flatten()] end_habitable = [tile.habitable for tile in self.end.map.tiles.flatten()] self.assertListEqual(start_habitable, end_habitable) # Used in optimization that caches the result of A* start_passable = [tile.impassible for tile in self.start.map.tiles.flatten()] end_passable = [tile.impassible for tile in self.end.map.tiles.flatten()] self.assertListEqual(start_passable, end_passable) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/core/test_tile_seize.py ================================================ # pylint: disable=protected-access import unittest import numpy as np import nmmo import nmmo.core.map from nmmo.core.tile import Tile, TileState from nmmo.datastore.numpy_datastore import NumpyDatastore from nmmo.lib import material class MockRealm: def __init__(self): self.datastore = NumpyDatastore() self.datastore.register_object_type("Tile", TileState.State.num_attributes) self.config = nmmo.config.Small() self._np_random = np.random self.tick = 0 self.event_log = None class MockTask: def __init__(self, ent_id): self.assignee = (ent_id,) class MockEntity: def __init__(self, ent_id): self.ent_id = ent_id self.my_task = None if ent_id > 0: # only for players self.my_task = MockTask(ent_id) class TestTileSeize(unittest.TestCase): # pylint: disable=no-member def test_tile(self): mock_realm = MockRealm() np_random = np.random tile = Tile(mock_realm, 10, 20, np_random) tile.reset(material.Foilage, nmmo.config.Small(), np_random) self.assertEqual(tile.row.val, 10) self.assertEqual(tile.col.val, 20) self.assertEqual(tile.material_id.val, material.Foilage.index) self.assertEqual(tile.seize_history, []) mock_realm.tick = 1 tile.add_entity(MockEntity(1)) self.assertEqual(tile.occupied, True) tile.update_seize() self.assertEqual(tile.seize_history[-1], (1, 1)) # Agent 1 stayed, so no change mock_realm.tick = 2 tile.update_seize() self.assertEqual(tile.seize_history[-1], (1, 1)) # Two agents occupy the tile, so no change mock_realm.tick = 3 tile.add_entity(MockEntity(2)) self.assertCountEqual(tile.entities.keys(), [1, 2]) self.assertEqual(tile.occupied, True) tile.update_seize() self.assertEqual(tile.seize_history[-1], (1, 1)) mock_realm.tick = 5 tile.remove_entity(1) self.assertCountEqual(tile.entities.keys(), [2]) self.assertEqual(tile.occupied, True) tile.update_seize() self.assertEqual(tile.seize_history[-1], (2, 5)) # new seize history # Two agents occupy the tile, so no change mock_realm.tick = 7 tile.add_entity(MockEntity(-10)) self.assertListEqual(list(tile.entities.keys()), [2, -10]) self.assertEqual(tile.occupied, True) tile.update_seize() self.assertEqual(tile.seize_history[-1], (2, 5)) # Should not change when occupied by an npc mock_realm.tick = 9 tile.remove_entity(2) self.assertListEqual(list(tile.entities.keys()), [-10]) self.assertEqual(tile.occupied, True) tile.update_seize() self.assertEqual(tile.seize_history[-1], (2, 5)) tile.harvest(True) self.assertEqual(tile.depleted, True) self.assertEqual(tile.material_id.val, material.Scrub.index) def test_map_seize_targets(self): mock_realm = MockRealm() config = mock_realm.config np_random = mock_realm._np_random map_dict = {"map": np.ones((config.MAP_SIZE, config.MAP_SIZE))*2} # all grass tiles center_tile = (config.MAP_SIZE//2, config.MAP_SIZE//2) test_map = nmmo.core.map.Map(config, mock_realm, np_random) test_map.reset(map_dict, np_random, seize_targets=["center"]) self.assertListEqual(test_map.seize_targets, [center_tile]) self.assertDictEqual(test_map.seize_status, {}) mock_realm.tick = 4 test_map.tiles[center_tile].add_entity(MockEntity(5)) test_map.step() self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)}) # ent_id, tick mock_realm.tick = 6 test_map.tiles[center_tile].remove_entity(5) test_map.step() self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)}) # should not change mock_realm.tick = 9 test_map.tiles[center_tile].add_entity(MockEntity(6)) test_map.tiles[center_tile].add_entity(MockEntity(-7)) test_map.step() self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)}) # should not change mock_realm.tick = 11 test_map.tiles[center_tile].remove_entity(6) # so that -7 is the only entity test_map.step() self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)}) # should not change mock_realm.tick = 14 test_map.tiles[center_tile].remove_entity(-7) test_map.tiles[center_tile].add_entity(MockEntity(10)) test_map.step() self.assertDictEqual(test_map.seize_status, {center_tile: (10, 14)}) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/datastore/test_datastore.py ================================================ import unittest import numpy as np from nmmo.datastore.numpy_datastore import NumpyDatastore class TestDatastore(unittest.TestCase): def testdatastore_record(self): datastore = NumpyDatastore() datastore.register_object_type("TestObject", 2) c1 = 0 c2 = 1 o = datastore.create_record("TestObject") self.assertEqual([o.get(c1), o.get(c2)], [0, 0]) o.update(c1, 1) o.update(c2, 2) self.assertEqual([o.get(c1), o.get(c2)], [1, 2]) np.testing.assert_array_equal( datastore.table("TestObject").get([o.id]), np.array([[1, 2]])) o2 = datastore.create_record("TestObject") o2.update(c2, 2) np.testing.assert_array_equal( datastore.table("TestObject").get([o.id, o2.id]), np.array([[1, 2], [0, 2]])) np.testing.assert_array_equal( datastore.table("TestObject").where_eq(c2, 2), np.array([[1, 2], [0, 2]])) o.delete() np.testing.assert_array_equal( datastore.table("TestObject").where_eq(c2, 2), np.array([[0, 2]])) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/datastore/test_id_allocator.py ================================================ import unittest from nmmo.datastore.id_allocator import IdAllocator class TestIdAllocator(unittest.TestCase): def test_id_allocator(self): id_allocator = IdAllocator(10) for i in range(1, 10): row_id = id_allocator.allocate() self.assertEqual(i, row_id) self.assertTrue(id_allocator.full()) id_allocator.remove(5) id_allocator.remove(6) id_allocator.remove(1) self.assertFalse(id_allocator.full()) self.assertSetEqual( set(id_allocator.allocate() for i in range(3)), set([5, 6, 1]) ) self.assertTrue(id_allocator.full()) id_allocator.expand(11) self.assertFalse(id_allocator.full()) self.assertEqual(id_allocator.allocate(), 10) with self.assertRaises(KeyError): id_allocator.allocate() def test_id_reuse(self): id_allocator = IdAllocator(10) for i in range(1, 10): row_id = id_allocator.allocate() self.assertEqual(i, row_id) self.assertTrue(id_allocator.full()) id_allocator.remove(5) id_allocator.remove(6) id_allocator.remove(1) self.assertFalse(id_allocator.full()) self.assertSetEqual( set(id_allocator.allocate() for i in range(3)), set([5, 6, 1]) ) self.assertTrue(id_allocator.full()) id_allocator.expand(11) self.assertFalse(id_allocator.full()) self.assertEqual(id_allocator.allocate(), 10) with self.assertRaises(KeyError): id_allocator.allocate() id_allocator.remove(10) self.assertEqual(id_allocator.allocate(), 10) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/datastore/test_numpy_datastore.py ================================================ import unittest import numpy as np from nmmo.datastore.numpy_datastore import NumpyTable # pylint: disable=protected-access class TestNumpyTable(unittest.TestCase): def test_continous_table(self): table = NumpyTable(3, 10, np.float32) table.update(2, 0, 2.1) table.update(2, 1, 2.2) table.update(5, 0, 5.1) table.update(5, 2, 5.3) np.testing.assert_array_equal( table.get([1,2,5]), np.array([[0, 0, 0], [2.1, 2.2, 0], [5.1, 0, 5.3]], dtype=np.float32) ) def test_discrete_table(self): table = NumpyTable(3, 10, np.int32) table.update(2, 0, 11) table.update(2, 1, 12) table.update(5, 0, 51) table.update(5, 2, 53) np.testing.assert_array_equal( table.get([1,2,5]), np.array([[0, 0, 0], [11, 12, 0], [51, 0, 53]], dtype=np.int32) ) def test_expand(self): table = NumpyTable(3, 10, np.float32) table.update(2, 0, 2.1) with self.assertRaises(IndexError): table.update(10, 0, 10.1) table._expand(11) table.update(10, 0, 10.1) np.testing.assert_array_equal( table.get([10, 2]), np.array([[10.1, 0, 0], [2.1, 0, 0]], dtype=np.float32) ) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/datastore/test_serialized.py ================================================ from collections import defaultdict import unittest from nmmo.datastore.serialized import SerializedState # pylint: disable=no-member,unused-argument,unsubscriptable-object FooState = SerializedState.subclass("FooState", [ "a", "b", "col" ]) FooState.Limits = { "a": (-10, 10), } class MockDatastoreRecord(): def __init__(self): self._data = defaultdict(lambda: 0) def get(self, name): return self._data[name] def update(self, name, value): self._data[name] = value class MockDatastore(): def create_record(self, name): return MockDatastoreRecord() def register_object_type(self, name, attributes): assert name == "FooState" assert attributes == ["a", "b", "col"] class TestSerialized(unittest.TestCase): def test_serialized(self): state = FooState(MockDatastore(), FooState.Limits) # initial value = 0 self.assertEqual(state.a.val, 0) # if given value is within the range, set to the value state.a.update(1) self.assertEqual(state.a.val, 1) # if given a lower value than the min, set to min a_min, a_max = FooState.Limits["a"] state.a.update(a_min - 100) self.assertEqual(state.a.val, a_min) # if given a higher value than the max, set to max state.a.update(a_max + 100) self.assertEqual(state.a.val, a_max) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/render/test_load_replay.py ================================================ '''Manual test for rendering replay''' if __name__ == '__main__': import time # pylint: disable=import-error from nmmo.render.render_client import DummyRenderer from nmmo.render.replay_helper import FileReplayHelper # open a client renderer = DummyRenderer() time.sleep(3) # load a replay: replace 'replay_dev.json' with your replay file replay = FileReplayHelper.load('replay_dev.json') # run the replay for packet in replay: renderer.render_packet(packet) time.sleep(1) ================================================ FILE: tests/render/test_render_save.py ================================================ # Deprecated test; old render system '''Manual test for render client connectivity and save replay import nmmo from nmmo.core.config import (AllGameSystems, Combat, Communication, Equipment, Exchange, Item, Medium, Profession, Progression, Resource, Small, Terrain) from nmmo.render.render_client import DummyRenderer from nmmo.render.replay_helper import FileReplayHelper from scripted import baselines def create_config(base, nent, *systems): systems = (base, *systems) name = '_'.join(cls.__name__ for cls in systems) conf = type(name, systems, {})() conf.TERRAIN_TRAIN_MAPS = 1 conf.TERRAIN_EVAL_MAPS = 1 conf.IMMORTAL = True conf.PLAYER_N = nent conf.PLAYERS = [baselines.Random] return conf no_npc_small_1_pop_conf = create_config(Small, 1, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) no_npc_med_1_pop_conf = create_config(Medium, 1, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) no_npc_med_100_pop_conf = create_config(Medium, 100, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) all_small_1_pop_conf = create_config(Small, 1, AllGameSystems) all_med_1_pop_conf = create_config(Medium, 1, AllGameSystems) all_med_100_pop_conf = create_config(Medium, 100, AllGameSystems) conf_dict = { 'no_npc_small_1_pop': no_npc_small_1_pop_conf, 'no_npc_med_1_pop': no_npc_med_1_pop_conf, 'no_npc_med_100_pop': no_npc_med_100_pop_conf, 'all_small_1_pop': all_small_1_pop_conf, 'all_med_1_pop': all_med_1_pop_conf, 'all_med_100_pop': all_med_100_pop_conf } if __name__ == '__main__': import random from tqdm import tqdm TEST_HORIZON = 100 RANDOM_SEED = random.randint(0, 9999) replay_helper = FileReplayHelper() # the renderer is external to the env, so need to manually initiate it renderer = DummyRenderer() for conf_name, config in conf_dict.items(): env = nmmo.Env(config) # to make replay, one should create replay_helper # and run the below line env.realm.record_replay(replay_helper) env.reset(seed=RANDOM_SEED) renderer.set_realm(env.realm) for tick in tqdm(range(TEST_HORIZON)): env.step({}) renderer.render_realm() # NOTE: save the data in uncompressed json format, since # the web client has trouble loading the compressed replay file replay_helper.save(f'replay_{conf_name}_seed_{RANDOM_SEED:04d}.json') ''' ================================================ FILE: tests/systems/test_exchange.py ================================================ # pylint: disable=unnecessary-lambda,protected-access,no-member from types import SimpleNamespace import unittest import numpy as np import nmmo from nmmo.datastore.numpy_datastore import NumpyDatastore from nmmo.systems.exchange import Exchange from nmmo.systems.item import ItemState from nmmo.systems import item class MockRealm: def __init__(self): self.config = nmmo.config.Default() self.config.EXCHANGE_LISTING_DURATION = 3 self.datastore = NumpyDatastore() self.items = {} self.datastore.register_object_type("Item", ItemState.State.num_attributes) self.tick = 0 class MockEntity: def __init__(self) -> None: self.items = [] self.inventory = SimpleNamespace( receive = lambda item: self.items.append(item), remove = lambda item: self.items.remove(item) ) class TestExchange(unittest.TestCase): def test_listings(self): realm = MockRealm() exchange = Exchange(realm) entity_1 = MockEntity() hat_1 = item.Hat(realm, 1) hat_2 = item.Hat(realm, 10) entity_1.inventory.receive(hat_1) entity_1.inventory.receive(hat_2) self.assertEqual(len(entity_1.items), 2) tick = realm.tick = 0 exchange._list_item(hat_1, entity_1, 10, tick) self.assertEqual(len(exchange._item_listings), 1) self.assertEqual(exchange._listings_queue[0], (hat_1.id.val, 0)) tick = realm.tick = 1 exchange._list_item(hat_2, entity_1, 20, tick) self.assertEqual(len(exchange._item_listings), 2) self.assertEqual(exchange._listings_queue[0], (hat_1.id.val, 0)) tick = realm.tick = 4 exchange.step() # hat_1 should expire and not be listed self.assertEqual(len(exchange._item_listings), 1) self.assertEqual(exchange._listings_queue[0], (hat_2.id.val, 1)) tick = realm.tick = 5 exchange._list_item(hat_2, entity_1, 10, tick) exchange.step() # hat_2 got re-listed, so should still be listed self.assertEqual(len(exchange._item_listings), 1) self.assertEqual(exchange._listings_queue[0], (hat_2.id.val, 5)) tick = realm.tick = 10 exchange.step() self.assertEqual(len(exchange._item_listings), 0) def test_for_sale_items(self): realm = MockRealm() exchange = Exchange(realm) entity_1 = MockEntity() hat_1 = item.Hat(realm, 1) hat_2 = item.Hat(realm, 10) exchange._list_item(hat_1, entity_1, 10, 0) exchange._list_item(hat_2, entity_1, 20, 10) np.testing.assert_array_equal( item.Item.Query.for_sale(realm.datastore)[:,0], [hat_1.id.val, hat_2.id.val]) # first listing should expire realm.tick = 10 exchange.step() np.testing.assert_array_equal( item.Item.Query.for_sale(realm.datastore)[:,0], [hat_2.id.val]) # second listing should expire realm.tick = 100 exchange.step() np.testing.assert_array_equal( item.Item.Query.for_sale(realm.datastore)[:,0], []) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/systems/test_item.py ================================================ import unittest import numpy as np import nmmo from nmmo.datastore.numpy_datastore import NumpyDatastore from nmmo.systems.item import Hat, Top, ItemState from nmmo.systems.exchange import Exchange class MockRealm: def __init__(self): self.config = nmmo.config.Default() self.datastore = NumpyDatastore() self.items = {} self.exchange = Exchange(self) self.datastore.register_object_type("Item", ItemState.State.num_attributes) self.players = {} # pylint: disable=no-member class TestItem(unittest.TestCase): def test_item(self): realm = MockRealm() hat_1 = Hat(realm, 1) self.assertTrue(ItemState.Query.by_id(realm.datastore, hat_1.id.val) is not None) self.assertEqual(hat_1.type_id.val, Hat.ITEM_TYPE_ID) self.assertEqual(hat_1.level.val, 1) self.assertEqual(hat_1.mage_defense.val, realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE) hat_2 = Hat(realm, 10) self.assertTrue(ItemState.Query.by_id(realm.datastore, hat_2.id.val) is not None) self.assertEqual(hat_2.level.val, 10) self.assertEqual(hat_2.melee_defense.val, hat_2.level.val * realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE) self.assertDictEqual(realm.items, {hat_1.id.val: hat_1, hat_2.id.val: hat_2}) # also test destroy ids = [hat_1.id.val, hat_2.id.val] hat_1.destroy() hat_2.destroy() # after destroy(), the datastore entry is gone, but the class still exsits # make sure that after destroy the owner_id is 0, at least self.assertTrue(hat_1.owner_id.val == 0) self.assertTrue(hat_2.owner_id.val == 0) for item_id in ids: self.assertTrue(len(ItemState.Query.by_id(realm.datastore, item_id)) == 0) self.assertDictEqual(realm.items, {}) # create a new item with the hat_1's id, but it must still be void new_top = Top(realm, 3) new_top.id.update(ids[0]) # hat_1's id new_top.owner_id.update(100) # make sure that the hat_1 is not linked to the new_top self.assertTrue(hat_1.owner_id.val == 0) def test_owned_by(self): realm = MockRealm() hat_1 = Hat(realm, 1) hat_2 = Hat(realm, 10) hat_1.owner_id.update(1) hat_2.owner_id.update(1) np.testing.assert_array_equal( ItemState.Query.owned_by(realm.datastore, 1)[:,0], [hat_1.id.val, hat_2.id.val]) self.assertEqual(Hat.Query.owned_by(realm.datastore, 2).size, 0) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/systems/test_skill_level.py ================================================ import unittest import numpy as np import nmmo import nmmo.systems.skill from tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv class TestSkillLevel(unittest.TestCase): @classmethod def setUpClass(cls): cls.config = ScriptedAgentTestConfig() cls.config.set("PROGRESSION_EXP_THRESHOLD", [0, 10, 20, 30, 40, 50]) cls.config.set("PROGRESSION_LEVEL_MAX", len(cls.config.PROGRESSION_EXP_THRESHOLD)) cls.env = ScriptedAgentTestEnv(cls.config) def test_experience_calculator(self): exp_calculator = nmmo.systems.skill.ExperienceCalculator(self.config) self.assertTrue(np.array_equal(self.config.PROGRESSION_EXP_THRESHOLD, exp_calculator.exp_threshold)) for level in range(1, self.config.PROGRESSION_LEVEL_MAX + 1): self.assertEqual(exp_calculator.level_at_exp(exp_calculator.exp_at_level(level)), level) self.assertEqual(exp_calculator.exp_at_level(-1), # invalid level min(self.config.PROGRESSION_EXP_THRESHOLD)) self.assertEqual(exp_calculator.exp_at_level(30), # level above the max max(self.config.PROGRESSION_EXP_THRESHOLD)) self.assertEqual(exp_calculator.level_at_exp(0), 1) self.assertEqual(exp_calculator.level_at_exp(5), 1) self.assertEqual(exp_calculator.level_at_exp(45), 5) self.assertEqual(exp_calculator.level_at_exp(50), 6) self.assertEqual(exp_calculator.level_at_exp(100), 6) def test_add_xp(self): self.env.reset() player = self.env.realm.players[1] skill_list = ["melee", "range", "mage", "fishing", "herbalism", "prospecting", "carving", "alchemy"] # check the initial levels and exp for skill in skill_list: self.assertEqual(getattr(player.skills, skill).level.val, 1) self.assertEqual(getattr(player.skills, skill).exp.val, 0) # add 1 exp to melee, does NOT level up player.skills.melee.add_xp(1) for skill in skill_list: if skill == "melee": self.assertEqual(getattr(player.skills, skill).level.val, 1) self.assertEqual(getattr(player.skills, skill).exp.val, 1) else: self.assertEqual(getattr(player.skills, skill).level.val, 1) self.assertEqual(getattr(player.skills, skill).exp.val, 0) # add 30 exp to fishing, levels up to 3 player.skills.fishing.add_xp(30) for skill in skill_list: if skill == "melee": self.assertEqual(getattr(player.skills, skill).level.val, 1) self.assertEqual(getattr(player.skills, skill).exp.val, 1) elif skill == "fishing": self.assertEqual(getattr(player.skills, skill).level.val, 4) self.assertEqual(getattr(player.skills, skill).exp.val, 30) else: self.assertEqual(getattr(player.skills, skill).level.val, 1) self.assertEqual(getattr(player.skills, skill).exp.val, 0) if __name__ == '__main__': unittest.main() # config = nmmo.config.Default() # exp_calculator = nmmo.systems.skill.ExperienceCalculator(config) # print(exp_calculator.exp_threshold) # print(exp_calculator.exp_at_level(10)) # print(exp_calculator.level_at_exp(150)) # 2 # print(exp_calculator.level_at_exp(300)) # 3 # print(exp_calculator.level_at_exp(1000)) # 7 ================================================ FILE: tests/task/test_demo_task_creation.py ================================================ # pylint: disable=invalid-name,unused-argument,unused-variable import unittest from tests.testhelpers import ScriptedAgentTestConfig from nmmo.core.env import Env from nmmo.lib.event_code import EventCode from nmmo.systems import skill from nmmo.task import predicate_api as p from nmmo.task import task_api as t from nmmo.task import task_spec as ts from nmmo.task import base_predicates as bp from nmmo.task.game_state import GameState from nmmo.task.group import Group def rollout(env, tasks, steps=5): env.reset(make_task_fn=lambda: tasks) for _ in range(steps): env.step({}) return env.step({}) class TestDemoTask(unittest.TestCase): def test_baseline_tasks(self): # Tasks from # https://github.com/NeuralMMO/baselines/ # blob/4c1088d2bbe0f74a08dcf7d71b714cd30772557f/tasks.py class Tier: REWARD_SCALE = 15 EASY = 4 / REWARD_SCALE NORMAL = 6 / REWARD_SCALE HARD = 11 / REWARD_SCALE # Predicates defined below can be evaluated over one agent or several agents, # which are sepcified separately # Reward multiplier is indendent from predicates and used by tasks. # The multipliers are just shown to indicate the difficulty level of predicates # Usage of base predicates (see nmmo/task/base_predicates.py) player_kills = [ # (predicate, kwargs, reward_multiplier) (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 1}, Tier.EASY), (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 2}, Tier.NORMAL), (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 3}, Tier.HARD)] exploration = [ # (predicate, reward_multiplier) (bp.DistanceTraveled, {'dist': 16}, Tier.EASY), (bp.DistanceTraveled, {'dist': 32}, Tier.NORMAL), (bp.DistanceTraveled, {'dist': 64}, Tier.HARD)] # Demonstrates custom predicate - return float/boolean def EquipmentLevel(gs: GameState, subject: Group, number: int): equipped = subject.item.equipped > 0 levels = subject.item.level[equipped] return levels.sum() >= number equipment = [ # (predicate, reward_multiplier) (EquipmentLevel, {'number': 1}, Tier.EASY), (EquipmentLevel, {'number': 5}, Tier.NORMAL), (EquipmentLevel, {'number': 10}, Tier.HARD)] def CombatSkill(gs, subject, lvl): # OR on predicate functions: max over all progress return max(bp.AttainSkill(gs, subject, skill.Melee, lvl, 1), bp.AttainSkill(gs, subject, skill.Range, lvl, 1), bp.AttainSkill(gs, subject, skill.Mage, lvl, 1)) combat = [ # (predicate, reward_multiplier) (CombatSkill, {'lvl': 2}, Tier.EASY), (CombatSkill, {'lvl': 3}, Tier.NORMAL), (CombatSkill, {'lvl': 4}, Tier.HARD)] def ForageSkill(gs, subject, lvl): return max(bp.AttainSkill(gs, subject, skill.Fishing, lvl, 1), bp.AttainSkill(gs, subject, skill.Herbalism, lvl, 1), bp.AttainSkill(gs, subject, skill.Prospecting, lvl, 1), bp.AttainSkill(gs, subject, skill.Carving, lvl, 1), bp.AttainSkill(gs, subject, skill.Alchemy, lvl, 1)) foraging = [ # (predicate, reward_multiplier) (ForageSkill, {'lvl': 2}, Tier.EASY), (ForageSkill, {'lvl': 3}, Tier.NORMAL), (ForageSkill, {'lvl': 4}, Tier.HARD)] # Test rollout config = ScriptedAgentTestConfig() config.set("ALLOW_MULTI_TASKS_PER_AGENT", True) env = Env(config) # Creating and testing "team" tasks # i.e., predicates are evalauated over all team members, # and all team members get the same reward from each task # The team mapping can come from anywhere. # The below is an arbitrary example and even doesn't include all agents teams = {0: [1, 2, 3, 4], 1: [5, 6, 7, 8]} # Making player_kills and exploration team tasks, team_tasks = [] for pred_fn, kwargs, weight in player_kills + exploration: pred_cls = p.make_predicate(pred_fn) for team in teams.values(): team_tasks.append( pred_cls(Group(team), **kwargs).create_task(reward_multiplier=weight)) # Run the environment with these tasks # check rewards and infos for the task info obs, rewards, terminated, truncated, infos = rollout(env, team_tasks) # Creating and testing the same task for all agents # i.e, each agent gets evaluated and rewarded individually same_tasks = [] for pred_fn, kwargs, weight in exploration + equipment + combat + foraging: pred_cls = p.make_predicate(pred_fn) for agent_id in env.possible_agents: same_tasks.append( pred_cls(Group([agent_id]), **kwargs).create_task(reward_multiplier=weight)) # Run the environment with these tasks # check rewards and infos for the task info obs, rewards, terminated, truncated, infos = rollout(env, same_tasks) # DONE def test_player_kill_reward(self): # pylint: disable=no-value-for-parameter """ Design a predicate with a complex progress scheme """ config = ScriptedAgentTestConfig() env = Env(config) # PARTICIPANT WRITES # ==================================== def KillPredicate(gs: GameState, subject: Group): """The progress, the max of which is 1, should * increase small for each player kill * increase big for the 1st and 3rd kills * reach 1 with 10 kills """ num_kills = len(subject.event.PLAYER_KILL) progress = num_kills * 0.06 if num_kills >= 1: progress += .1 if num_kills >= 3: progress += .3 return min(progress, 1.0) # participants don't need to know about Predicate classes kill_pred_cls = p.make_predicate(KillPredicate) kill_tasks = [kill_pred_cls(Group(agent_id)).create_task() for agent_id in env.possible_agents] # Test Reward env.reset(make_task_fn=lambda: kill_tasks) players = env.realm.players code = EventCode.PLAYER_KILL env.realm.event_log.record(code, players[1], target=players[3]) env.realm.event_log.record(code, players[2], target=players[4]) env.realm.event_log.record(code, players[2], target=players[5]) env.realm.event_log.record(EventCode.EAT_FOOD, players[2]) # Award given as designed # Agent 1 kills 1 - reward .06 + .1 # Agent 2 kills 2 - reward .12 + .1 # Agent 3 kills 0 - reward 0 _, rewards, _, _, _ = env.step({}) self.assertEqual(rewards[1], 0.16) self.assertEqual(rewards[2], 0.22) self.assertEqual(rewards[3], 0) # No reward when no changes _, rewards, _, _, _ = env.step({}) self.assertEqual(rewards[1], 0) self.assertEqual(rewards[2], 0) self.assertEqual(rewards[3], 0) # DONE def test_predicate_math(self): # pylint: disable=no-value-for-parameter config = ScriptedAgentTestConfig() env = Env(config) # each predicate function returns float, so one can do math on them def PredicateMath(gs, subject): progress = 0.8 * bp.CountEvent(gs, subject, event='PLAYER_KILL', N=7) + \ 1.1 * bp.TickGE(gs, subject, num_tick=3) # NOTE: the resulting progress will be bounded from [0, 1] afterwards return progress # participants don't need to know about Predicate classes pred_math_cls = p.make_predicate(PredicateMath) task_for_agent_1 = pred_math_cls(Group(1)).create_task() # Test Reward env.reset(make_task_fn=lambda: [task_for_agent_1]) code = EventCode.PLAYER_KILL players = env.realm.players env.realm.event_log.record(code, players[1], target=players[2]) env.realm.event_log.record(code, players[1], target=players[3]) _, rewards, _, _, _ = env.step({}) self.assertAlmostEqual(rewards[1], 0.8*2/7 + 1.1*1/3) for _ in range(2): _, _, _, _, infos = env.step({}) # 0.8*2/7 + 1.1 > 1, but the progress is maxed at 1 self.assertEqual(infos[1]['task'][env.tasks[0].name]['progress'], 1.0) self.assertTrue(env.tasks[0].completed) # because progress >= 1 # DONE def test_task_spec_based_curriculum(self): task_spec = [ ts.TaskSpec(eval_fn=bp.CountEvent, eval_fn_kwargs={'event': 'PLAYER_KILL', 'N': 1}, reward_to='team'), ts.TaskSpec(eval_fn=bp.CountEvent, eval_fn_kwargs={'event': 'PLAYER_KILL', 'N': 2}, reward_to='agent'), ts.TaskSpec(eval_fn=bp.AllDead, eval_fn_kwargs={'target': 'left_team'}, reward_to='agent'), ts.TaskSpec(eval_fn=bp.CanSeeAgent, eval_fn_kwargs={'target': 'right_team_leader'}, task_cls=t.OngoingTask, reward_to='team'), ] # NOTE: len(teams) and len(task_spec) don't need to match teams = {1:[1,2,3], 3:[4,5], 6:[6,7], 9:[8,9], 14:[10,11]} config = ScriptedAgentTestConfig() env = Env(config) env.reset(make_task_fn=lambda: ts.make_task_from_spec(teams, task_spec)) self.assertEqual(len(env.tasks), 6) # 6 tasks were created self.assertEqual(env.tasks[0].name, # team 0 task assigned to agents 1,2,3 '(Task_eval_fn:(CountEvent_(1,2,3)_event:PLAYER_KILL_N:1)_assignee:(1,2,3))') self.assertEqual(env.tasks[1].name, # team 1, agent task assigned to agent 4 '(Task_eval_fn:(CountEvent_(4,)_event:PLAYER_KILL_N:2)_assignee:(4,))') self.assertEqual(env.tasks[2].name, # team 1, agent task assigned to agent 5 '(Task_eval_fn:(CountEvent_(5,)_event:PLAYER_KILL_N:2)_assignee:(5,))') self.assertEqual(env.tasks[3].name, # team 2, agent 6 task, left_team is team 3 (agents 8,9) '(Task_eval_fn:(AllDead_(8,9))_assignee:(6,))') self.assertEqual(env.tasks[5].name, # team 3 task, right_team is team 2 (6,7), leader 6 '(OngoingTask_eval_fn:(CanSeeAgent_(8,9)_target:6)_assignee:(8,9))') for _ in range(2): env.step({}) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/task/test_manual_curriculum.py ================================================ '''Manual test for creating learning curriculum manually''' # pylint: disable=invalid-name,redefined-outer-name,bad-builtin # pylint: disable=wildcard-import,unused-wildcard-import from typing import List import nmmo.lib.material as m import nmmo.systems.item as i import nmmo.systems.skill as s from nmmo.task.base_predicates import * from nmmo.task.task_api import OngoingTask from nmmo.task.task_spec import TaskSpec, check_task_spec EVENT_NUMBER_GOAL = [3, 4, 5, 7, 9, 12, 15, 20, 30, 50] INFREQUENT_GOAL = list(range(1, 10)) STAY_ALIVE_GOAL = [50, 100, 150, 200, 300, 500] TEAM_NUMBER_GOAL = [10, 20, 30, 50, 70, 100] LEVEL_GOAL = list(range(1, 10)) # TODO: get config AGENT_NUM_GOAL = [1, 2, 3, 4, 5] # competition team size: 8 ITEM_NUM_GOAL = AGENT_NUM_GOAL TEAM_ITEM_GOAL = [1, 3, 5, 7, 10, 15, 20] SKILLS = s.COMBAT_SKILL + s.HARVEST_SKILL COMBAT_STYLE = s.COMBAT_SKILL ALL_ITEM = i.ALL_ITEM EQUIP_ITEM = i.ARMOR + i.WEAPON + i.TOOL + i.AMMUNITION HARVEST_ITEM = i.WEAPON + i.AMMUNITION + i.CONSUMABLE task_spec: List[TaskSpec] = [] # explore, eat, drink, attack any agent, harvest any item, level up any skill # which can happen frequently essential_skills = ['GO_FARTHEST', 'EAT_FOOD', 'DRINK_WATER', 'SCORE_HIT', 'HARVEST_ITEM', 'LEVEL_UP'] for event_code in essential_skills: for cnt in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=CountEvent, eval_fn_kwargs={'event': event_code, 'N': cnt}, sampling_weight=30)) # item/market skills, which happen less frequently or should not do too much item_skills = ['CONSUME_ITEM', 'GIVE_ITEM', 'DESTROY_ITEM', 'EQUIP_ITEM', 'GIVE_GOLD', 'LIST_ITEM', 'EARN_GOLD', 'BUY_ITEM'] for event_code in item_skills: task_spec += [TaskSpec(eval_fn=CountEvent, eval_fn_kwargs={'event': event_code, 'N': cnt}) for cnt in INFREQUENT_GOAL] # less than 10 # find resource tiles for resource in m.Harvestable: for reward_to in ['agent', 'team']: task_spec.append(TaskSpec(eval_fn=CanSeeTile, eval_fn_kwargs={'tile_type': resource}, reward_to=reward_to, sampling_weight=10)) # stay alive ... like ... for 300 ticks # i.e., getting incremental reward for each tick alive as an individual or a team for reward_to in ['agent', 'team']: for num_tick in STAY_ALIVE_GOAL: task_spec.append(TaskSpec(eval_fn=TickGE, eval_fn_kwargs={'num_tick': num_tick}, reward_to=reward_to)) # protect the leader: get reward for each tick the leader is alive # NOTE: a tuple of length four, to pass in the task_kwargs task_spec.append(TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={'target': 'my_team_leader'}, reward_to='team', task_cls=OngoingTask)) # want the other team or team leader to die for target in ['left_team', 'left_team_leader', 'right_team', 'right_team_leader']: task_spec.append(TaskSpec(eval_fn=AllDead, eval_fn_kwargs={'target': target}, reward_to='team')) # occupy the center tile, assuming the Medium map size # TODO: it'd be better to have some intermediate targets toward the center for reward_to in ['agent', 'team']: task_spec.append(TaskSpec(eval_fn=OccupyTile, eval_fn_kwargs={'row': 80, 'col': 80}, reward_to=reward_to)) # TODO: get config for map size # form a tight formation, for a certain number of ticks def PracticeFormation(gs, subject, dist, num_tick): return AllMembersWithinRange(gs, subject, dist) * TickGE(gs, subject, num_tick) for dist in [1, 3, 5, 10]: task_spec += [TaskSpec(eval_fn=PracticeFormation, eval_fn_kwargs={'dist': dist, 'num_tick': num_tick}, reward_to='team') for num_tick in STAY_ALIVE_GOAL] # find the other team leader for reward_to in ['agent', 'team']: for target in ['left_team_leader', 'right_team_leader']: task_spec.append(TaskSpec(eval_fn=CanSeeAgent, eval_fn_kwargs={'target': target}, reward_to=reward_to)) # find the other team (any agent) for reward_to in ['agent']: #, 'team']: for target in ['left_team', 'right_team']: task_spec.append(TaskSpec(eval_fn=CanSeeGroup, eval_fn_kwargs={'target': target}, reward_to=reward_to)) # explore the map -- sum the l-inf distance traveled by all subjects for dist in [10, 20, 30, 50, 100]: # each agent task_spec.append(TaskSpec(eval_fn=DistanceTraveled, eval_fn_kwargs={'dist': dist})) for dist in [30, 50, 70, 100, 150, 200, 300, 500]: # summed over all team members task_spec.append(TaskSpec(eval_fn=DistanceTraveled, eval_fn_kwargs={'dist': dist}, reward_to='team')) # level up a skill for skill in SKILLS: for level in LEVEL_GOAL[1:]: # since this is an agent task, num_agent must be 1 task_spec.append(TaskSpec(eval_fn=AttainSkill, eval_fn_kwargs={'skill': skill, 'level': level, 'num_agent': 1}, reward_to='agent', sampling_weight=10*(5-level) if level < 5 else 1)) # make attain skill a team task by varying the number of agents for skill in SKILLS: for level in LEVEL_GOAL[1:]: for num_agent in AGENT_NUM_GOAL: if level + num_agent <= 6 or num_agent == 1: # heuristic prune task_spec.append( TaskSpec(eval_fn=AttainSkill, eval_fn_kwargs={'skill': skill, 'level': level, 'num_agent': num_agent}, reward_to='team')) # practice specific combat style for style in COMBAT_STYLE: for cnt in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=ScoreHit, eval_fn_kwargs={'combat_style': style, 'N': cnt}, sampling_weight=5)) for cnt in TEAM_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=ScoreHit, eval_fn_kwargs={'combat_style': style, 'N': cnt}, reward_to='team')) # defeat agents of a certain level as a team for agent_type in ['player', 'npc']: # c.AGENT_TYPE_CONSTRAINT for level in LEVEL_GOAL: for num_agent in AGENT_NUM_GOAL: if level + num_agent <= 6 or num_agent == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=DefeatEntity, eval_fn_kwargs={'agent_type': agent_type, 'level': level, 'num_agent': num_agent}, reward_to='team')) # hoarding gold -- evaluated on the current gold for amount in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=HoardGold, eval_fn_kwargs={'amount': amount}, sampling_weight=3)) for amount in TEAM_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=HoardGold, eval_fn_kwargs={'amount': amount}, reward_to='team')) # earning gold -- evaluated on the total gold earned by selling items # does NOT include looted gold for amount in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=EarnGold, eval_fn_kwargs={'amount': amount}, sampling_weight=3)) for amount in TEAM_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=EarnGold, eval_fn_kwargs={'amount': amount}, reward_to='team')) # spending gold, by buying items for amount in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=SpendGold, eval_fn_kwargs={'amount': amount}, sampling_weight=3)) for amount in TEAM_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=SpendGold, eval_fn_kwargs={'amount': amount}, reward_to='team')) # making profits by trading -- only buying and selling are counted for amount in EVENT_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=MakeProfit, eval_fn_kwargs={'amount': amount}, sampling_weight=3)) for amount in TEAM_NUMBER_GOAL: task_spec.append(TaskSpec(eval_fn=MakeProfit, eval_fn_kwargs={'amount': amount}, reward_to='team')) # managing inventory space def PracticeInventoryManagement(gs, subject, space, num_tick): return InventorySpaceGE(gs, subject, space) * TickGE(gs, subject, num_tick) for space in [2, 4, 8]: task_spec += [TaskSpec(eval_fn=PracticeInventoryManagement, eval_fn_kwargs={'space': space, 'num_tick': num_tick}) for num_tick in STAY_ALIVE_GOAL] # own item, evaluated on the current inventory for item in ALL_ITEM: for level in LEVEL_GOAL: # agent task for quantity in ITEM_NUM_GOAL: if level + quantity <= 6 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=OwnItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, sampling_weight=4-level if level < 4 else 1)) # team task for quantity in TEAM_ITEM_GOAL: if level + quantity <= 10 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=OwnItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, reward_to='team')) # equip item, evaluated on the current inventory and equipment status for item in EQUIP_ITEM: for level in LEVEL_GOAL: # agent task task_spec.append(TaskSpec(eval_fn=EquipItem, eval_fn_kwargs={'item': item, 'level': level, 'num_agent': 1}, sampling_weight=4-level if level < 4 else 1)) # team task for num_agent in AGENT_NUM_GOAL: if level + num_agent <= 6 or num_agent == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=EquipItem, eval_fn_kwargs={'item': item, 'level': level, 'num_agent': num_agent}, reward_to='team')) # consume items (ration, potion), evaluated based on the event log for item in i.CONSUMABLE: for level in LEVEL_GOAL: # agent task for quantity in ITEM_NUM_GOAL: if level + quantity <= 6 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=ConsumeItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, sampling_weight=4-level if level < 4 else 1)) # team task for quantity in TEAM_ITEM_GOAL: if level + quantity <= 10 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=ConsumeItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, reward_to='team')) # harvest items, evaluated based on the event log for item in HARVEST_ITEM: for level in LEVEL_GOAL: # agent task for quantity in ITEM_NUM_GOAL: if level + quantity <= 6 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=HarvestItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, sampling_weight=4-level if level < 4 else 1)) # team task for quantity in TEAM_ITEM_GOAL: if level + quantity <= 10 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=HarvestItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, reward_to='team')) # list items, evaluated based on the event log for item in ALL_ITEM: for level in LEVEL_GOAL: # agent task for quantity in ITEM_NUM_GOAL: if level + quantity <= 6 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=ListItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, sampling_weight=4-level if level < 4 else 1)) # team task for quantity in TEAM_ITEM_GOAL: if level + quantity <= 10 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=ListItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, reward_to='team')) # buy items, evaluated based on the event log for item in ALL_ITEM: for level in LEVEL_GOAL: # agent task for quantity in ITEM_NUM_GOAL: if level + quantity <= 6 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=BuyItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, sampling_weight=4-level if level < 4 else 1)) # team task for quantity in TEAM_ITEM_GOAL: if level + quantity <= 10 or quantity == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=BuyItem, eval_fn_kwargs={'item': item, 'level': level, 'quantity': quantity}, reward_to='team')) # fully armed, evaluated based on the current player/inventory status for style in COMBAT_STYLE: for level in LEVEL_GOAL: for num_agent in AGENT_NUM_GOAL: if level + num_agent <= 6 or num_agent == 1: # heuristic prune task_spec.append(TaskSpec(eval_fn=FullyArmed, eval_fn_kwargs={'combat_style': style, 'level': level, 'num_agent': num_agent}, reward_to='team')) if __name__ == '__main__': import psutil from contextlib import contextmanager import multiprocessing as mp import numpy as np import dill @contextmanager def create_pool(num_proc): pool = mp.Pool(processes=num_proc) yield pool pool.close() pool.join() # 3495 task specs: divide the specs into chunks num_workers = round(psutil.cpu_count(logical=False)*0.7) spec_chunks = np.array_split(task_spec, num_workers) with create_pool(num_workers) as pool: chunk_results = pool.map(check_task_spec, spec_chunks) num_error = 0 for results in chunk_results: for result in results: if result["runnable"] is False: print("ERROR: ", result["spec_name"]) num_error += 1 print("Total number of errors: ", num_error) # test if the task spec is pickalable with open('sample_curriculum.pkl', 'wb') as f: dill.dump(task_spec, f, recurse=True) ================================================ FILE: tests/task/test_predicates.py ================================================ import unittest from typing import List, Tuple, Union, Iterable import random from tests.testhelpers import ScriptedAgentTestConfig, provide_item from tests.testhelpers import change_spawn_pos as change_agent_pos from scripted.baselines import Sleeper from nmmo.entity.entity import EntityState from nmmo.systems import item as Item from nmmo.systems import skill as Skill from nmmo.lib import material as Material from nmmo.lib.event_code import EventCode # pylint: disable=import-error from nmmo.core.env import Env from nmmo.task.predicate_api import Predicate, make_predicate from nmmo.task.task_api import OngoingTask from nmmo.task.group import Group import nmmo.task.base_predicates as bp # use the constant reward of 1 for testing predicates NUM_AGENT = 6 ALL_AGENT = list(range(1, NUM_AGENT+1)) class TestBasePredicate(unittest.TestCase): # pylint: disable=protected-access,no-member,invalid-name def _get_taskenv(self, test_preds: List[Tuple[Predicate, Union[Iterable[int], int]]], grass_map=False): config = ScriptedAgentTestConfig() config.set("PLAYERS", [Sleeper]) config.set("PLAYER_N", NUM_AGENT) config.set("IMMORTAL", True) config.set("ALLOW_MULTI_TASKS_PER_AGENT", True) # OngoingTask keeps evaluating and returns progress as the reward # vs. Task stops evaluating once the task is completed, returns reward = delta(progress) test_tasks = [OngoingTask(pred, assignee) for pred, assignee in test_preds] env = Env(config) env.reset(make_task_fn=lambda: test_tasks) if grass_map: MS = env.config.MAP_SIZE # Change entire map to grass to become habitable for i in range(MS): for j in range(MS): tile = env.realm.map.tiles[i,j] tile.material = Material.Grass tile.material_id.update(Material.Grass.index) tile.state = Material.Grass(env.config) return env def _check_result(self, env, test_preds, infos, true_task): for tid, (predicate, assignee) in enumerate(test_preds): # result is cached when at least one assignee is alive so that the task is evaled if len(set(assignee) & set(infos)) > 0: self.assertEqual(int(env.game_state.cache_result[predicate.name]), int(tid in true_task)) for ent_id in infos: if ent_id in assignee: # the agents that are assigned the task get evaluated for reward self.assertEqual(int(infos[ent_id]['task'][env.tasks[tid].name]['reward']), int(tid in true_task)) else: # the agents that are not assigned the task are not evaluated self.assertTrue(env.tasks[tid].name not in infos[ent_id]['task']) def _check_progress(self, task, infos, value): """ Tasks return a float in the range 0-1 indicating completion progress. """ for ent_id in infos: if ent_id in task.assignee: self.assertAlmostEqual(infos[ent_id]['task'][task.name]['progress'],value) def test_tickge_stay_alive_rip(self): tickge_pred_cls = make_predicate(bp.TickGE) stay_alive_pred_cls = make_predicate(bp.StayAlive) all_dead_pred_cls = make_predicate(bp.AllDead) tick_true = 5 death_note = [1, 2, 3] test_preds = [ # (instantiated predicate, task assignee) (tickge_pred_cls(Group([1]), tick_true), ALL_AGENT), (stay_alive_pred_cls(Group([1,3])), ALL_AGENT), (stay_alive_pred_cls(Group([3,4])), [1,2]), (stay_alive_pred_cls(Group([4])), [5,6]), (all_dead_pred_cls(Group([1,3])), ALL_AGENT), (all_dead_pred_cls(Group([3,4])), [1,2]), (all_dead_pred_cls(Group([4])), [5,6])] env = self._get_taskenv(test_preds) for _ in range(tick_true-1): _, _, _, _, infos = env.step({}) # TickGE_5 is false. All agents are alive, # so all StayAlive (ti in [1,2,3]) tasks are true # and all AllDead tasks (ti in [4, 5, 6]) are false true_task = [1, 2, 3] self._check_result(env, test_preds, infos, true_task) self._check_progress(env.tasks[0], infos, (tick_true-1) / tick_true) # kill agents 1-3 for ent_id in death_note: env.realm.players[ent_id].resources.health.update(0) # 6th tick _, _, _, _, infos = env.step({}) # those who have survived entities = EntityState.Query.table(env.realm.datastore) entities = list(entities[:, EntityState.State.attr_name_to_col['id']]) # ent_ids # make sure the dead agents are not in the realm & datastore for ent_id in env.realm.players: if ent_id in death_note: # make sure that dead players not in the realm nor the datastore self.assertTrue(ent_id not in env.realm.players) self.assertTrue(ent_id not in entities) # TickGE_5 is true. Agents 1-3 are dead, so # StayAlive(1,3) and StayAlive(3,4) are false, StayAlive(4) is true # AllDead(1,3) is true, AllDead(3,4) and AllDead(4) are false true_task = [0, 3, 4] self._check_result(env, test_preds, infos, true_task) # 3 is dead but 4 is alive. Half of agents killed, 50% completion. self._check_progress(env.tasks[5], infos, 0.5) # DONE def test_can_see_tile(self): canseetile_pred_cls = make_predicate(bp.CanSeeTile) a1_target = Material.Foilage a2_target = Material.Water test_preds = [ # (instantiated predicate, task assignee) (canseetile_pred_cls(Group([1]), a1_target), ALL_AGENT), # True (canseetile_pred_cls(Group([1,3,5]), a2_target), ALL_AGENT), # False (canseetile_pred_cls(Group([2]), a2_target), [1,2,3]), # True (canseetile_pred_cls(Group([2,5,6]), a1_target), ALL_AGENT), # False (canseetile_pred_cls(Group(ALL_AGENT), a2_target), [2,3,4])] # True # setup env with all grass map env = self._get_taskenv(test_preds, grass_map=True) # Two corners to the target materials BORDER = env.config.MAP_BORDER MS = env.config.MAP_CENTER + BORDER tile = env.realm.map.tiles[BORDER,MS-2] tile.material = Material.Foilage tile.material_id.update(Material.Foilage.index) tile = env.realm.map.tiles[MS-1,BORDER] tile.material = Material.Water tile.material_id.update(Material.Water.index) # All agents to one corner for ent_id in env.realm.players: change_agent_pos(env.realm,ent_id,(BORDER,BORDER)) _, _, _, _, infos = env.step({}) # no target tiles are found, so all are false true_task = [] self._check_result(env, test_preds, infos, true_task) # Team one to foilage, team two to water change_agent_pos(env.realm,1,(BORDER,MS-2)) # agent 1, team 0, foilage change_agent_pos(env.realm,2,(MS-2,BORDER)) # agent 2, team 1, water _, _, _, _, infos = env.step({}) # t0, t2, t4 are true true_task = [0, 2, 4] self._check_result(env, test_preds, infos, true_task) # DONE def test_can_see_agent(self): cansee_agent_pred_cls = make_predicate(bp.CanSeeAgent) cansee_group_pred_cls = make_predicate(bp.CanSeeGroup) search_target = 1 test_preds = [ # (Predicate, Team), the reward is 1 by default (cansee_agent_pred_cls(Group([1]), search_target), ALL_AGENT), # Always True (cansee_agent_pred_cls(Group([2]), search_target), [2,3,4]), # False -> True -> True (cansee_agent_pred_cls(Group([3,4,5]), search_target), [1,2,3]), # False -> False -> True (cansee_group_pred_cls(Group([1]), [3,4]), ALL_AGENT)] # False -> False -> True env = self._get_taskenv(test_preds, grass_map=True) # All agents to one corner BORDER = env.config.MAP_BORDER MS = env.config.MAP_CENTER + BORDER for ent_id in env.realm.players: change_agent_pos(env.realm,ent_id,(BORDER,BORDER)) # the map border # Teleport agent 1 to the opposite corner change_agent_pos(env.realm,1,(MS-2,MS-2)) _, _, _, _, infos = env.step({}) # Only CanSeeAgent(Group([1]), search_target) is true, others are false true_task = [0] self._check_result(env, test_preds, infos, true_task) # Teleport agent 2 to agent 1's pos change_agent_pos(env.realm,2,(MS-2,MS-2)) _, _, _, _, infos = env.step({}) # SearchAgent(Team([2]), search_target) is also true true_task = [0,1] self._check_result(env, test_preds, infos, true_task) # Teleport agent 3 to agent 1s position change_agent_pos(env.realm,3,(MS-2,MS-2)) _, _, _, _, infos = env.step({}) true_task = [0,1,2,3] self._check_result(env, test_preds, infos, true_task) # DONE def test_occupy_tile(self): occupy_tile_pred_cls = make_predicate(bp.OccupyTile) target_tile = (30, 30) test_preds = [ # (Predicate, Team), the reward is 1 by default (occupy_tile_pred_cls(Group([1]), *target_tile), ALL_AGENT), # False -> True (occupy_tile_pred_cls(Group([1,2,3]), *target_tile), [4,5,6]), # False -> True (occupy_tile_pred_cls(Group([2]), *target_tile), [2,3,4]), # False (occupy_tile_pred_cls(Group([3,4,5]), *target_tile), [1,2,3])] # False # make all tiles habitable env = self._get_taskenv(test_preds, grass_map=True) # All agents to one corner BORDER = env.config.MAP_BORDER for ent_id in env.realm.players: change_agent_pos(env.realm,ent_id,(BORDER,BORDER)) _, _, _, _, infos = env.step({}) # all tasks must be false true_task = [] self._check_result(env, test_preds, infos, true_task) # teleport agent 1 to the target tile, agent 2 to the adjacent tile change_agent_pos(env.realm,1,target_tile) change_agent_pos(env.realm,2,(target_tile[0],target_tile[1]-1)) _, _, _, _, infos = env.step({}) # tid 0 and 1 should be true: OccupyTile(Group([1]), *target_tile) # & OccupyTile(Group([1,2,3]), *target_tile) true_task = [0, 1] self._check_result(env, test_preds, infos, true_task) # DONE def test_distance_traveled(self): distance_traveled_pred_cls = make_predicate(bp.DistanceTraveled) agent_dist = 6 team_dist = 10 # NOTE: when evaluating predicates, to whom tasks are assigned are irrelevant test_preds = [ # (Predicate, Team), the reward is 1 by default (distance_traveled_pred_cls(Group([1]), agent_dist), ALL_AGENT), # False -> True (distance_traveled_pred_cls(Group([2, 5]), agent_dist), ALL_AGENT), # False (distance_traveled_pred_cls(Group([3, 4]), agent_dist), ALL_AGENT), # False (distance_traveled_pred_cls(Group([1, 2, 3]), team_dist), ALL_AGENT), # False -> True (distance_traveled_pred_cls(Group([6]), agent_dist), ALL_AGENT)] # False # make all tiles habitable env = self._get_taskenv(test_preds, grass_map=True) _, _, _, _, infos = env.step({}) # one cannot accomplish these goals in the first tick, so all false true_task = [] self._check_result(env, test_preds, infos, true_task) # all are sleeper, so they all stay in the spawn pos spawn_pos = { ent_id: ent.pos for ent_id, ent in env.realm.players.items() } ent_id = 1 # move 6 tiles, to reach the goal change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0]+6, spawn_pos[ent_id][1])) ent_id = 2 # move 2, fail to reach agent_dist, but reach team_dist if add all change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0]+2, spawn_pos[ent_id][1])) ent_id = 3 # move 3, fail to reach agent_dist, but reach team_dist if add all change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0], spawn_pos[ent_id][1]+3)) _, _, _, _, infos = env.step({}) true_task = [0, 3] self._check_result(env, test_preds, infos, true_task) # DONE def test_all_members_within_range(self): within_range_pred_cls = make_predicate(bp.AllMembersWithinRange) dist_123 = 1 dist_135 = 5 test_preds = [ # (Predicate, Team), the reward is 1 by default (within_range_pred_cls(Group([1]), dist_123), ALL_AGENT), # Always true for group of 1 (within_range_pred_cls(Group([1,2]), dist_123), ALL_AGENT), # True (within_range_pred_cls(Group([1,3]), dist_123), ALL_AGENT), # True (within_range_pred_cls(Group([2,3]), dist_123), ALL_AGENT), # False (within_range_pred_cls(Group([1,3,5]), dist_123), ALL_AGENT), # False (within_range_pred_cls(Group([1,3,5]), dist_135), ALL_AGENT), # True (within_range_pred_cls(Group([2,4,6]), dist_135), ALL_AGENT)] # False # make all tiles habitable env = self._get_taskenv(test_preds, grass_map=True) MS = env.config.MAP_SIZE # team 0: staying within goal_dist change_agent_pos(env.realm, 1, (MS//2, MS//2)) change_agent_pos(env.realm, 3, (MS//2-1, MS//2)) # also StayCloseTo a1 = True change_agent_pos(env.realm, 5, (MS//2-5, MS//2)) # team 1: staying goal_dist+1 apart change_agent_pos(env.realm, 2, (MS//2+1, MS//2)) # also StayCloseTo a1 = True change_agent_pos(env.realm, 4, (MS//2+5, MS//2)) change_agent_pos(env.realm, 6, (MS//2+8, MS//2)) _, _, _, _, infos = env.step({}) true_task = [0, 1, 2, 5] self._check_result(env, test_preds, infos, true_task) # DONE def test_attain_skill(self): attain_skill_pred_cls = make_predicate(bp.AttainSkill) goal_level = 5 test_preds = [ # (Predicate, Team), the reward is 1 by default (attain_skill_pred_cls(Group([1]), Skill.Melee, goal_level, 1), ALL_AGENT), # False (attain_skill_pred_cls(Group([2]), Skill.Melee, goal_level, 1), ALL_AGENT), # False (attain_skill_pred_cls(Group([1]), Skill.Range, goal_level, 1), ALL_AGENT), # True (attain_skill_pred_cls(Group([1,3]), Skill.Fishing, goal_level, 1), ALL_AGENT), # True (attain_skill_pred_cls(Group([1,2,3]), Skill.Carving, goal_level, 3), ALL_AGENT), # False (attain_skill_pred_cls(Group([2,4]), Skill.Carving, goal_level, 2), ALL_AGENT)] # True env = self._get_taskenv(test_preds) # AttainSkill(Group([1]), Skill.Melee, goal_level, 1) is false # AttainSkill(Group([2]), Skill.Melee, goal_level, 1) is false env.realm.players[1].skills.melee.level.update(goal_level-1) # AttainSkill(Group([1]), Skill.Range, goal_level, 1) is true env.realm.players[1].skills.range.level.update(goal_level) # AttainSkill(Group([1,3]), Skill.Fishing, goal_level, 1) is true env.realm.players[1].skills.fishing.level.update(goal_level) # AttainSkill(Group([1,2,3]), Skill.Carving, goal_level, 3) is false env.realm.players[1].skills.carving.level.update(goal_level) env.realm.players[2].skills.carving.level.update(goal_level) # AttainSkill(Group([2,4]), Skill.Carving, goal_level, 2) is true env.realm.players[4].skills.carving.level.update(goal_level+2) _, _, _, _, infos = env.step({}) true_task = [2, 3, 5] self._check_result(env, test_preds, infos, true_task) # DONE def test_gain_experience(self): attain_gain_exp_cls = make_predicate(bp.GainExperience) goal_exp = 5 test_preds = [ # (Predicate, Team), the reward is 1 by default (attain_gain_exp_cls(Group([1]), Skill.Melee, goal_exp, 1), ALL_AGENT), # False (attain_gain_exp_cls(Group([2]), Skill.Melee, goal_exp, 1), ALL_AGENT), # False (attain_gain_exp_cls(Group([1]), Skill.Range, goal_exp, 1), ALL_AGENT), # True (attain_gain_exp_cls(Group([1,3]), Skill.Fishing, goal_exp, 1), ALL_AGENT), # True (attain_gain_exp_cls(Group([1,2,3]), Skill.Carving, goal_exp, 3), ALL_AGENT), # False (attain_gain_exp_cls(Group([2,4]), Skill.Carving, goal_exp, 2), ALL_AGENT)] # True env = self._get_taskenv(test_preds) # AttainSkill(Group([1]), Skill.Melee, goal_level, 1) is false # AttainSkill(Group([2]), Skill.Melee, goal_level, 1) is false env.realm.players[1].skills.melee.exp.update(goal_exp-1) # AttainSkill(Group([1]), Skill.Range, goal_level, 1) is true env.realm.players[1].skills.range.exp.update(goal_exp) # AttainSkill(Group([1,3]), Skill.Fishing, goal_level, 1) is true env.realm.players[1].skills.fishing.exp.update(goal_exp) # AttainSkill(Group([1,2,3]), Skill.Carving, goal_level, 3) is false env.realm.players[1].skills.carving.exp.update(goal_exp) env.realm.players[2].skills.carving.exp.update(goal_exp) # AttainSkill(Group([2,4]), Skill.Carving, goal_level, 2) is true env.realm.players[4].skills.carving.exp.update(goal_exp+2) _, _, _, _, infos = env.step({}) true_task = [2, 3, 5] self._check_result(env, test_preds, infos, true_task) # DONE def test_inventory_space_ge_not(self): inv_space_ge_pred_cls = make_predicate(bp.InventorySpaceGE) # also test NOT InventorySpaceGE target_space = 3 test_preds = [ # (Predicate, Team), the reward is 1 by default (inv_space_ge_pred_cls(Group([1]), target_space), ALL_AGENT), # True -> False (inv_space_ge_pred_cls(Group([2,3]), target_space), ALL_AGENT), # True (inv_space_ge_pred_cls(Group([1,2,3]), target_space), ALL_AGENT), # True -> False (inv_space_ge_pred_cls(Group([1,2,3,4]), target_space+1), ALL_AGENT), # False (~inv_space_ge_pred_cls(Group([1]), target_space+1), ALL_AGENT), # True (~inv_space_ge_pred_cls(Group([1,2,3]), target_space), ALL_AGENT), # False -> True (~inv_space_ge_pred_cls(Group([1,2,3,4]), target_space+1), ALL_AGENT)] # True env = self._get_taskenv(test_preds) # add one items to agent 1 within the limit capacity = env.realm.players[1].inventory.capacity provide_item(env.realm, 1, Item.Ration, level=1, quantity=capacity-target_space) _, _, _, _, infos = env.step({}) self.assertTrue(env.realm.players[1].inventory.space >= target_space) true_task = [0, 1, 2, 4, 6] self._check_result(env, test_preds, infos, true_task) # add one more item to agent 1 provide_item(env.realm, 1, Item.Ration, level=1, quantity=1) _, _, _, _, infos = env.step({}) self.assertTrue(env.realm.players[1].inventory.space < target_space) true_task = [1, 4, 5, 6] self._check_result(env, test_preds, infos, true_task) # DONE def test_own_equip_item(self): own_item_pred_cls = make_predicate(bp.OwnItem) equip_item_pred_cls = make_predicate(bp.EquipItem) # ration, level 2, quantity 3 (non-stackable) # ammo level 2, quantity 3 (stackable, equipable) goal_level = 2 goal_quantity = 3 test_preds = [ # (Predicate, Team), the reward is 1 by default (own_item_pred_cls(Group([1]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # False (own_item_pred_cls(Group([2]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # False (own_item_pred_cls(Group([1,2]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # True (own_item_pred_cls(Group([3]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # True (own_item_pred_cls(Group([4,5,6]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # F (equip_item_pred_cls(Group([4]), Item.Whetstone, goal_level, 1), ALL_AGENT), # False (equip_item_pred_cls(Group([4,5]), Item.Whetstone, goal_level, 1), ALL_AGENT), # True (equip_item_pred_cls(Group([4,5,6]), Item.Whetstone, goal_level, 2), ALL_AGENT)] # True env = self._get_taskenv(test_preds) # set the level, so that agents 4-6 can equip the Whetstone equip_stone = [4, 5, 6] for ent_id in equip_stone: env.realm.players[ent_id].skills.melee.level.update(6) # melee skill level=6 # provide items ent_id = 1 # OwnItem(Group([1]), Item.Ration, goal_level, goal_quantity) is false provide_item(env.realm, ent_id, Item.Ration, level=1, quantity=4) provide_item(env.realm, ent_id, Item.Ration, level=2, quantity=2) # OwnItem(Group([2]), Item.Ration, goal_level, goal_quantity) is false ent_id = 2 # OwnItem(Group([1,2]), Item.Ration, goal_level, goal_quantity) is true provide_item(env.realm, ent_id, Item.Ration, level=4, quantity=1) ent_id = 3 # OwnItem(Group([3]), Item.Ration, goal_level, goal_quantity) is true provide_item(env.realm, ent_id, Item.Ration, level=3, quantity=3) # OwnItem(Group([4,5,6]), Item.Ration, goal_level, goal_quantity) is false # provide and equip items ent_id = 4 # EquipItem(Group([4]), Item.Whetstone, goal_level, 1) is false provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=4) ent_id = 5 # EquipItem(Group([4,5]), Item.Whetstone, goal_level, 1) is true provide_item(env.realm, ent_id, Item.Whetstone, level=4, quantity=1) ent_id = 6 # EquipItem(Group([4,5,6]), Item.Whetstone, goal_level, 2) is true provide_item(env.realm, ent_id, Item.Whetstone, level=2, quantity=4) for ent_id in [4, 5, 6]: whetstone = env.realm.players[ent_id].inventory.items[0] whetstone.use(env.realm.players[ent_id]) _, _, _, _, infos = env.step({}) true_task = [2, 3, 6, 7] self._check_result(env, test_preds, infos, true_task) # DONE def test_fully_armed(self): fully_armed_pred_cls = make_predicate(bp.FullyArmed) goal_level = 5 test_preds = [ # (Predicate, Team), the reward is 1 by default (fully_armed_pred_cls(Group([1,2,3]), Skill.Range, goal_level, 1), ALL_AGENT), # False (fully_armed_pred_cls(Group([3,4]), Skill.Range, goal_level, 1), ALL_AGENT), # True (fully_armed_pred_cls(Group([4]), Skill.Melee, goal_level, 1), ALL_AGENT), # False (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level, 3), ALL_AGENT), # True (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level+3, 1), ALL_AGENT), # False (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level, 4), ALL_AGENT)] # False env = self._get_taskenv(test_preds) # fully equip agents 4-6 fully_equip = [4, 5, 6] for ent_id in fully_equip: env.realm.players[ent_id].skills.range.level.update(goal_level+2) # prepare the items item_list = [ itm(env.realm, goal_level) for itm in [ Item.Hat, Item.Top, Item.Bottom, Item.Bow, Item.Arrow]] for itm in item_list: env.realm.players[ent_id].inventory.receive(itm) itm.use(env.realm.players[ent_id]) _, _, _, _, infos = env.step({}) true_task = [1, 3] self._check_result(env, test_preds, infos, true_task) # DONE def test_hoard_gold_and_team(self): # HoardGold, TeamHoardGold hoard_gold_pred_cls = make_predicate(bp.HoardGold) agent_gold_goal = 10 team_gold_goal = 30 test_preds = [ # (Predicate, Team), the reward is 1 by default (hoard_gold_pred_cls(Group([1]), agent_gold_goal), ALL_AGENT), # True (hoard_gold_pred_cls(Group([4,5,6]), agent_gold_goal), ALL_AGENT), # False (hoard_gold_pred_cls(Group([1,3,5]), team_gold_goal), ALL_AGENT), # True (hoard_gold_pred_cls(Group([2,4,6]), team_gold_goal), ALL_AGENT)] # False env = self._get_taskenv(test_preds) # give gold to agents 1-3 gold_struck = [1, 2, 3] for ent_id in gold_struck: env.realm.players[ent_id].gold.update(ent_id * 10) _, _, _, _, infos = env.step({}) true_task = [0, 2] self._check_result(env, test_preds, infos, true_task) g = sum(env.realm.players[eid].gold.val for eid in Group([2,4,6]).agents) self._check_progress(env.tasks[3], infos, g / team_gold_goal) # DONE def test_exchange_gold_predicates(self): # Earn Gold, Spend Gold, Make Profit earn_gold_pred_cls = make_predicate(bp.EarnGold) spend_gold_pred_cls = make_predicate(bp.SpendGold) make_profit_pred_cls = make_predicate(bp.MakeProfit) gold_goal = 10 test_preds = [ (earn_gold_pred_cls(Group([1,2]), gold_goal), ALL_AGENT), # True (earn_gold_pred_cls(Group([2,4]), gold_goal), ALL_AGENT), # False (spend_gold_pred_cls(Group([1]), 5), ALL_AGENT), # False -> True (spend_gold_pred_cls(Group([1]), 6), ALL_AGENT), # False, (make_profit_pred_cls(Group([1,2]), 5), ALL_AGENT), # True, (make_profit_pred_cls(Group([1]), 5), ALL_AGENT) # True -> False ] env = self._get_taskenv(test_preds) players = env.realm.players # 8 gold earned for agent 1 # 2 for agent 2 env.realm.event_log.record(EventCode.EARN_GOLD, players[1], amount = 5) env.realm.event_log.record(EventCode.EARN_GOLD, players[1], amount = 3) env.realm.event_log.record(EventCode.EARN_GOLD, players[2], amount = 2) _, _, _, _, infos = env.step({}) true_task = [0,4,5] self._check_result(env, test_preds, infos, true_task) self._check_progress(env.tasks[1], infos, 2 / gold_goal) env.realm.event_log.record(EventCode.BUY_ITEM, players[1], item=Item.Ration(env.realm,1), price=5) _, _, _, _, infos = env.step({}) true_task = [0,2,4] self._check_result(env, test_preds, infos, true_task) # DONE def test_count_event(self): # CountEvent count_event_pred_cls = make_predicate(bp.CountEvent) test_preds = [ (count_event_pred_cls(Group([1]),"EAT_FOOD",1), ALL_AGENT), # True (count_event_pred_cls(Group([1]),"EAT_FOOD",2), ALL_AGENT), # False (count_event_pred_cls(Group([1]),"DRINK_WATER",1), ALL_AGENT), # False (count_event_pred_cls(Group([1,2]),"GIVE_GOLD",1), ALL_AGENT) # True ] # 1 Drinks water once # 2 Gives gold once env = self._get_taskenv(test_preds) players = env.realm.players env.realm.event_log.record(EventCode.EAT_FOOD, players[1]) env.realm.event_log.record(EventCode.GIVE_GOLD, players[2]) _, _, _, _, infos = env.step({}) true_task = [0,3] self._check_result(env, test_preds, infos, true_task) # DONE def test_score_hit(self): # ScoreHit score_hit_pred_cls = make_predicate(bp.ScoreHit) test_preds = [ (score_hit_pred_cls(Group([1]), Skill.Mage, 2), ALL_AGENT), # False -> True (score_hit_pred_cls(Group([1]), Skill.Melee, 1), ALL_AGENT) # True ] env = self._get_taskenv(test_preds) players = env.realm.players env.realm.event_log.record(EventCode.SCORE_HIT, players[1], target=players[2], combat_style = Skill.Mage, damage=1) env.realm.event_log.record(EventCode.SCORE_HIT, players[1], target=players[2], combat_style = Skill.Melee, damage=1) _, _, _, _, infos = env.step({}) true_task = [1] self._check_result(env, test_preds, infos, true_task) self._check_progress(env.tasks[0], infos, 0.5) env.realm.event_log.record(EventCode.SCORE_HIT, players[1], target=players[2], combat_style = Skill.Mage, damage=1) env.realm.event_log.record(EventCode.SCORE_HIT, players[1], target=players[2], combat_style = Skill.Melee, damage=1) _, _, _, _, infos = env.step({}) true_task = [0,1] self._check_result(env, test_preds, infos, true_task) # DONE def test_defeat_entity(self): # PlayerKill defeat_pred_cls = make_predicate(bp.DefeatEntity) test_preds = [ (defeat_pred_cls(Group([1]), 'npc', level=1, num_agent=1), ALL_AGENT), (defeat_pred_cls(Group([1]), 'player', level=2, num_agent=2), ALL_AGENT)] env = self._get_taskenv(test_preds) players = env.realm.players npcs = env.realm.npcs # set levels npcs[-1].skills.melee.level.update(1) npcs[-1].skills.range.level.update(1) npcs[-1].skills.mage.level.update(1) self.assertEqual(npcs[-1].attack_level, 1) self.assertEqual(players[2].attack_level, 1) players[3].skills.melee.level.update(3) players[4].skills.melee.level.update(2) # killing player 2 does not progress the both tasks env.realm.event_log.record(EventCode.PLAYER_KILL, players[1], target=players[2]) # level 1 player _, _, _, _, infos = env.step({}) true_task = [] # all false self._check_result(env, test_preds, infos, true_task) for task in env.tasks: self._check_progress(task, infos, 0) # killing npc -1 completes the first task env.realm.event_log.record(EventCode.PLAYER_KILL, players[1], target=npcs[-1]) # level 1 npc _, _, _, _, infos = env.step({}) true_task = [0] self._check_result(env, test_preds, infos, true_task) self._check_progress(env.tasks[0], infos, 1) # killing player 3 makes half progress on the second task env.realm.event_log.record(EventCode.PLAYER_KILL, players[1], target=players[3]) # level 3 player _, _, _, _, infos = env.step({}) self._check_progress(env.tasks[1], infos, .5) # killing player 4 completes the second task env.realm.event_log.record(EventCode.PLAYER_KILL, players[1], target=players[4]) # level 2 player _, _, _, _, infos = env.step({}) true_task = [0,1] self._check_result(env, test_preds, infos, true_task) self._check_progress(env.tasks[1], infos, 1) # DONE def test_item_event_predicates(self): # Consume, Harvest, List, Buy for pred_fn, event_type in [(bp.ConsumeItem, 'CONSUME_ITEM'), (bp.HarvestItem, 'HARVEST_ITEM'), (bp.ListItem, 'LIST_ITEM'), (bp.BuyItem, 'BUY_ITEM')]: predicate = make_predicate(pred_fn) id_ = getattr(EventCode, event_type) lvl = random.randint(5,10) quantity = random.randint(5,10) true_item = Item.Ration false_item = Item.Potion test_preds = [ (predicate(Group([1,3,5]), true_item, lvl, quantity), ALL_AGENT), # True (predicate(Group([2]), true_item, lvl, quantity), ALL_AGENT), # False (predicate(Group([4]), true_item, lvl, quantity), ALL_AGENT), # False (predicate(Group([6]), true_item, lvl, quantity), ALL_AGENT) # False ] env = self._get_taskenv(test_preds) players = env.realm.players # True case: split the required items between 3 and 5 for player in (1,3): for _ in range(quantity // 2 + 1): env.realm.event_log.record(id_, players[player], price=1, item=true_item(env.realm, lvl+random.randint(0,3))) # False case 1: Quantity for _ in range(quantity-1): env.realm.event_log.record(id_, players[2], price=1, item=true_item(env.realm, lvl)) # False case 2: Type for _ in range(quantity+1): env.realm.event_log.record(id_, players[4], price=1, item=false_item(env.realm, lvl)) # False case 3: Level for _ in range(quantity+1): env.realm.event_log.record(id_, players[4], price=1, item=true_item(env.realm, random.randint(0,lvl-1))) _, _, _, _, infos = env.step({}) true_task = [0] self._check_result(env, test_preds, infos, true_task) # DONE if __name__ == '__main__': unittest.main() ================================================ FILE: tests/task/test_sample_task_from_file.py ================================================ import unittest import nmmo from tests.testhelpers import ScriptedAgentTestConfig class TestSampleTaskFromFile(unittest.TestCase): def test_sample_task_from_file(self): # init the env with the pickled training task spec config = ScriptedAgentTestConfig() config.CURRICULUM_FILE_PATH = 'tests/task/sample_curriculum.pkl' env = nmmo.Env(config) # env.reset() samples and instantiates a task for each agent # when sample_traning_tasks is set True env.reset() self.assertEqual(len(env.possible_agents), len(env.tasks)) # for the training tasks, the task assignee and subject should be the same for task in env.tasks: self.assertEqual(task.assignee, task.subject) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/task/test_task_api.py ================================================ # pylint: disable=unused-argument,invalid-name import unittest from types import FunctionType import numpy as np import nmmo from nmmo.core.env import Env from nmmo.task.predicate_api import make_predicate, Predicate from nmmo.task.task_api import Task, OngoingTask, HoldDurationTask from nmmo.task.task_spec import TaskSpec, make_task_from_spec from nmmo.task.group import Group from nmmo.task.base_predicates import ( TickGE, AllMembersWithinRange, StayAlive, HoardGold ) from nmmo.systems import item as Item from nmmo.core import action as Action from scripted.baselines import Sleeper from tests.testhelpers import ScriptedAgentTestConfig, change_spawn_pos # define predicates in the function form # with the required signatures: gs, subject def Success(gs, subject: Group): return True def Failure(gs, subject: Group): return False def Fake(gs, subject, a,b,c): return False class MockGameState(): def __init__(self): # pylint: disable=super-init-not-called self.config = nmmo.config.Default() self.current_tick = -1 self.cache_result = {} self.get_subject_view = lambda _: None def clear_cache(self): pass class TestTaskAPI(unittest.TestCase): def test_predicate_operators(self): # pylint: disable=unsupported-binary-operation,invalid-unary-operand-type # pylint: disable=no-value-for-parameter,not-callable,no-member self.assertTrue(isinstance(Success, FunctionType)) self.assertTrue(isinstance(Failure, FunctionType)) # make predicate class from function success_pred_cls = make_predicate(Success) failure_pred_cls = make_predicate(Failure) self.assertTrue(isinstance(success_pred_cls, type)) # class self.assertTrue(isinstance(failure_pred_cls, type)) # then instantiate predicates SUCCESS = success_pred_cls(Group(0)) FAILURE = failure_pred_cls(Group(0)) self.assertTrue(isinstance(SUCCESS, Predicate)) self.assertTrue(isinstance(FAILURE, Predicate)) # NOTE: only the instantiated predicate can be used with operators like below mock_gs = MockGameState() # get the individual predicate"s source code self.assertEqual(SUCCESS.get_source_code(), "def Success(gs, subject: Group):\n return True") self.assertEqual(FAILURE.get_source_code(), "def Failure(gs, subject: Group):\n return False") # AND (&), OR (|), NOT (~) pred1 = SUCCESS & FAILURE self.assertFalse(pred1(mock_gs)) # NOTE: get_source_code() of the combined predicates returns the joined str # of each predicate"s source code, which may NOT represent what the actual # predicate is doing self.assertEqual(pred1.get_source_code(), "def Success(gs, subject: Group):\n return True\n\n"+ "def Failure(gs, subject: Group):\n return False") pred2 = SUCCESS | FAILURE | SUCCESS self.assertTrue(pred2(mock_gs)) self.assertEqual(pred2.get_source_code(), "def Success(gs, subject: Group):\n return True\n\n"+ "def Failure(gs, subject: Group):\n return False\n\n"+ "def Success(gs, subject: Group):\n return True") pred3 = SUCCESS & ~ FAILURE & SUCCESS self.assertTrue(pred3(mock_gs)) # NOTE: demonstrating the above point -- it just returns the functions # NOT what this predicate actually evaluates. self.assertEqual(pred2.get_source_code(), pred3.get_source_code()) # predicate math pred4 = 0.1 * SUCCESS + 0.3 self.assertEqual(pred4(mock_gs), 0.4) self.assertEqual(pred4.name, "(ADD_(MUL_(Success_(0,))_0.1)_0.3)") # NOTE: demonstrating the above point again, -- it just returns the functions # NOT what this predicate actually evaluates. self.assertEqual(pred4.get_source_code(), "def Success(gs, subject: Group):\n return True") pred5 = 0.3 * SUCCESS - 1 self.assertEqual(pred5(mock_gs), 0.0) # cannot go below 0 pred6 = 0.3 * SUCCESS + 1 self.assertEqual(pred6(mock_gs), 1.0) # cannot go over 1 def test_team_assignment(self): team = Group([1, 2, 8, 9], "TeamFoo") self.assertEqual(team.name, "TeamFoo") self.assertEqual(team[2].name, "TeamFoo.2") self.assertEqual(team[2], (8,)) # don"t allow member of one-member team self.assertEqual(team[2][0].name, team[2].name) def test_predicate_name(self): # pylint: disable=no-value-for-parameter,no-member # make predicate class from function success_pred_cls = make_predicate(Success) failure_pred_cls = make_predicate(Failure) fake_pred_cls = make_predicate(Fake) # instantiate the predicates SUCCESS = success_pred_cls(Group([0,2])) FAILURE = failure_pred_cls(Group(0)) fake_pred = fake_pred_cls(Group(2), 1, Item.Hat, Action.Melee) combination = (SUCCESS & ~ (FAILURE | fake_pred)) | (FAILURE * fake_pred + .3) - .4 self.assertEqual(combination.name, "(OR_(AND_(Success_(0,2))_(NOT_(OR_(Failure_(0,))_(Fake_(2,)_1_Hat_Melee))))_"+\ "(SUB_(ADD_(MUL_(Failure_(0,))_(Fake_(2,)_1_Hat_Melee))_0.3)_0.4))") def test_task_api_with_predicate(self): # pylint: disable=no-value-for-parameter,no-member fake_pred_cls = make_predicate(Fake) mock_gs = MockGameState() group = Group(2) item = Item.Hat action = Action.Melee predicate = fake_pred_cls(group, a=1, b=item, c=action) self.assertEqual(predicate.get_source_code(), "def Fake(gs, subject, a,b,c):\n return False") self.assertEqual(predicate.get_signature(), ["gs", "subject", "a", "b", "c"]) self.assertEqual(predicate.args, tuple(group,)) self.assertDictEqual(predicate.kwargs, {"a": 1, "b": item, "c": action}) assignee = [1,2,3] # list of agent ids task = predicate.create_task(assignee=assignee) rewards, infos = task.compute_rewards(mock_gs) self.assertEqual(task.name, # contains predicate name and assignee list "(Task_eval_fn:(Fake_(2,)_a:1_b:Hat_c:Melee)_assignee:(1,2,3))") self.assertEqual(task.get_source_code(), "def Fake(gs, subject, a,b,c):\n return False") self.assertEqual(task.get_signature(), ["gs", "subject", "a", "b", "c"]) self.assertEqual(task.args, tuple(group,)) self.assertDictEqual(task.kwargs, {"a": 1, "b": item, "c": action}) for agent_id in assignee: self.assertEqual(rewards[agent_id], 0) self.assertEqual(infos[agent_id]["progress"], 0) # progress (False -> 0) self.assertFalse(task.completed) def test_task_api_with_function(self): mock_gs = MockGameState() def eval_with_subject_fn(subject: Group): def is_agent_1(gs): return any(agent_id == 1 for agent_id in subject.agents) return is_agent_1 assignee = [1,2,3] # list of agent ids task = Task(eval_with_subject_fn(Group(assignee)), assignee) rewards, infos = task.compute_rewards(mock_gs) self.assertEqual(task.name, # contains predicate name and assignee list "(Task_eval_fn:is_agent_1_assignee:(1,2,3))") self.assertEqual(task.get_source_code(), "def is_agent_1(gs):\n " + "return any(agent_id == 1 for agent_id in subject.agents)") self.assertEqual(task.get_signature(), ["gs"]) self.assertEqual(task.args, []) self.assertDictEqual(task.kwargs, {}) self.assertEqual(task.subject, tuple(assignee)) self.assertEqual(task.assignee, tuple(assignee)) for agent_id in assignee: self.assertEqual(rewards[agent_id], 1) self.assertEqual(infos[agent_id]["progress"], 1) # progress (True -> 1) self.assertTrue(task.completed) def test_predicate_fn_using_other_predicate_fn(self): # define a predicate: to form a tight formation, for a certain number of ticks def PracticeFormation(gs, subject, dist, num_tick): return AllMembersWithinRange(gs, subject, dist) * TickGE(gs, subject, num_tick) # team should stay together within 1 tile for 10 ticks goal_tick = 10 task_spec = TaskSpec(eval_fn=PracticeFormation, eval_fn_kwargs={"dist": 1, "num_tick": goal_tick}, reward_to="team") # create the test task from the task spec teams = {1:[1,2,3], 3:[4,5], 6:[6,7], 9:[8,9], 14:[10,11]} team_ids= list(teams.keys()) config = ScriptedAgentTestConfig() config.set("PLAYERS", [Sleeper]) config.set("IMMORTAL", True) env = Env(config) env.reset(make_task_fn=lambda: make_task_from_spec(teams, [task_spec])) # check the task information task = env.tasks[0] self.assertEqual(task.name, "(Task_eval_fn:(PracticeFormation_(1,2,3)_dist:1_num_tick:10)"+ "_assignee:(1,2,3))") self.assertEqual(task.get_source_code(), "def PracticeFormation(gs, subject, dist, num_tick):\n "+ "return AllMembersWithinRange(gs, subject, dist) * "+ "TickGE(gs, subject, num_tick)") self.assertEqual(task.get_signature(), ["gs", "subject", "dist", "num_tick"]) self.assertEqual(task.subject, tuple(teams[team_ids[0]])) self.assertEqual(task.kwargs, task_spec.eval_fn_kwargs) self.assertEqual(task.assignee, tuple(teams[team_ids[0]])) # check the agent-task map for agent_id, agent_tasks in env.agent_task_map.items(): for task in agent_tasks: self.assertTrue(agent_id in task.assignee) # move agent 2, 3 to agent 1"s pos for agent_id in [2,3]: change_spawn_pos(env.realm, agent_id, env.realm.players[1].pos) for tick in range(goal_tick+2): _, rewards, _, _, infos = env.step({}) if tick < 10: target_reward = 1/goal_tick self.assertAlmostEqual(rewards[1], target_reward) self.assertAlmostEqual((1+tick)/goal_tick, infos[1]["task"][env.tasks[0].name]["progress"]) else: # tick 11, task should be completed self.assertEqual(rewards[1], 0) self.assertEqual(infos[1]["task"][env.tasks[0].name]["progress"], 1) self.assertEqual(infos[1]["task"][env.tasks[0].name]["completed"], True) # test the task_spec_with_embedding task_embedding = np.ones(config.TASK_EMBED_DIM, dtype=np.float16) task_spec_with_embedding = TaskSpec(eval_fn=PracticeFormation, eval_fn_kwargs={"dist": 1, "num_tick": goal_tick}, reward_to="team", embedding=task_embedding) env.reset(make_task_fn=lambda: make_task_from_spec(teams, [task_spec_with_embedding])) task = env.tasks[0] self.assertEqual(task.spec_name, # without the subject and assignee agent ids "Task_PracticeFormation_(dist:1_num_tick:10)_reward_to:team") self.assertEqual(task.name, "(Task_eval_fn:(PracticeFormation_(1,2,3)_dist:1_num_tick:10)"+ "_assignee:(1,2,3))") self.assertEqual(task.get_source_code(), "def PracticeFormation(gs, subject, dist, num_tick):\n "+ "return AllMembersWithinRange(gs, subject, dist) * "+ "TickGE(gs, subject, num_tick)") self.assertEqual(task.get_signature(), ["gs", "subject", "dist", "num_tick"]) self.assertEqual(task.subject, tuple(teams[team_ids[0]])) self.assertEqual(task.kwargs, task_spec.eval_fn_kwargs) self.assertEqual(task.assignee, tuple(teams[team_ids[0]])) self.assertTrue(np.array_equal(task.embedding, task_embedding)) obs_spec = env.observation_space(1) self.assertTrue(obs_spec["Task"].contains(task.embedding)) def test_completed_tasks_in_info(self): # pylint: disable=no-value-for-parameter,no-member config = ScriptedAgentTestConfig() config.set("ALLOW_MULTI_TASKS_PER_AGENT", True) env = Env(config) # make predicate class from function success_pred_cls = make_predicate(Success) failure_pred_cls = make_predicate(Failure) fake_pred_cls = make_predicate(Fake) # instantiate the predicates same_team = [1, 2, 3, 4] predicates = [ success_pred_cls(Group(1)), # task 1 failure_pred_cls(Group(2)), # task 2 fake_pred_cls(Group(3), 1, Item.Hat, Action.Melee), # task 3 success_pred_cls(Group(same_team))] # task 4 # tasks can be created directly from predicate instances test_tasks = [pred.create_task() for pred in predicates] # tasks are all instantiated with the agent ids env.reset(make_task_fn=lambda: test_tasks) _, _, _, _, infos = env.step({}) # agent 1: assigned only task 1, which is always True self.assertEqual(infos[1]["task"][env.tasks[0].name]["reward"], 1.0) for i in [1, 2]: # task 2 and 3 self.assertTrue(env.tasks[i].name not in infos[1]["task"]) # agent 2: assigned task 2 (Failure) and task 4 (Success) self.assertEqual(infos[2]["task"][env.tasks[1].name]["reward"], 0.0) # task 2 self.assertEqual(infos[2]["task"][env.tasks[3].name]["reward"], 1.0) # task 4 # agent 3 assigned task 3, Fake(), which is always False (0) self.assertEqual(infos[3]["task"][env.tasks[2].name]["reward"], 0.0) # task 3 # all agents in the same team with agent 2 have SUCCESS # other agents don"t have any tasks assigned for ent_id in env.possible_agents: if ent_id in same_team: self.assertEqual(infos[ent_id]["task"][env.tasks[3].name]["reward"], 1.0) else: self.assertTrue(env.tasks[3].name not in infos[ent_id]["task"]) # DONE def test_make_task_from_spec(self): teams = {0:[1,2,3], 1:[4,5,6]} test_embedding = np.array([1,2,3]) task_spec = [ TaskSpec(eval_fn=TickGE, eval_fn_kwargs={"num_tick": 20}), TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={}, task_cls=OngoingTask), TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={"target": "my_team_leader"}, task_cls=OngoingTask, reward_to="team"), TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={"target": "left_team"}, task_cls=OngoingTask, task_kwargs={"reward_multiplier": 2}, reward_to="team", embedding=test_embedding), ] task_list = [] # testing each task spec, individually for single_spec in task_spec: task_list.append(make_task_from_spec(teams, [single_spec])) # check the task spec names self.assertEqual(task_list[0][0].spec_name, "Task_TickGE_(num_tick:20)_reward_to:agent") self.assertEqual(task_list[1][0].spec_name, "OngoingTask_StayAlive_()_reward_to:agent") self.assertEqual(task_list[2][0].spec_name, "OngoingTask_StayAlive_(target:my_team_leader)_reward_to:team") self.assertEqual(task_list[3][0].spec_name, "OngoingTask_StayAlive_(target:left_team)_reward_to:team") # check the task names self.assertEqual(task_list[0][0].name, "(Task_eval_fn:(TickGE_(1,)_num_tick:20)_assignee:(1,))") self.assertEqual(task_list[1][0].name, "(OngoingTask_eval_fn:(StayAlive_(1,))_assignee:(1,))") self.assertEqual(task_list[2][0].name, "(OngoingTask_eval_fn:(StayAlive_(1,))_assignee:(1,2,3))") self.assertEqual(task_list[3][0].name, "(OngoingTask_eval_fn:(StayAlive_(4,5,6))_assignee:(1,2,3))") self.assertEqual(task_list[3][0].reward_multiplier, 2) self.assertTrue(np.array_equal(task_list[3][0].embedding, np.array([1,2,3]))) def test_hold_duration_task(self): # pylint: disable=protected-access # each agent should hoard gold for 10 ticks goal_tick = goal_gold = 10 task_spec = [TaskSpec(eval_fn=HoardGold, eval_fn_kwargs={"amount": goal_gold}, task_cls=HoldDurationTask, task_kwargs={"hold_duration": goal_tick})] * 3 config = ScriptedAgentTestConfig() config.PLAYERS =[Sleeper] config.IMMORTAL = True teams = {id: [id] for id in range(1,4)} env = Env(config) env.reset(make_task_fn=lambda: make_task_from_spec(teams, task_spec)) # give agent 1, 2 enough gold for agent_id in [1,2]: env.realm.players[agent_id].gold.update(goal_gold+1) for _ in range(5): env.step({}) # check the task information self.assertEqual(env.tasks[0].spec_name, "HoldDurationTask_HoardGold_(amount:10)_reward_to:agent") for idx in [0, 1]: self.assertEqual(env.tasks[idx]._progress, 0.5) # agent 1 & 2 has enough gold self.assertEqual(env.tasks[idx]._max_progress, 0.5) self.assertEqual(env.tasks[idx].reward_signal_count, 5) self.assertTrue(env.tasks[2]._progress == 0.0) # agent 3 has no gold for task in env.tasks: self.assertTrue(task.completed is False) # not completed yet # take away gold from agent 2 env.realm.players[2].gold.update(goal_gold-1) env.step({}) self.assertEqual(env.tasks[0]._progress, 0.6) # agent 1 has enough gold self.assertEqual(env.tasks[0]._max_progress, 0.6) self.assertEqual(env.tasks[0].reward_signal_count, 6) self.assertEqual(env.tasks[1]._progress, 0) # agent 2 has not enough gold self.assertEqual(env.tasks[1]._max_progress, 0.5) # max values are preserved self.assertEqual(env.tasks[1]._positive_reward_count, 5) self.assertEqual(env.tasks[1].reward_signal_count, 6) # 5 positive + 1 negative for _ in range(4): env.step({}) # only agent 1 successfully held 10 gold for 10 ticks self.assertTrue(env.tasks[0].completed is True) self.assertTrue(env.tasks[1].completed is False) self.assertTrue(env.tasks[2].completed is False) def test_task_spec_with_predicate(self): teams = {0:[1,2,3], 1:[4,5,6]} SUCCESS = make_predicate(Success)(Group(1)) FAILURE = make_predicate(Failure)(Group([2,3])) predicate = SUCCESS & FAILURE predicate.name = "SuccessAndFailure" # make task spec task_spec = [TaskSpec(predicate=predicate, eval_fn=None, eval_fn_kwargs={"success_target": 1, "test_item": Item.Hat})] tasks = make_task_from_spec(teams, task_spec) env = Env(ScriptedAgentTestConfig()) env.reset(make_task_fn=lambda: tasks) env.step({}) # check the task information self.assertEqual(env.tasks[0].spec_name, "Task_SuccessAndFailure_(success_target:1_test_item:Hat)_reward_to:agent") if __name__ == "__main__": unittest.main() ================================================ FILE: tests/task/test_task_system_perf.py ================================================ import unittest import nmmo from nmmo.core.env import Env from nmmo.task.task_api import Task, nmmo_default_task from tests.testhelpers import profile_env_step PROFILE_PERF = False class TestTaskSystemPerf(unittest.TestCase): def test_nmmo_default_task(self): config = nmmo.config.Default() env = Env(config) agent_list = env.possible_agents for test_mode in [None, 'no_task', 'dummy_eval_fn', 'pure_func_eval']: # create tasks if test_mode == 'pure_func_eval': def create_stay_alive_eval_wo_group(agent_id: int): return lambda gs: agent_id in gs.alive_agents tasks = [Task(create_stay_alive_eval_wo_group(agent_id), assignee=agent_id) for agent_id in agent_list] else: tasks = nmmo_default_task(agent_list, test_mode) # check tasks for agent_id in agent_list: if test_mode is None: self.assertTrue('TickGE' in tasks[agent_id-1].name) # default task if test_mode != 'no_task': self.assertTrue(f'assignee:({agent_id},)' in tasks[agent_id-1].name) # pylint: disable=cell-var-from-loop if PROFILE_PERF: test_cond = 'default' if test_mode is None else test_mode profile_env_step(tasks=tasks, condition=test_cond) else: env.reset(make_task_fn=lambda: tasks) for _ in range(3): env.step({}) # DONE if __name__ == '__main__': unittest.main() # """ Tested on Win 11, docker # === Test condition: default (StayAlive-based Predicate) === # - env.step({}): 13.398321460997977 # - env.realm.step(): 3.6524868449996575 # - env._compute_observations(): 3.2038183499971638 # - obs.to_gym(), ActionTarget: 2.30746804500086 # - env._compute_rewards(): 2.7206644940015394 # === Test condition: no_task === # - env.step({}): 10.576253965999058 # - env.realm.step(): 3.674701832998835 # - env._compute_observations(): 3.260661373002222 # - obs.to_gym(), ActionTarget: 2.313872797996737 # - env._compute_rewards(): 0.009020475001307204 # === Test condition: dummy_eval_fn -based Predicate === # - env.step({}): 12.797982947995479 # - env.realm.step(): 3.604593793003005 # - env._compute_observations(): 3.2095355240016943 # - obs.to_gym(), ActionTarget: 2.313207338003849 # - env._compute_rewards(): 2.266267291997792 # === Test condition: pure_func_eval WITHOUT Predicate === # - env.step({}): 10.637560240997118 # - env.realm.step(): 3.633970066999609 # - env._compute_observations(): 3.2308093659958104 # - obs.to_gym(), ActionTarget: 2.331246039000689 # - env._compute_rewards(): 0.0988905300037004 # """ ================================================ FILE: tests/test_death_fog.py ================================================ # pylint: disable=protected-access, no-member import unittest import nmmo class TestDeathFog(unittest.TestCase): def test_death_fog(self): config = nmmo.config.Default() config.set("DEATH_FOG_ONSET", 3) config.set("DEATH_FOG_SPEED", 1/2) config.set("DEATH_FOG_FINAL_SIZE", 16) config.set("PROVIDE_DEATH_FOG_OBS", True) env = nmmo.Env(config) env.reset() # check the initial fog map border = config.MAP_BORDER other_border = config.MAP_SIZE - config.MAP_BORDER - 1 center = config.MAP_SIZE // 2 safe = config.DEATH_FOG_FINAL_SIZE self.assertEqual(env.realm.fog_map[border,border], 0) self.assertEqual(env.realm.fog_map[other_border,other_border], 0) self.assertEqual(env.realm.fog_map[border+1,border+1], -1) # Safe area should be marked with the negative map size self.assertEqual(env.realm.fog_map[center-safe,center-safe], -config.MAP_SIZE) self.assertEqual(env.realm.fog_map[center+safe-1,center+safe-1], -config.MAP_SIZE) for _ in range(config.DEATH_FOG_ONSET): env.step({}) # check the fog map after the death fog onset self.assertEqual(env.realm.fog_map[border,border], config.DEATH_FOG_SPEED) self.assertEqual(env.realm.fog_map[border+1,border+1], -1 + config.DEATH_FOG_SPEED) for _ in range(3): env.step({}) # check the fog map after 3 ticks after the death fog onset self.assertEqual(env.realm.fog_map[border,border], config.DEATH_FOG_SPEED*4) self.assertEqual(env.realm.fog_map[border+1,border+1], -1 + config.DEATH_FOG_SPEED*4) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/test_determinism.py ================================================ import unittest from timeit import timeit import numpy as np from tqdm import tqdm import nmmo from nmmo.lib import seeding from tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv from tests.testhelpers import observations_are_equal # 30 seems to be enough to test variety of agent actions TEST_HORIZON = 30 RANDOM_SEED = np.random.randint(0, 100000) class TestDeterminism(unittest.TestCase): def test_np_random_get_direction(self): # pylint: disable=protected-access,bad-builtin,unnecessary-lambda np_random_1, np_seed_1 = seeding.np_random(RANDOM_SEED) np_random_2, np_seed_2 = seeding.np_random(RANDOM_SEED) self.assertEqual(np_seed_1, np_seed_2) # also test get_direction, which was added for speed optimization self.assertTrue(np.array_equal(np_random_1._dir_seq, np_random_2._dir_seq)) print("---test_np_random_get_direction---") print("np_random.integers():", timeit(lambda: np_random_1.integers(0,4), number=100000, globals=globals())) print("np_random.get_direction():", timeit(lambda: np_random_1.get_direction(), number=100000, globals=globals())) def test_map_determinism(self): config = nmmo.config.Default() config.set("MAP_FORCE_GENERATION", True) config.set("TERRAIN_FLIP_SEED", False) map_generator = config.MAP_GENERATOR(config) np_random1, _ = seeding.np_random(RANDOM_SEED) np_random1_1, _ = seeding.np_random(RANDOM_SEED) terrain1, tiles1 = map_generator.generate_map(0, np_random1) terrain1_1, tiles1_1 = map_generator.generate_map(0, np_random1_1) self.assertTrue(np.array_equal(terrain1, terrain1_1)) self.assertTrue(np.array_equal(tiles1, tiles1_1)) # test flip seed config2 = nmmo.config.Default() config2.set("MAP_FORCE_GENERATION", True) config2.set("TERRAIN_FLIP_SEED", True) map_generator2 = config2.MAP_GENERATOR(config2) np_random2, _ = seeding.np_random(RANDOM_SEED) terrain2, tiles2 = map_generator2.generate_map(0, np_random2) self.assertFalse(np.array_equal(terrain1, terrain2)) self.assertFalse(np.array_equal(tiles1, tiles2)) def test_env_level_rng(self): # two envs running independently should return the same results # config to always generate new maps, to test map determinism config1 = ScriptedAgentTestConfig() config1.set("MAP_FORCE_GENERATION", True) config1.set("PATH_MAPS", "maps/det1") config1.set("RESOURCE_RESILIENT_POPULATION", 0.2) # uses np_random config2 = ScriptedAgentTestConfig() config2.set("MAP_FORCE_GENERATION", True) config2.set("PATH_MAPS", "maps/det2") config2.set("RESOURCE_RESILIENT_POPULATION", 0.2) # to create the same maps, seed must be provided env1 = ScriptedAgentTestEnv(config1, seed=RANDOM_SEED) env2 = ScriptedAgentTestEnv(config2, seed=RANDOM_SEED) envs = [env1, env2] init_obs = [env.reset(seed=RANDOM_SEED+1)[0] for env in envs] self.assertTrue(observations_are_equal(init_obs[0], init_obs[0])) # sanity check self.assertTrue(observations_are_equal(init_obs[0], init_obs[1]), f"The multi-env determinism failed. Seed: {RANDOM_SEED}.") for _ in tqdm(range(TEST_HORIZON)): # step returns a tuple of (obs, rewards, dones, infos) step_results = [env.step({}) for env in envs] self.assertTrue(observations_are_equal(step_results[0][0], step_results[1][0]), f"The multi-env determinism failed. Seed: {RANDOM_SEED}.") event_logs = [env.realm.event_log.get_data() for env in envs] self.assertTrue(np.array_equal(event_logs[0], event_logs[1]), f"The multi-env determinism failed. Seed: {RANDOM_SEED}.") if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_eventlog.py ================================================ import unittest import nmmo from nmmo.datastore.numpy_datastore import NumpyDatastore from nmmo.lib.event_log import EventState, EventLogger from nmmo.lib.event_code import EventCode from nmmo.entity.entity import Entity from nmmo.systems.item import ItemState from nmmo.systems.item import Whetstone, Ration, Hat from nmmo.systems import skill as Skill class MockRealm: def __init__(self): self.config = nmmo.config.Default() self.datastore = NumpyDatastore() self.items = {} self.datastore.register_object_type("Event", EventState.State.num_attributes) self.datastore.register_object_type("Item", ItemState.State.num_attributes) self.tick = 0 self.event_log = None def step(self): self.tick += 1 self.event_log.update() class MockEntity(Entity): # pylint: disable=super-init-not-called def __init__(self, ent_id, **kwargs): self.id = ent_id self.level = kwargs.pop('attack_level', 0) @property def ent_id(self): return self.id @property def attack_level(self): return self.level class TestEventLog(unittest.TestCase): def test_event_logging(self): mock_realm = MockRealm() mock_realm.event_log = EventLogger(mock_realm) event_log = mock_realm.event_log event_log.record(EventCode.EAT_FOOD, MockEntity(1)) event_log.record(EventCode.DRINK_WATER, MockEntity(2)) event_log.record(EventCode.SCORE_HIT, MockEntity(2), target=MockEntity(1), combat_style=Skill.Melee, damage=50) event_log.record(EventCode.PLAYER_KILL, MockEntity(3), target=MockEntity(5, attack_level=5)) mock_realm.step() event_log.record(EventCode.CONSUME_ITEM, MockEntity(4), item=Ration(mock_realm, 8)) event_log.record(EventCode.GIVE_ITEM, MockEntity(4)) event_log.record(EventCode.DESTROY_ITEM, MockEntity(5)) event_log.record(EventCode.HARVEST_ITEM, MockEntity(6), item=Whetstone(mock_realm, 3)) mock_realm.step() event_log.record(EventCode.GIVE_GOLD, MockEntity(7)) event_log.record(EventCode.LIST_ITEM, MockEntity(8), item=Ration(mock_realm, 5), price=11) event_log.record(EventCode.EARN_GOLD, MockEntity(9), amount=15) event_log.record(EventCode.BUY_ITEM, MockEntity(10), item=Whetstone(mock_realm, 7), price=21) #event_log.record(EventCode.SPEND_GOLD, env.realm.players[11], amount=25) mock_realm.step() event_log.record(EventCode.LEVEL_UP, MockEntity(12), skill=Skill.Fishing, level=3) mock_realm.step() event_log.record(EventCode.GO_FARTHEST, MockEntity(12), distance=6) event_log.record(EventCode.EQUIP_ITEM, MockEntity(12), item=Hat(mock_realm, 4)) mock_realm.step() log_data = [list(row) for row in event_log.get_data()] self.assertListEqual(log_data, [ [1, 1, 1, EventCode.EAT_FOOD, 0, 0, 0, 0, 0], [1, 2, 1, EventCode.DRINK_WATER, 0, 0, 0, 0, 0], [1, 2, 1, EventCode.SCORE_HIT, 1, 0, 50, 0, 1], [1, 3, 1, EventCode.PLAYER_KILL, 0, 5, 0, 0, 5], [1, 4, 2, EventCode.CONSUME_ITEM, 16, 8, 1, 0, 1], [1, 4, 2, EventCode.GIVE_ITEM, 0, 0, 0, 0, 0], [1, 5, 2, EventCode.DESTROY_ITEM, 0, 0, 0, 0, 0], [1, 6, 2, EventCode.HARVEST_ITEM, 13, 3, 1, 0, 2], [1, 7, 3, EventCode.GIVE_GOLD, 0, 0, 0, 0, 0], [1, 8, 3, EventCode.LIST_ITEM, 16, 5, 1, 11, 3], [1, 9, 3, EventCode.EARN_GOLD, 0, 0, 0, 15, 0], [1, 10, 3, EventCode.BUY_ITEM, 13, 7, 1, 21, 4], [1, 12, 4, EventCode.LEVEL_UP, 4, 3, 0, 0, 0], [1, 12, 5, EventCode.GO_FARTHEST, 0, 0, 6, 0, 0], [1, 12, 5, EventCode.EQUIP_ITEM, 2, 4, 1, 0, 5]]) log_by_tick = [list(row) for row in event_log.get_data(tick = 4)] self.assertListEqual(log_by_tick, [ [1, 12, 4, EventCode.LEVEL_UP, 4, 3, 0, 0, 0]]) log_by_event = [list(row) for row in event_log.get_data(event_code = EventCode.CONSUME_ITEM)] self.assertListEqual(log_by_event, [ [1, 4, 2, EventCode.CONSUME_ITEM, 16, 8, 1, 0, 1]]) log_by_tick_agent = [list(row) for row in \ event_log.get_data(tick = 5, agents = [12], event_code = EventCode.EQUIP_ITEM)] self.assertListEqual(log_by_tick_agent, [ [1, 12, 5, EventCode.EQUIP_ITEM, 2, 4, 1, 0, 5]]) empty_log = event_log.get_data(tick = 10) self.assertTrue(empty_log.shape[0] == 0) if __name__ == '__main__': unittest.main() """ TEST_HORIZON = 50 RANDOM_SEED = 338 from tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv config = ScriptedAgentTestConfig() env = ScriptedAgentTestEnv(config) env.reset(seed=RANDOM_SEED) from tqdm import tqdm for tick in tqdm(range(TEST_HORIZON)): env.step({}) # events to check log = env.realm.event_log.get_data() idx = (log[:,2] == tick+1) & (log[:,3] == EventCode.EQUIP_ITEM) if sum(idx): print(log[idx]) print() print('done') """ ================================================ FILE: tests/test_memory_usage.py ================================================ # pylint: disable=bad-builtin, unused-variable import psutil import nmmo def test_memory_usage(): env = nmmo.Env() process = psutil.Process() print("memory", process.memory_info().rss) if __name__ == '__main__': test_memory_usage() ================================================ FILE: tests/test_mini_games.py ================================================ # pylint: disable=protected-access import unittest import numpy as np import nmmo from nmmo import minigames as mg from nmmo.lib import team_helper TEST_HORIZON = 10 class TestMinigames(unittest.TestCase): def test_mini_games(self): config = nmmo.config.Default() config.set("TEAMS", team_helper.make_teams(config, num_teams=16)) env = nmmo.Env(config) for game_cls in mg.AVAILABLE_GAMES: game = game_cls(env) env.reset(game=game) game.test(env, TEST_HORIZON) # Check if the gym_obs is correctly set, on alive agents for agent_id in env.realm.players: gym_obs = env.obs[agent_id].to_gym() self.assertEqual(gym_obs["AgentId"], agent_id) self.assertEqual(gym_obs["CurrentTick"], env.realm.tick) self.assertTrue( np.array_equal(gym_obs["Task"], env.agent_task_map[agent_id][0].embedding)) if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_performance.py ================================================ # pylint: disable=no-member # import time import cProfile import io import pstats from tqdm import tqdm import nmmo from nmmo.core.config import (NPC, AllGameSystems, Combat, Communication, Equipment, Exchange, Item, Medium, Profession, Progression, Resource, Small, Terrain) from nmmo.task.task_api import nmmo_default_task, make_same_task from nmmo.task.base_predicates import CountEvent, FullyArmed from nmmo.systems.skill import Melee from tests.testhelpers import profile_env_step from scripted import baselines # Test utils def create_and_reset(conf): env = nmmo.Env(conf()) env.reset(map_id=1) def create_config(base, *systems): systems = (base, *systems) name = '_'.join(cls.__name__ for cls in systems) conf = type(name, systems, {})() conf.set("TERRAIN_TRAIN_MAPS", 1) conf.set("TERRAIN_EVAL_MAPS", 1) conf.set("IMMORTAL", True) return conf def benchmark_config(benchmark, base, nent, *systems): conf = create_config(base, *systems) conf.set("PLAYER_N", nent) conf.set("PLAYERS", [baselines.Random]) env = nmmo.Env(conf) env.reset() benchmark(env.step, actions={}) # Small map tests -- fast with greater coverage for individual game systems def test_small_env_creation(benchmark): benchmark(lambda: nmmo.Env(Small())) def test_small_env_reset(benchmark): config = Small() config.set("PLAYERS", [baselines.Random]) env = nmmo.Env(config) benchmark(lambda: env.reset(map_id=1)) def test_fps_base_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1) def test_fps_minimal_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression) def test_fps_npc_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression, NPC) def test_fps_test_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression, Item, Exchange) def test_fps_no_npc_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) def test_fps_all_small_1_pop(benchmark): benchmark_config(benchmark, Small, 1, AllGameSystems) def test_fps_base_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1) def test_fps_minimal_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat) def test_fps_npc_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat, NPC) def test_fps_test_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat, Progression, Item, Exchange) def test_fps_no_npc_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) def test_fps_all_med_1_pop(benchmark): benchmark_config(benchmark, Medium, 1, AllGameSystems) def test_fps_base_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100) def test_fps_minimal_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat) def test_fps_npc_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat, NPC) def test_fps_test_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat, Progression, Item, Exchange) def test_fps_no_npc_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat, Progression, Item, Equipment, Profession, Exchange, Communication) def test_fps_all_med_100_pop(benchmark): benchmark_config(benchmark, Medium, 100, AllGameSystems) def set_seed_test(): random_seed = 5000 # conf = create_config(Medium, Terrain, Resource, Combat, NPC, Communication) # conf.set("PLAYER_N", 7) # conf.set("PLAYERS", [baselines.Random]) conf = nmmo.config.Default() conf.set("TERRAIN_TRAIN_MAPS", 1) conf.set("TERRAIN_EVAL_MAPS", 1) conf.set("IMMORTAL", True) conf.set("NPC_N", 128) conf.set("USE_CYTHON", True) conf.set("PROVIDE_DEATH_FOG_OBS", True) env = nmmo.Env(conf) env.reset(seed=random_seed) for _ in tqdm(range(1024)): env.step({}) def set_seed_test_complex(): tasks = nmmo_default_task(range(1, 129)) tasks += make_same_task(CountEvent, range(128), pred_kwargs={'event': 'EAT_FOOD', 'N': 10}) tasks += make_same_task(FullyArmed, range(128), pred_kwargs={'combat_style': Melee, 'level': 3, 'num_agent': 1}) profile_env_step(tasks=tasks) if __name__ == '__main__': pr = cProfile.Profile() pr.enable() #set_seed_test_complex() set_seed_test() pr.disable() with open('profile.run','a', encoding="utf-8") as f: s = io.StringIO() ps = pstats.Stats(pr,stream=s).sort_stats('tottime') ps.print_stats(100) f.write(s.getvalue()) ''' def benchmark_env(benchmark, env, nent): env.config.PLAYER_N = nent env.config.PLAYERS = [nmmo.agent.Random] env.reset() benchmark(env.step, actions={}) # Reuse large maps since we aren't benchmarking the reset function def test_large_env_creation(benchmark): benchmark(lambda: nmmo.Env(Large())) def test_large_env_reset(benchmark): env = nmmo.Env(Large()) benchmark(lambda: env.reset(idx=1)) LargeMapsRCP = nmmo.Env(create_config(Large, Resource, Terrain, Combat, Progression)) LargeMapsAll = nmmo.Env(create_config(Large, AllGameSystems)) def test_fps_large_rcp_1_pop(benchmark): benchmark_env(benchmark, LargeMapsRCP, 1) def test_fps_large_rcp_100_pop(benchmark): benchmark_env(benchmark, LargeMapsRCP, 100) def test_fps_large_rcp_1000_pop(benchmark): benchmark_env(benchmark, LargeMapsRCP, 1000) def test_fps_large_all_1_pop(benchmark): benchmark_env(benchmark, LargeMapsAll, 1) def test_fps_large_all_100_pop(benchmark): benchmark_env(benchmark, LargeMapsAll, 100) def test_fps_large_all_1000_pop(benchmark): benchmark_env(benchmark, LargeMapsAll, 1000) ''' ================================================ FILE: tests/test_pettingzoo.py ================================================ import unittest from pettingzoo.test import parallel_api_test import nmmo from scripted import baselines class TestPettingZoo(unittest.TestCase): def test_pettingzoo_api(self): config = nmmo.config.Default() config.set("PLAYERS", [baselines.Random]) config.set("HORIZON", 290) env = nmmo.Env(config) parallel_api_test(env, num_cycles=300) if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_rollout.py ================================================ import nmmo from scripted.baselines import Random class SimpleConfig(nmmo.config.Small, nmmo.config.Combat): pass def test_rollout(): config = nmmo.config.Default() # SimpleConfig() config.set("PLAYERS", [Random]) config.set("USE_CYTHON", True) env = nmmo.Env(config) env.reset() for _ in range(64): env.step({}) env.reset() if __name__ == '__main__': test_rollout() ================================================ FILE: tests/testhelpers.py ================================================ import logging import unittest from copy import deepcopy from timeit import timeit import numpy as np import nmmo from nmmo.core import action from nmmo.systems import item as Item from nmmo.core.realm import Realm from nmmo.lib import material as Material from scripted import baselines # this function can be replaced by assertDictEqual # but might be still useful for debugging def actions_are_equal(source_atn, target_atn, debug=True): # compare the numbers and player ids player_src = list(source_atn.keys()) player_tgt = list(target_atn.keys()) if player_src != player_tgt: if debug: logging.error("players don't match") return False # for each player, compare the actions for ent_id in player_src: atn1 = source_atn[ent_id] atn2 = target_atn[ent_id] if list(atn1.keys()) != list(atn2.keys()): if debug: logging.error("action keys don't match. player: %s", str(ent_id)) return False for atn, args in atn1.items(): if atn2[atn] != args: if debug: logging.error("action args don't match. player: %s, action: %s", str(ent_id), str(atn)) return False return True # this function CANNOT be replaced by assertDictEqual def observations_are_equal(source_obs, target_obs, debug=True): keys_src = list(source_obs.keys()) keys_obs = list(target_obs.keys()) if keys_src != keys_obs: if debug: #print("entities don't match") logging.error("entities don't match") return False for k in keys_src: ent_src = source_obs[k] ent_tgt = target_obs[k] if list(ent_src.keys()) != list(ent_tgt.keys()): if debug: #print(f"entries don't match. key: {k}") logging.error("entries don't match. key: %s", str(k)) return False obj = ent_src.keys() for o in obj: # ActionTargets causes a problem here, so skip it if o == "ActionTargets": continue obj_src = ent_src[o] obj_tgt = ent_tgt[o] if np.sum(obj_src != obj_tgt) > 0: if debug: #print(f"objects don't match. key: {k}, obj: {o}") logging.error("objects don't match. key: %s, obj: %s", str(k), str(o)) return False return True def player_total(env): return sum(ent.gold.val for ent in env.realm.players.values()) def count_actions(tick, actions): cnt_action = {} for atn in (action.Move, action.Attack, action.Sell, action.Use, action.Give, action.Buy): cnt_action[atn] = 0 for ent_id in actions: for atn, _ in actions[ent_id].items(): if atn in cnt_action: cnt_action[atn] += 1 else: cnt_action[atn] = 1 info_str = f"Tick: {tick}, acting agents: {len(actions)}, action counts " + \ f"move: {cnt_action[action.Move]}, attack: {cnt_action[action.Attack]}, " + \ f"sell: {cnt_action[action.Sell]}, use: {cnt_action[action.Move]}, " + \ f"give: {cnt_action[action.Give]}, buy: {cnt_action[action.Buy]}" logging.info(info_str) return cnt_action class ScriptedAgentTestConfig(nmmo.config.Small, nmmo.config.AllGameSystems): __test__ = False DEATH_FOG_ONSET = 5 PLAYERS = [ baselines.Fisher, baselines.Herbalist, baselines.Prospector,baselines.Carver, baselines.Alchemist, baselines.Melee, baselines.Range, baselines.Mage] # pylint: disable=abstract-method,duplicate-code class ScriptedAgentTestEnv(nmmo.Env): ''' EnvTest step() bypasses some differential treatments for scripted agents To do so, actions of scripted must be serialized using the serialize_actions function above ''' __test__ = False def __init__(self, config: nmmo.config.Config, seed=None): super().__init__(config=config, seed=seed) # all agent must be scripted agents when using ScriptedAgentTestEnv for ent in self.realm.players.values(): assert isinstance(ent.agent, baselines.Scripted), 'All agent must be scripted.' # this is to cache the actions generated by scripted policies self.actions = {} def reset(self, map_id=None, seed=None, options=None): self.actions = {} return super().reset(map_id=map_id, seed=seed, options=options) def _compute_scripted_agent_actions(self, actions): assert actions is not None, "actions must be provided, even it's {}" # if actions are not provided, generate actions using the scripted policy if actions == {}: for eid, ent in self.realm.players.items(): actions[eid] = ent.agent(self.obs[eid]) # cache the actions for replay before deserialization self.actions = deepcopy(actions) # if actions are provided, just run ent.agent() to set the RNG to the same state else: # NOTE: This is a hack to set the random number generator to the same state # since scripted agents also use RNG. Without this, the RNG is in different state, # and the env.step() does not give the same results in the deterministic replay. for eid, ent in self.realm.players.items(): ent.agent(self.obs[eid]) return actions def change_spawn_pos(realm: Realm, ent_id: int, new_pos): # check if the position is valid assert realm.map.tiles[new_pos].habitable, "Given pos is not habitable." assert realm.entity(ent_id), "No such entity in the realm" entity = realm.entity(ent_id) old_pos = entity.pos realm.map.tiles[old_pos].remove_entity(ent_id) # set to new pos entity.set_pos(*new_pos) entity.spawn_pos = new_pos realm.map.tiles[new_pos].add_entity(entity) def provide_item(realm: Realm, ent_id: int, item: Item.Item, level: int, quantity: int): for _ in range(quantity): realm.players[ent_id].inventory.receive( item(realm, level=level)) # pylint: disable=invalid-name,protected-access class ScriptedTestTemplate(unittest.TestCase): @classmethod def setUpClass(cls): # only use Combat agents cls.config = ScriptedAgentTestConfig() cls.config.set("PROVIDE_ACTION_TARGETS", True) cls.config.set("PLAYERS", [baselines.Melee, baselines.Range, baselines.Mage]) cls.config.set("PLAYER_N", 3) #cls.config.IMMORTAL = True # set up agents to test ammo use cls.policy = { 1:'Melee', 2:'Range', 3:'Mage' } # 1 cannot hit 3, 2 can hit 1, 3 cannot hit 2 cls.spawn_locs = { 1:(17, 17), 2:(17, 19), 3:(21, 21) } cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Runes } cls.ammo_quantity = 2 # items to provide cls.init_gold = 5 # TODO: there should not be level 0 items cls.item_level = [0, 3] # 0 can be used, 3 cannot be used cls.item_sig = {} def _make_item_sig(self): item_sig = {} for ent_id, ammo in self.ammo.items(): item_sig[ent_id] = [] for item in [ammo, Item.Top, Item.Gloves, Item.Ration, Item.Potion]: for lvl in self.item_level: item_sig[ent_id].append((item, lvl)) return item_sig def _setup_env(self, random_seed, check_assert=True, remove_immunity=False): """ set up a new env and perform initial checks """ config = deepcopy(self.config) if remove_immunity: config.set("COMBAT_SPAWN_IMMUNITY", 0) env = ScriptedAgentTestEnv(config, seed=random_seed) env.reset() # provide money for all for ent_id in env.realm.players: env.realm.players[ent_id].gold.update(self.init_gold) # provide items that are in item_sig self.item_sig = self._make_item_sig() for ent_id, items in self.item_sig.items(): for item_sig in items: if item_sig[0] == self.ammo[ent_id]: provide_item(env.realm, ent_id, item_sig[0], item_sig[1], self.ammo_quantity) else: provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1) # teleport the players, if provided with specific locations for ent_id, pos in self.spawn_locs.items(): change_spawn_pos(env.realm, ent_id, pos) # Change entire map to grass to become habitable and non-harvestable MS = env.config.MAP_SIZE for i in range(MS): for j in range(MS): tile = env.realm.map.tiles[i,j] tile.material = Material.Grass tile.material_id.update(Material.Grass.index) tile.state = Material.Grass(env.config) env._compute_observations() if check_assert: self._check_default_asserts(env) return env def _check_ent_mask(self, ent_obs, atn, target_id): assert atn in [action.Give, action.GiveGold], "Invalid action" gym_obs = ent_obs.to_gym() mask = gym_obs["ActionTargets"][atn.__name__]["Target"][:ent_obs.entities.len] > 0 return target_id in ent_obs.entities.ids[mask] def _check_inv_mask(self, ent_obs, atn, item_sig): assert atn in [action.Destroy, action.Give, action.Sell, action.Use], "Invalid action" gym_obs = ent_obs.to_gym() mask = gym_obs["ActionTargets"][atn.__name__]["InventoryItem"][:ent_obs.inventory.len] > 0 inv_idx = ent_obs.inventory.sig(*item_sig) return ent_obs.inventory.id(inv_idx) in ent_obs.inventory.ids[mask] def _check_mkt_mask(self, ent_obs, item_id): gym_obs = ent_obs.to_gym() mask = gym_obs["ActionTargets"]["Buy"]["MarketItem"][:ent_obs.market.len] > 0 return item_id in ent_obs.market.ids[mask] def _check_default_asserts(self, env): """ The below asserts are based on the hardcoded values in setUpClass() This should not run when different values were used """ # check if the agents are in specified positions for ent_id, pos in self.spawn_locs.items(): self.assertEqual(env.realm.players[ent_id].pos, pos) for ent_id, sig_list in self.item_sig.items(): # ammo instances are in the datastore and global item registry (realm) inventory = env.obs[ent_id].inventory self.assertTrue(inventory.len == len(sig_list)) for inv_idx in range(inventory.len): item_id = inventory.id(inv_idx) self.assertTrue(Item.ItemState.Query.by_id(env.realm.datastore, item_id) is not None) self.assertTrue(item_id in env.realm.items) for lvl in self.item_level: inv_idx = inventory.sig(self.ammo[ent_id], lvl) self.assertTrue(inv_idx is not None) self.assertEqual(self.ammo_quantity, # provided 2 ammos Item.ItemState.parse_array(inventory.values[inv_idx]).quantity) # check ActionTargets ent_obs = env.obs[ent_id] if env.config.ITEM_SYSTEM_ENABLED: # USE InventoryItem mask for item_sig in sig_list: if item_sig[1] == 0: # items that can be used self.assertTrue(self._check_inv_mask(ent_obs, action.Use, item_sig)) else: # items that are too high to use self.assertFalse(self._check_inv_mask(ent_obs, action.Use, item_sig)) if env.config.EXCHANGE_SYSTEM_ENABLED: # SELL InventoryItem mask for item_sig in sig_list: # the agent can sell anything now self.assertTrue(self._check_inv_mask(ent_obs, action.Sell, item_sig)) # BUY MarketItem mask -- there is nothing on the market, so mask should be all 0 self.assertTrue(len(env.obs[ent_id].market.ids) == 0) def _check_assert_make_action(self, env, atn, test_cond): assert atn in [action.Give, action.GiveGold, action.Buy], "Invalid action" actions = {} for ent_id, cond in test_cond.items(): ent_obs = env.obs[ent_id] if atn in [action.Give, action.GiveGold]: # self should be always masked self.assertFalse(self._check_ent_mask(ent_obs, atn, ent_id)) # check if the target is masked as expected self.assertEqual( cond['ent_mask'], self._check_ent_mask(ent_obs, atn, cond['tgt_id']), f"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['tgt_id']}" ) if atn in [action.Give]: self.assertEqual( cond['inv_mask'], self._check_inv_mask(ent_obs, atn, cond['item_sig']), f"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['item_sig']}" ) if atn in [action.Buy]: self.assertEqual( cond['mkt_mask'], self._check_mkt_mask(ent_obs, cond['item_id']), f"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['item_id']}" ) # append the actions if atn == action.Give: actions[ent_id] = { action.Give: { action.InventoryItem: env.obs[ent_id].inventory.sig(*cond['item_sig']), action.Target: env.obs[ent_id].entities.index(cond['tgt_id']) } } elif atn == action.GiveGold: actions[ent_id] = { action.GiveGold: { action.Target: env.obs[ent_id].entities.index(cond['tgt_id']), action.Price: action.Price.index(cond['gold']) } } elif atn == action.Buy: mkt_idx = ent_obs.market.index(cond['item_id']) actions[ent_id] = { action.Buy: { action.MarketItem: mkt_idx } } return actions # pylint: disable=unnecessary-lambda,bad-builtin def profile_env_step(action_target=True, tasks=None, condition=None): config = nmmo.config.Default() config.set("PLAYERS", [baselines.Sleeper]) # the scripted agents doing nothing config.set("IMMORTAL", True) # otherwise the agents will die config.set("PROVIDE_ACTION_TARGETS", action_target) env = nmmo.Env(config, seed=0) if tasks is None: tasks = [] env.reset(seed=0, make_task_fn=lambda: tasks) for _ in range(3): env.step({}) env._compute_observations() obs = deepcopy(env.obs) test_func = [ ('env.step({}):', lambda: env.step({})), ('env.realm.step():', lambda: env.realm.step({})), ('env._compute_observations():', lambda: env._compute_observations()), ('obs.to_gym(), ActionTarget:', lambda: {a: o.to_gym() for a,o in obs.items()}), ('env._compute_rewards():', lambda: env._compute_rewards()) ] if condition: print('=== Test condition:', condition, '===') for name, func in test_func: print(' -', name, timeit(func, number=100, globals=globals())) ================================================ FILE: utils/git-pr.sh ================================================ #!/bin/bash MASTER_BRANCH="2.0" # check if in master branch current_branch=$(git rev-parse --abbrev-ref HEAD) if [ "$current_branch" == MASTER_BRANCH ]; then echo "Please run 'git pr' from a topic branch." exit 1 fi # check if there are any uncommitted changes git_status=$(git status --porcelain) if [ -n "$git_status" ]; then read -p "Uncommitted changes found. Commit before running 'git pr'? (y/n) " ans if [ "$ans" = "y" ]; then git commit -m -a "Automatic commit for git-pr" else echo "Please commit or stash changes before running 'git pr'." exit 1 fi fi # Merging master echo "Merging master..." git merge origin/$MASTER_BRANCH # Checking pylint, xcxc, pytest without touching git PRE_GIT_CHECK=$(find . -name pre-git-check.sh) if test -f "$PRE_GIT_CHECK"; then $PRE_GIT_CHECK if [ $? -ne 0 ]; then echo "pre-git-check.sh failed. Exiting." exit 1 fi else echo "Missing pre-git-check.sh. Exiting." exit 1 fi # create a new branch from current branch and reset to master echo "Creating and switching to new topic branch..." git_user=$(git config user.email | cut -d'@' -f1) branch_name="${git_user}-git-pr-$RANDOM-$RANDOM" git checkout -b $branch_name git reset --soft origin/$MASTER_BRANCH # Verify that a commit message was added echo "Verifying commit message..." if ! git commit -a ; then echo "Commit message is empty. Exiting." exit 1 fi # Push the topic branch to origin echo "Pushing topic branch to origin..." git push -u origin $branch_name # Generate a Github pull request (just the url, not actually making a PR) echo "Generating Github pull request..." pull_request_url="https://github.com/CarperAI/nmmo-environment/compare/$MASTER_BRANCH...CarperAI:nmmo-environment:$branch_name?expand=1" echo "Pull request URL: $pull_request_url" ================================================ FILE: utils/pre-git-check.sh ================================================ #!/bin/bash echo echo "Checking pylint, xcxc, pytest without touching git" echo # Check the number of physical cores only if command -v lscpu &> /dev/null then # lscpu is available cores=$(lscpu -b -p=Core,Socket | grep -v '^#' | sort -u | wc -l) else # lscpu is not available, use sysctl instead cores=$(sysctl -n hw.physicalcpu) fi # Run linter echo "--------------------------------------------------------------------" echo "Running linter..." files=$(git ls-files -m -o --exclude-standard '*.py') for file in $files; do if test -e $file; then echo $file if ! pylint --score=no --fail-under=10 $file; then echo "Lint failed. Exiting." exit 1 fi fi done if ! pylint --recursive=y nmmo tests; then echo "Lint failed. Exiting." exit 1 fi # Check if there are any "xcxc" strings in the code echo "--------------------------------------------------------------------" echo "Looking for xcxc..." files=$(find . -name '*.py') for file in $files; do if grep -q 'xcxc' $file; then echo "Found xcxc in $file!" >&2 read -p "Do you like to stop here? (y/n) " ans if [ "$ans" = "y" ]; then exit 1 fi fi done # Run unit tests echo echo "--------------------------------------------------------------------" echo "Running unit tests..." if ! pytest; then echo "Unit tests failed. Exiting." exit 1 fi echo echo "Pre-git checks look good!" echo ================================================ FILE: utils/run-perf-tests.sh ================================================ pytest --benchmark-columns=ops,rounds,median,mean,stddev,min,max,iterations --benchmark-max-time=5 --benchmark-min-rounds=500 \ --benchmark-warmup=on --benchmark-warmup-iterations=300 tests/test_performance.py