[
  {
    "path": ".gitattributes",
    "content": "neural_mmo/_version.py export-subst\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report a bug with the Neural MMO environment or Unity3D Embyr client\ntitle: \"[Bug Report]\"\nlabels: ''\nassignees: ''\n\n---\n\nFill 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 \n\n**OS:** Your operating system\n\n**Description:** What's wrong\n\n**Repro:** How do we reproduce the issue? Minimal scripts are best. Instructions are acceptable. \"I don't know\" is valid.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\nname: Documentation\nabout: Report problems with the documentation\ntitle: \"[Docs]\"\nlabels: ''\nassignees: ''\n\n---\n\nOne of:\n\n**Insufficient**: Something is documented, but the current documentation is inadequate\n\n**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.\n\n**Other**: Something else. We will update this template to include your problem category afterwards.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement.md",
    "content": "---\nname: Enhancement\nabout: Suggest an improvement to an API or a refactorization of existing code for\n  better efficiency or clarity\ntitle: \"[Enhancement]\"\nlabels: ''\nassignees: ''\n\n---\n\nThis 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.\n\nTry 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.\n\n**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.\n\n**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.\n\n**Poor performance**: A function or subroutine is slow. Describe cases in which this functionality becomes a bottleneck and submit timing data.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Request a feature for the Neural MMO environment of Unity3D Embyr client\ntitle: \"[Feature Request]\"\nlabels: ''\nassignees: ''\n\n---\n\nEventually, 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.\n\n**I am trying to:** Describe your use case. What is the end result you would like to achieve?\n\n**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?\n\n**The solution should look like:** Describe your ideal solution --  a requirement, an API, a restructuring, additional documentation, etc.\n"
  },
  {
    "path": ".github/workflows/pylint-test.yml",
    "content": "name: pylint-test\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        # Randomly hitting TypeError: object int can't be used in 'await' expression in 3.11\n        # So, excluding 3.11 for now\n        python-version: [\"3.8\", \"3.9\", \"3.10\"]\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v3\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip setuptools wheel cython\n        pip install .\n        python setup.py build_ext --inplace\n    - name: Running unit tests\n      run: pytest\n    - name: Analysing the code with pylint\n      run: pylint --recursive=y nmmo tests\n    - name: Looking for xcxc, just in case\n      run: |\n        if grep -r --include='*.py' 'xcxc'; then\n          echo \"Found xcxc in the code. Please check the file.\"\n          exit 1\n        fi\n"
  },
  {
    "path": ".gitignore",
    "content": "# Game maps\nmaps/\n*.swp\nruns/*\nwandb/*\n\n# local replay file from test_render_save.py\ntests/replay_local*.pickle\nreplay*\neval*\n\n.vscode\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions, cython\n*.so\n*.c\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\n#lib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.*venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintainted in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\nprofile.run"
  },
  {
    "path": ".pylintrc",
    "content": "[MESSAGES CONTROL]\n\ndisable=W0511, # TODO/FIXME\n        W0105, # string is used as a statement\n        C0114, # missing module docstring\n        C0115, # missing class docstring\n        C0116, # missing function docstring\n        W0221, # arguments differ from overridden method\n        C0415, # import outside toplevel\n        E0611, # no name in module\n        R0901, # too many ancestors\n        R0902, # too many instance attributes\n        R0903, # too few public methods\n        R0911, # too many return statements\n        R0912, # too many branches\n        R0913, # too many arguments\n        R0914, # too many local variables\n        R0914, # too many local variables\n        R0915, # too many statements\n        R0401, # cyclic import\n\n[INDENTATION]\nindent-string='  '\n\n[MASTER]\ngood-names-rgxs=^[_a-zA-Z][_a-z0-9]?$ # whitelist short variables\nknown-third-party=ordered_set,numpy,gym,pettingzoo,vec_noise,imageio,scipy,tqdm\nload-plugins=pylint.extensions.bad_builtin\n\n[BASIC]\nbad-functions=print # checks if these functions are used"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 OpenAI, 2020 Joseph Suarez\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "global-include *.pyx\ninclude nmmo/resource/*\n"
  },
  {
    "path": "README.md",
    "content": "![figure](https://neuralmmo.github.io/_static/banner.jpg)\n\n# ![icon](https://neuralmmo.github.io/_build/html/_images/icon.png) Welcome to the Platform!\n\n[![PyPI version](https://badge.fury.io/py/nmmo.svg)](https://badge.fury.io/py/nmmo)\n[![](https://dcbadge.vercel.app/api/server/BkMmFUC?style=plastic)](https://discord.gg/BkMmFUC)\n[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40jsuarez5341)](https://twitter.com/jsuarez5341)\n\nNeural 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.\n"
  },
  {
    "path": "nmmo/__init__.py",
    "content": "import logging\n\nfrom .version import __version__\n\nfrom .lib import material, spawn\nfrom .render.overlay import Overlay, OverlayRegistry\nfrom .core import config, agent, action\nfrom .core.action import Action\nfrom .core.agent import Agent, Scripted\nfrom .core.env import Env\nfrom .core.terrain import MapGenerator, Terrain\n\nMOTD = rf'''      ___           ___           ___           ___\n     /__/\\         /__/\\         /__/\\         /  /\\      Version {__version__:<8}\n     \\  \\:\\       |  |::\\       |  |::\\       /  /::\\\n      \\  \\:\\      |  |:|:\\      |  |:|:\\     /  /:/\\:\\    An open source\n  _____\\__\\:\\   __|__|:|\\:\\   __|__|:|\\:\\   /  /:/  \\:\\   project originally\n /__/::::::::\\ /__/::::| \\:\\ /__/::::| \\:\\ /__/:/ \\__\\:\\  founded by Joseph Suarez\n \\  \\:\\~~\\~~\\/ \\  \\:\\~~\\__\\/ \\  \\:\\~~\\__\\/ \\  \\:\\ /  /:/  and formalized at OpenAI\n  \\  \\:\\  ~~~   \\  \\:\\        \\  \\:\\        \\  \\:\\  /:/\n   \\  \\:\\        \\  \\:\\        \\  \\:\\        \\  \\:\\/:/    Now developed and\n    \\  \\:\\        \\  \\:\\        \\  \\:\\        \\  \\::/     maintained at MIT in\n     \\__\\/         \\__\\/         \\__\\/         \\__\\/      Phillip Isola's lab '''\n\n__all__ = ['Env', 'config', 'agent', 'Agent', 'Scripted', 'MapGenerator', 'Terrain',\n        'action', 'Action', 'material', 'spawn',\n        'Overlay', 'OverlayRegistry']\n\ntry:\n  __all__.append('OpenSkillRating')\nexcept RuntimeError:\n  logging.error('Warning: OpenSkill not installed. Ignore if you do not need this feature')\n"
  },
  {
    "path": "nmmo/core/__init__.py",
    "content": ""
  },
  {
    "path": "nmmo/core/action.py",
    "content": "# pylint: disable=no-method-argument,unused-argument,no-self-argument,no-member\nfrom enum import Enum, auto\nimport numpy as np\n\nfrom nmmo.lib import utils\nfrom nmmo.lib.utils import staticproperty\nfrom nmmo.systems.item import Stack\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.core.observation import Observation\n\n\nclass NodeType(Enum):\n  #Tree edges\n  STATIC = auto()    #Traverses all edges without decisions\n  SELECTION = auto() #Picks an edge to follow\n\n  #Executable actions\n  ACTION    = auto() #No arguments\n  CONSTANT  = auto() #Constant argument\n  VARIABLE  = auto() #Variable argument\n\nclass Node(metaclass=utils.IterableNameComparable):\n  @classmethod\n  def init(cls, config):\n    # noop_action is used in some of the N() methods\n    cls.noop_action = 1 if config.PROVIDE_NOOP_ACTION_TARGET else 0\n\n  @staticproperty\n  def edges():\n    return []\n\n  #Fill these in\n  @staticproperty\n  def priority():\n    return None\n\n  @staticproperty\n  def type():\n    return None\n\n  @staticproperty\n  def leaf():\n    return False\n\n  @classmethod\n  def N(cls, config):\n    return len(cls.edges)\n\n  def deserialize(realm, entity, index: int, obs: Observation):\n    return index\n\nclass Fixed:\n  pass\n\n#ActionRoot\nclass Action(Node):\n  nodeType = NodeType.SELECTION\n  hooked   = False\n\n  @classmethod\n  def init(cls, config):\n    # Sets up serialization domain\n    if Action.hooked:\n      return\n\n    Action.hooked = True\n\n  #Called upon module import (see bottom of file)\n  #Sets up serialization domain\n  def hook(config):\n    idx = 0\n    arguments = []\n    for action in Action.edges(config):\n      action.init(config)\n      for args in action.edges: # pylint: disable=not-an-iterable\n        args.init(config)\n        if not \"edges\" in args.__dict__:\n          continue\n        for arg in args.edges:\n          arguments.append(arg)\n          arg.serial = tuple([idx])\n          arg.idx = idx\n          idx += 1\n    Action.arguments = arguments\n\n  @staticproperty\n  def n():\n    return len(Action.arguments)\n\n  # pylint: disable=invalid-overridden-method\n  @classmethod\n  def edges(cls, config):\n    \"\"\"List of valid actions\"\"\"\n    edges = [Move]\n    if config.COMBAT_SYSTEM_ENABLED:\n      edges.append(Attack)\n    if config.ITEM_SYSTEM_ENABLED:\n      edges += [Use, Give, Destroy]\n    if config.EXCHANGE_SYSTEM_ENABLED:\n      edges += [Buy, Sell, GiveGold]\n    if config.COMMUNICATION_SYSTEM_ENABLED:\n      edges.append(Comm)\n    return edges\n\nclass Move(Node):\n  priority = 60\n  nodeType = NodeType.SELECTION\n  def call(realm, entity, direction):\n    if direction is None:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert realm.map.is_valid_pos(*entity.pos), \"Invalid entity position\"\n\n    r, c  = entity.pos\n    ent_id = entity.ent_id\n    entity.history.last_pos = (r, c)\n    r_delta, c_delta = direction.delta\n    r_new, c_new = r+r_delta, c+c_delta\n\n    if not realm.map.is_valid_pos(r_new, c_new) or \\\n       realm.map.tiles[r_new, c_new].impassible:\n      return\n\n    # ALLOW_MOVE_INTO_OCCUPIED_TILE only applies to players, NOT npcs\n    if entity.is_player and not realm.config.ALLOW_MOVE_INTO_OCCUPIED_TILE and \\\n       realm.map.tiles[r_new, c_new].occupied:\n      return\n\n    if entity.status.freeze > 0:\n      return\n\n    entity.set_pos(r_new, c_new)\n    realm.map.tiles[r, c].remove_entity(ent_id)\n    realm.map.tiles[r_new, c_new].add_entity(entity)\n\n    # exploration record keeping. moved from entity.py, History.update()\n    progress_to_center = realm.map.dist_border_center -\\\n      utils.linf_single(realm.map.center_coord, (r_new, c_new))\n    if progress_to_center > entity.history.exploration:\n      entity.history.exploration = progress_to_center\n      if entity.is_player:\n        realm.event_log.record(EventCode.GO_FARTHEST, entity,\n                               distance=progress_to_center)\n\n    # CHECK ME: material.Impassible includes void, so this line is not reachable\n    #   Does this belong to Entity/Player.update()?\n    if realm.map.tiles[r_new, c_new].void:\n      entity.receive_damage(None, entity.resources.health.val)\n\n  @staticproperty\n  def edges():\n    return [Direction]\n\n  @staticproperty\n  def leaf():\n    return True\n\n  def enabled(config):\n    return True\n\nclass Direction(Node):\n  argType = Fixed\n\n  @staticproperty\n  def edges():\n    return [North, South, East, West, Stay]\n\n  def deserialize(realm, entity, index: int, obs):\n    return deserialize_fixed_arg(Direction, index)\n\n# a quick helper function\ndef deserialize_fixed_arg(arg, index):\n  if isinstance(index, (int, np.int64)):\n    if index < 0:\n      return None # so that the action will be discarded\n    val = min(index, len(arg.edges)-1)\n    return arg.edges[val]\n\n  # if index is not int, it's probably already deserialized\n  if index not in arg.edges:\n    return None # so that the action will be discarded\n  return index\n\nclass North(Node):\n  delta = (-1, 0)\n\nclass South(Node):\n  delta = (1, 0)\n\nclass East(Node):\n  delta = (0, 1)\n\nclass West(Node):\n  delta = (0, -1)\n\nclass Stay(Node):\n  delta = (0, 0)\n\nclass Attack(Node):\n  priority = 50\n  nodeType = NodeType.SELECTION\n  @staticproperty\n  def n():\n    return 3\n\n  @staticproperty\n  def edges():\n    return [Style, Target]\n\n  @staticproperty\n  def leaf():\n    return True\n\n  def enabled(config):\n    return config.COMBAT_SYSTEM_ENABLED\n\n  def in_range(entity, stim, config, N):\n    R, C = stim.shape\n    R, C = R//2, C//2\n\n    rets = set([entity])\n    for r in range(R-N, R+N+1):\n      for c in range(C-N, C+N+1):\n        for e in stim[r, c].entities.values():\n          rets.add(e)\n\n    rets = list(rets)\n    return rets\n\n  def call(realm, entity, style, target):\n    if style is None or target is None:\n      return None\n\n    assert entity.alive, \"Dead entity cannot act\"\n\n    config = realm.config\n    if entity.is_player and not config.COMBAT_SYSTEM_ENABLED:\n      return None\n\n    # Testing a spawn immunity against old agents to avoid spawn camping\n    immunity = config.COMBAT_SPAWN_IMMUNITY\n    if entity.is_player and target.is_player and \\\n      target.history.time_alive < immunity:\n      return None\n\n    #Check if self targeted or target already dead\n    if entity.ent_id == target.ent_id or not target.alive:\n      return None\n\n    #Can't attack out of range\n    if utils.linf_single(entity.pos, target.pos) > style.attack_range(config):\n      return None\n\n    #Execute attack\n    entity.history.attack = {}\n    entity.history.attack[\"target\"] = target.ent_id\n    entity.history.attack[\"style\"] = style.__name__\n    target.attacker = entity\n    target.attacker_id.update(entity.ent_id)\n\n    from nmmo.systems import combat\n    dmg = combat.attack(realm, entity, target, style.skill)\n\n    # record the combat tick for both entities\n    # players and npcs both have latest_combat_tick in EntityState\n    for ent in [entity, target]:\n      ent.latest_combat_tick.update(realm.tick + 1) # because the tick is about to increment\n\n    return dmg\n\nclass Style(Node):\n  argType = Fixed\n  @staticproperty\n  def edges():\n    return [Melee, Range, Mage]\n\n  def deserialize(realm, entity, index: int, obs):\n    return deserialize_fixed_arg(Style, index)\n\nclass Target(Node):\n  argType = None\n\n  @classmethod\n  def N(cls, config):\n    return config.PLAYER_N_OBS + cls.noop_action\n\n  def deserialize(realm, entity, index: int, obs: Observation):\n    if index >= len(obs.entities.ids):\n      return None\n    return realm.entity_or_none(obs.entities.ids[index])\n\nclass Melee(Node):\n  nodeType = NodeType.ACTION\n  freeze=False\n\n  def attack_range(config):\n    return config.COMBAT_MELEE_REACH\n\n  def skill(entity):\n    return entity.skills.melee\n\nclass Range(Node):\n  nodeType = NodeType.ACTION\n  freeze=False\n\n  def attack_range(config):\n    return config.COMBAT_RANGE_REACH\n\n  def skill(entity):\n    return entity.skills.range\n\nclass Mage(Node):\n  nodeType = NodeType.ACTION\n  freeze=False\n\n  def attack_range(config):\n    return config.COMBAT_MAGE_REACH\n\n  def skill(entity):\n    return entity.skills.mage\n\nclass InventoryItem(Node):\n  argType  = None\n\n  @classmethod\n  def N(cls, config):\n    return config.INVENTORY_N_OBS + cls.noop_action\n\n  def deserialize(realm, entity, index: int, obs: Observation):\n    if index >= len(obs.inventory.ids):\n      return None\n    return realm.items.get(obs.inventory.ids[index])\n\nclass Use(Node):\n  priority = 10\n\n  @staticproperty\n  def edges():\n    return [InventoryItem]\n\n  def enabled(config):\n    return config.ITEM_SYSTEM_ENABLED\n\n  def call(realm, entity, item):\n    if item is None or item.owner_id.val != entity.ent_id:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot use an item\"\n    assert item.quantity.val > 0, \"Item quantity cannot be 0\" # indicates item leak\n\n    if not realm.config.ITEM_SYSTEM_ENABLED:\n      return\n\n    if item not in entity.inventory:\n      return\n\n    if entity.in_combat: # player cannot use item during combat\n      return\n\n    # cannot use listed items or items that have higher level\n    if item.listed_price.val > 0 or item.level_gt(entity):\n      return\n\n    item.use(entity)\n\nclass Destroy(Node):\n  priority = 40\n\n  @staticproperty\n  def edges():\n    return [InventoryItem]\n\n  def enabled(config):\n    return config.ITEM_SYSTEM_ENABLED\n\n  def call(realm, entity, item):\n    if item is None or item.owner_id.val != entity.ent_id:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot destroy an item\"\n    assert item.quantity.val > 0, \"Item quantity cannot be 0\" # indicates item leak\n\n    if not realm.config.ITEM_SYSTEM_ENABLED:\n      return\n\n    if item not in entity.inventory:\n      return\n\n    if item.equipped.val: # cannot destroy equipped item\n      return\n\n    if entity.in_combat: # player cannot destroy item during combat\n      return\n\n    item.destroy()\n\n    realm.event_log.record(EventCode.DESTROY_ITEM, entity)\n\nclass Give(Node):\n  priority = 30\n\n  @staticproperty\n  def edges():\n    return [InventoryItem, Target]\n\n  def enabled(config):\n    return config.ITEM_SYSTEM_ENABLED\n\n  def call(realm, entity, item, target):\n    if item is None or item.owner_id.val != entity.ent_id or target is None:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot give an item\"\n    assert item.quantity.val > 0, \"Item quantity cannot be 0\" # indicates item leak\n\n    config = realm.config\n    if not config.ITEM_SYSTEM_ENABLED:\n      return\n\n    if not (target.is_player and target.alive):\n      return\n\n    if item not in entity.inventory:\n      return\n\n    # cannot give the equipped or listed item\n    if item.equipped.val or item.listed_price.val:\n      return\n\n    if entity.in_combat: # player cannot give item during combat\n      return\n\n    if not (config.ITEM_ALLOW_GIFT and\n            entity.ent_id != target.ent_id and                      # but not self\n            target.is_player):\n      return\n\n    # NOTE: allow give within the visual range\n    if utils.linf_single(entity.pos, target.pos) > config.PLAYER_VISION_RADIUS:\n      return\n\n    if not target.inventory.space:\n      # receiver inventory is full - see if it has an ammo stack with the same sig\n      if isinstance(item, Stack):\n        if not target.inventory.has_stack(item.signature):\n          # no ammo stack with the same signature, so cannot give\n          return\n      else: # no space, and item is not ammo stack, so cannot give\n        return\n\n    entity.inventory.remove(item)\n    target.inventory.receive(item)\n\n    realm.event_log.record(EventCode.GIVE_ITEM, entity)\n\nclass GiveGold(Node):\n  priority = 30\n\n  @staticproperty\n  def edges():\n    # CHECK ME: for now using Price to indicate the gold amount to give\n    return [Price, Target]\n\n  def enabled(config):\n    return config.EXCHANGE_SYSTEM_ENABLED\n\n  def call(realm, entity, amount, target):\n    if amount is None or target is None:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot give gold\"\n\n    config = realm.config\n    if not config.EXCHANGE_SYSTEM_ENABLED:\n      return\n\n    if not (target.is_player and target.alive):\n      return\n\n    if entity.in_combat: # player cannot give gold during combat\n      return\n\n    if not (config.ITEM_ALLOW_GIFT and\n            entity.ent_id != target.ent_id and                      # but not self\n            target.is_player):\n      return\n\n    # NOTE: allow give within the visual range\n    if utils.linf_single(entity.pos, target.pos) > config.PLAYER_VISION_RADIUS:\n      return\n\n    if not isinstance(amount, int):\n      amount = amount.val\n\n    if amount > entity.gold.val: # no gold to give\n      return\n\n    entity.gold.decrement(amount)\n    target.gold.increment(amount)\n\n    realm.event_log.record(EventCode.GIVE_GOLD, entity)\n\nclass MarketItem(Node):\n  argType  = None\n\n  @classmethod\n  def N(cls, config):\n    return config.MARKET_N_OBS + cls.noop_action\n\n  def deserialize(realm, entity, index: int, obs: Observation):\n    if index >= len(obs.market.ids):\n      return None\n    return realm.items.get(obs.market.ids[index])\n\nclass Buy(Node):\n  priority = 20\n  argType  = Fixed\n\n  @staticproperty\n  def edges():\n    return [MarketItem]\n\n  def enabled(config):\n    return config.EXCHANGE_SYSTEM_ENABLED\n\n  def call(realm, entity, item):\n    if item is None or item.owner_id.val == 0:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot buy an item\"\n    assert item.quantity.val > 0, \"Item quantity cannot be 0\" # indicates item leak\n    assert item.equipped.val == 0, \"Listed item must not be equipped\"\n\n    if not realm.config.EXCHANGE_SYSTEM_ENABLED:\n      return\n\n    if entity.gold.val < item.listed_price.val: # not enough money\n      return\n\n    if entity.ent_id == item.owner_id.val: # cannot buy own item\n      return\n\n    if entity.in_combat: # player cannot buy item during combat\n      return\n\n    if not entity.inventory.space:\n      # buyer inventory is full - see if it has an ammo stack with the same sig\n      if isinstance(item, Stack):\n        if not entity.inventory.has_stack(item.signature):\n          # no ammo stack with the same signature, so cannot give\n          return\n      else: # no space, and item is not ammo stack, so cannot give\n        return\n\n    # one can try to buy, but the listing might have gone (perhaps bought by other)\n    realm.exchange.buy(entity, item)\n\nclass Sell(Node):\n  priority = 70\n  argType  = Fixed\n\n  @staticproperty\n  def edges():\n    return [InventoryItem, Price]\n\n  def enabled(config):\n    return config.EXCHANGE_SYSTEM_ENABLED\n\n  def call(realm, entity, item, price):\n    if item is None or item.owner_id.val != entity.ent_id or price is None:\n      return\n\n    assert entity.alive, \"Dead entity cannot act\"\n    assert entity.is_player, \"Npcs cannot sell an item\"\n    assert item.quantity.val > 0, \"Item quantity cannot be 0\" # indicates item leak\n\n    if not realm.config.EXCHANGE_SYSTEM_ENABLED:\n      return\n\n    if item not in entity.inventory:\n      return\n\n    if entity.in_combat: # player cannot sell item during combat\n      return\n\n    # cannot sell the equipped or listed item\n    if item.equipped.val or item.listed_price.val:\n      return\n\n    if not isinstance(price, int):\n      price = price.val\n\n    if not price > 0:\n      return\n\n    realm.exchange.sell(entity, item, price, realm.tick)\n\ndef init_discrete(values):\n  classes = []\n  for i in values:\n    name = f\"Discrete_{i}\"\n    cls  = type(name, (object,), {\"val\": i})\n    classes.append(cls)\n\n  return classes\n\nclass Price(Node):\n  argType  = Fixed\n\n  @classmethod\n  def init(cls, config):\n    # gold should be > 0\n    cls.price_range = range(1, config.PRICE_N_OBS+1)\n    Price.classes = init_discrete(cls.price_range)\n\n  @classmethod\n  def index(cls, price):\n    try:\n      return cls.price_range.index(price)\n    except ValueError:\n      # use the max price, which is config.PRICE_N_OBS\n      return len(cls.price_range) - 1\n\n  @staticproperty\n  def edges():\n    return Price.classes\n\n  def deserialize(realm, entity, index: int, obs):\n    return deserialize_fixed_arg(Price, index)\n\nclass Token(Node):\n  argType  = Fixed\n\n  @classmethod\n  def init(cls, config):\n    Token.classes = init_discrete(range(1, config.COMMUNICATION_NUM_TOKENS+1))\n\n  @staticproperty\n  def edges():\n    return Token.classes\n\n  def deserialize(realm, entity, index: int, obs):\n    return deserialize_fixed_arg(Token, index)\n\nclass Comm(Node):\n  argType  = Fixed\n  priority = 99\n\n  @staticproperty\n  def edges():\n    return [Token]\n\n  def enabled(config):\n    return config.COMMUNICATION_SYSTEM_ENABLED\n\n  def call(realm, entity, token):\n    if token is None:\n      return\n\n    entity.message.update(token.val)\n\n#TODO: Solve AGI\nclass BecomeSkynet:\n  pass\n"
  },
  {
    "path": "nmmo/core/agent.py",
    "content": "class Agent:\n  policy   = 'Neural'\n\n  def __init__(self, config, idx):\n    '''Base class for agents\n\n    Args:\n      config: A Config object\n      idx: Unique AgentID int\n    '''\n    self.config = config\n    self.iden   = idx\n    self._np_random = None\n\n  def __call__(self, obs):\n    '''Used by scripted agents to compute actions. Override in subclasses.\n\n    Args:\n        obs: Agent observation provided by the environment\n    '''\n\n  def set_rng(self, np_random):\n    '''Set the random number generator for the agent for reproducibility\n\n    Args:\n        np_random: A numpy random.Generator object\n    '''\n    self._np_random = np_random\n\nclass Scripted(Agent):\n  '''Base class for scripted agents'''\n  policy   = 'Scripted'\n"
  },
  {
    "path": "nmmo/core/config.py",
    "content": "# pylint: disable=invalid-name\nfrom __future__ import annotations\n\nimport os\nimport sys\nimport logging\nimport re\n\nimport nmmo\nfrom nmmo.core.agent import Agent\nfrom nmmo.core.terrain import MapGenerator\nfrom nmmo.lib import utils, material, spawn\n\nCONFIG_ATTR_PATTERN = r\"^[A-Z_]+$\"\nGAME_SYSTEMS = [\"TERRAIN\", \"RESOURCE\", \"COMBAT\", \"NPC\", \"PROGRESSION\", \"ITEM\",\n                \"EQUIPMENT\", \"PROFESSION\", \"EXCHANGE\", \"COMMUNICATION\"]\n\n# These attributes are critical for trainer and must not change from the initial values\nOBS_ATTRS = set([\"MAX_HORIZON\", \"PLAYER_N\", \"MAP_N_OBS\", \"PLAYER_N_OBS\", \"TASK_EMBED_DIM\",\n                 \"ITEM_INVENTORY_CAPACITY\", \"MARKET_N_OBS\", \"PRICE_N_OBS\",\n                 \"COMMUNICATION_NUM_TOKENS\", \"COMMUNICATION_N_OBS\", \"PROVIDE_ACTION_TARGETS\",\n                 \"PROVIDE_DEATH_FOG_OBS\", \"PROVIDE_NOOP_ACTION_TARGET\"])\nIMMUTABLE_ATTRS = set([\"USE_CYTHON\", \"CURRICULUM_FILE_PATH\", \"PLAYER_VISION_RADIUS\", \"MAP_SIZE\",\n                       \"PLAYER_BASE_HEALTH\", \"RESOURCE_BASE\", \"PROGRESSION_LEVEL_MAX\"])\n\n\nclass Template(metaclass=utils.StaticIterable):\n  def __init__(self):\n    self._data = {}\n    cls = type(self)\n\n    # Set defaults from static properties\n    for attr in dir(cls):\n      val = getattr(cls, attr)\n      if re.match(CONFIG_ATTR_PATTERN, attr) and not isinstance(val, property):\n        self._data[attr] = val\n\n  def override(self, **kwargs):\n    for k, v in kwargs.items():\n      err = f'CLI argument: {k} is not a Config property'\n      assert hasattr(self, k), err\n      self.set(k, v)\n\n  def set(self, k, v):\n    if not isinstance(v, property):\n      try:\n        setattr(self, k, v)\n      except AttributeError:\n        logging.error('Cannot set attribute: %s to %s', str(k), str(v))\n        sys.exit()\n    self._data[k] = v\n\n  # pylint: disable=bad-builtin\n  def print(self):\n    key_len = 0\n    for k in self._data:\n      key_len = max(key_len, len(k))\n\n    print('Configuration')\n    for k, v in self._data.items():\n      print(f'   {k:{key_len}s}: {v}')\n\n  def items(self):\n    return self._data.items()\n\n  def __iter__(self):\n    for k in self._data:\n      yield k\n\n  def keys(self):\n    return self._data.keys()\n\n  def values(self):\n    return self._data.values()\n\ndef validate(config):\n  err = 'config.Config is a base class. Use config.{Small, Medium Large}'''\n  assert isinstance(config, Config), err\n  assert config.HORIZON < config.MAX_HORIZON, 'HORIZON must be <= MAX_HORIZON'\n\n  if not config.TERRAIN_SYSTEM_ENABLED:\n    err = 'Invalid Config: {} requires Terrain'\n    assert not config.RESOURCE_SYSTEM_ENABLED, err.format('Resource')\n    assert not config.PROFESSION_SYSTEM_ENABLED, err.format('Profession')\n\n  if not config.COMBAT_SYSTEM_ENABLED:\n    err = 'Invalid Config: {} requires Combat'\n    assert not config.NPC_SYSTEM_ENABLED, err.format('NPC')\n\n  if not config.ITEM_SYSTEM_ENABLED:\n    err = 'Invalid Config: {} requires Inventory'\n    assert not config.EQUIPMENT_SYSTEM_ENABLED, err.format('Equipment')\n    assert not config.PROFESSION_SYSTEM_ENABLED, err.format('Profession')\n    assert not config.EXCHANGE_SYSTEM_ENABLED, err.format('Exchange')\n\n\nclass Config(Template):\n  '''An environment configuration object'''\n  env_initialized = False\n\n  def __init__(self):\n    super().__init__()\n    self._attr_to_reset = []\n\n    # TODO: Come up with a better way\n    # to resolve mixin MRO conflicts\n    for system in GAME_SYSTEMS:\n      if not hasattr(self, f'{system}_SYSTEM_ENABLED'):\n        self.set(f'{system}_SYSTEM_ENABLED', False)\n\n    if __debug__:\n      validate(self)\n\n    deprecated_attrs = [\n      'NENT', 'NPOP', 'AGENTS', 'NMAPS', 'FORCE_MAP_GENERATION', 'SPAWN']\n\n    for attr in deprecated_attrs:\n      assert not hasattr(self, attr), f'{attr} has been deprecated or renamed'\n\n  @property\n  def original(self):\n    return self._data\n\n  def reset(self):\n    '''Reset all attributes changed during the episode'''\n    for attr in self._attr_to_reset:\n      setattr(self, attr, self.original[attr])\n\n  def set(self, k, v):\n    assert self.env_initialized is False, 'Cannot set config attr after env init'\n    super().set(k, v)\n\n  def set_for_episode(self, k, v):\n    '''Set a config property for the current episode'''\n    assert hasattr(self, k), f'Invalid config property: {k}'\n    assert k not in OBS_ATTRS, f'Cannot change OBS config {k} during the episode'\n    assert k not in IMMUTABLE_ATTRS, f'Cannot change {k} during the episode'\n    # Cannot turn on a game system that was not enabled when the env was created\n    if k.endswith('_SYSTEM_ENABLED') and self._data[k] is False and v is True:\n      raise AssertionError(f'Cannot turn on {k} because it was not enabled during env init')\n\n    # Change only the attribute and keep the original value in the data dict\n    setattr(self, k, v)\n    self._attr_to_reset.append(k)\n\n  @property\n  def enabled_systems(self):\n    '''Return a list of the enabled systems from Env.__init__()'''\n    return [k[:-len('_SYSTEM_ENABLED')]\n            for k, v in self._data.items() if k.endswith('_SYSTEM_ENABLED') and v is True]\n\n  @property\n  def system_states(self):\n    '''Return a one-hot encoding of each system enabled/disabled,\n       which can be used as an observation and changed from episode to episode'''\n    return [int(getattr(self, f'{system}_SYSTEM_ENABLED')) for system in GAME_SYSTEMS]\n\n  def are_systems_enabled(self, systems):  # systems is a list of strings\n    '''Check if all provided systems are enabled'''\n    return all(s.upper() in self.enabled_systems for s in systems)\n\n  def toggle_systems(self, target_systems):  # systems is a list of strings\n    '''Activate only the provided game systems and turn off the others'''\n    target_systems = [s.upper() for s in target_systems]\n    for system in target_systems:\n      assert system in self.enabled_systems, f'Invalid game system: {system}'\n      self.set_for_episode(f'{system}_SYSTEM_ENABLED', True)\n\n    for system in self.enabled_systems:\n      if system not in target_systems:\n        self.set_for_episode(f'{system}_SYSTEM_ENABLED', False)\n\n  ############################################################################\n  ### Meta-Parameters\n  PLAYERS                      = [Agent]\n  '''Player classes from which to spawn'''\n\n  @property\n  def PLAYER_POLICIES(self):\n    '''Number of player policies'''\n    return len(self.PLAYERS)\n\n  PLAYER_N                     = None\n  '''Maximum number of players spawnable in the environment'''\n\n  @property\n  def POSSIBLE_AGENTS(self):\n    '''List of possible agents to spawn'''\n    return list(range(1, self.PLAYER_N + 1))\n\n  # TODO: CHECK if there could be 100+ entities within one's vision\n  PLAYER_N_OBS                 = 100\n  '''Number of distinct agent observations'''\n\n  MAX_HORIZON = 2**15 - 1  # this is arbitrary\n  '''Maximum number of steps the environment can run for'''\n\n  HORIZON = 1024\n  '''Number of steps before the environment resets'''\n\n  GAME_PACKS = None\n  '''List of game packs to load and sample: [(game class, sampling weight)]'''\n\n  CURRICULUM_FILE_PATH = None\n  '''Path to a curriculum task file containing a list of task specs for training'''\n\n  TASK_EMBED_DIM = 4096\n  '''Dimensionality of task embeddings'''\n\n  ALLOW_MULTI_TASKS_PER_AGENT = False\n  '''Whether to allow multiple tasks per agent'''\n\n  PROVIDE_ACTION_TARGETS       = True\n  '''Provide action targets mask'''\n\n  PROVIDE_NOOP_ACTION_TARGET   = True\n  '''Provide a no-op option for each action'''\n\n  PROVIDE_DEATH_FOG_OBS = False\n  '''Provide death fog observation'''\n\n  ALLOW_MOVE_INTO_OCCUPIED_TILE = True\n  '''Whether agents can move into tiles occupied by other agents/npcs\n     However, this does not apply to spawning'''\n\n\n  ############################################################################\n  ### System/debug Parameters\n  USE_CYTHON = True\n  '''Whether to use cython modules for performance'''\n\n  IMMORTAL = False\n  '''Debug parameter: prevents agents from dying except by void'''\n\n\n  ############################################################################\n  ### Player Parameters\n  PLAYER_BASE_HEALTH           = 100\n  '''Initial agent health'''\n\n  PLAYER_VISION_RADIUS         = 7\n  '''Number of tiles an agent can see in any direction'''\n\n  @property\n  def PLAYER_VISION_DIAMETER(self):\n    '''Size of the square tile crop visible to an agent'''\n    return 2*self.PLAYER_VISION_RADIUS + 1\n\n  PLAYER_HEALTH_INCREMENT      = 0\n  '''The amount to increment health by 1 per tick for players, like npcs'''\n\n  DEATH_FOG_ONSET              = None\n  '''How long before spawning death fog. None for no death fog'''\n\n  DEATH_FOG_SPEED              = 1\n  '''Number of tiles per tick that the fog moves in'''\n\n  DEATH_FOG_FINAL_SIZE         = 8\n  '''Number of tiles from the center that the fog stops'''\n\n  PLAYER_LOADER                = spawn.SequentialLoader\n  '''Agent loader class specifying spawn sampling'''\n\n\n  ############################################################################\n  ### Team Parameters\n  TEAMS                        = None  # Dict[Any, List[int]]\n  '''A dictionary of team assignments: key is team_id, value is a list of agent_ids'''\n\n\n  ############################################################################\n  ### Map Parameters\n  MAP_N                        = 1\n  '''Number of maps to generate'''\n\n  MAP_N_TILE                   = len(material.All.materials)\n  '''Number of distinct terrain tile types'''\n\n  @property\n  def MAP_N_OBS(self):\n    '''Number of distinct tile observations'''\n    return int(self.PLAYER_VISION_DIAMETER ** 2)\n\n  MAP_SIZE                     = None\n  '''Size of the whole map, including the center and borders'''\n\n  MAP_CENTER                   = None\n  '''Size of each map (number of tiles along each side), where agents can move around'''\n\n  @property\n  def MAP_BORDER(self):\n    '''Number of background, void border tiles surrounding each side of the map'''\n    return int((self.MAP_SIZE - self.MAP_CENTER) // 2)\n\n  MAP_GENERATOR                = MapGenerator\n  '''Specifies a user map generator. Uses default generator if unspecified.'''\n\n  MAP_FORCE_GENERATION         = True\n  '''Whether to regenerate and overwrite existing maps'''\n\n  MAP_RESET_FROM_FRACTAL       = True\n  '''Whether to regenerate the map from the fractal source'''\n\n  MAP_GENERATE_PREVIEWS        = False\n  '''Whether map generation should also save .png previews (slow + large file size)'''\n\n  MAP_PREVIEW_DOWNSCALE        = 1\n  '''Downscaling factor for png previews'''\n\n\n  ############################################################################\n  ### Path Parameters\n  PATH_ROOT                = os.path.dirname(nmmo.__file__)\n  '''Global repository directory'''\n\n  PATH_CWD                 = os.getcwd()\n  '''Working directory'''\n\n  PATH_RESOURCE            = os.path.join(PATH_ROOT, 'resource')\n  '''Resource directory'''\n\n  PATH_TILE                = os.path.join(PATH_RESOURCE, '{}.png')\n  '''Tile path -- format me with tile name'''\n\n  PATH_MAPS                = None\n  '''Generated map directory'''\n\n  PATH_MAP_SUFFIX          = 'map{}/map.npy'\n  '''Map file name'''\n\n  PATH_FRACTAL_SUFFIX      = 'map{}/fractal.npy'\n  '''Fractal file name'''\n\n\n############################################################################\n### Game Systems (Static Mixins)\nclass Terrain:\n  '''Terrain Game System'''\n\n  TERRAIN_SYSTEM_ENABLED       = True\n  '''Game system flag'''\n\n  TERRAIN_FLIP_SEED            = False\n  '''Whether to negate the seed used for generation (useful for unique heldout maps)'''\n\n  TERRAIN_FREQUENCY            = -3\n  '''Base noise frequency range (log2 space)'''\n\n  TERRAIN_FREQUENCY_OFFSET     = 7\n  '''Noise frequency octave offset (log2 space)'''\n\n  TERRAIN_LOG_INTERPOLATE_MIN  = -2\n  '''Minimum interpolation log-strength for noise frequencies'''\n\n  TERRAIN_LOG_INTERPOLATE_MAX  = 0\n  '''Maximum interpolation log-strength for noise frequencies'''\n\n  TERRAIN_TILES_PER_OCTAVE     = 8\n  '''Number of octaves sampled from log2 spaced TERRAIN_FREQUENCY range'''\n\n  TERRAIN_VOID                 = 0.0\n  '''Noise threshold for void generation'''\n\n  TERRAIN_WATER                = 0.30\n  '''Noise threshold for water generation'''\n\n  TERRAIN_GRASS                = 0.70\n  '''Noise threshold for grass'''\n\n  TERRAIN_FOILAGE              = 0.85\n  '''Noise threshold for foilage (food tile)'''\n\n  TERRAIN_RESET_TO_GRASS       = False\n  '''Whether to make all tiles grass.\n     Only works when MAP_RESET_FROM_FRACTAL is True'''\n\n  TERRAIN_DISABLE_STONE        = False\n  '''Disable stone (obstacle) tiles'''\n\n  TERRAIN_SCATTER_EXTRA_RESOURCES = True\n  '''Whether to scatter extra food, water on the map.\n     Only works when MAP_RESET_FROM_FRACTAL is True'''\n\nclass Resource:\n  '''Resource Game System'''\n\n  RESOURCE_SYSTEM_ENABLED             = True\n  '''Game system flag'''\n\n  RESOURCE_BASE                       = 100\n  '''Initial level and capacity for food and water'''\n\n  RESOURCE_DEPLETION_RATE             = 5\n  '''Depletion rate for food and water'''\n\n  RESOURCE_STARVATION_RATE            = 10\n  '''Damage per tick without food'''\n\n  RESOURCE_DEHYDRATION_RATE           = 10\n  '''Damage per tick without water'''\n\n  RESOURCE_RESILIENT_POPULATION       = 0\n  '''Training helper: proportion of population that is resilient to starvation and dehydration\n     (e.g. 0.1 means 10% of the population is resilient to starvation and dehydration)\n     This is to make some agents live longer during training to sample from \"advanced\" agents.'''\n\n  RESOURCE_DAMAGE_REDUCTION           = 0.5\n  '''Training helper: damage reduction from starvation and dehydration for resilient agents'''\n\n  RESOURCE_FOILAGE_CAPACITY           = 1\n  '''Maximum number of harvests before a foilage tile decays'''\n\n  RESOURCE_FOILAGE_RESPAWN            = 0.025\n  '''Probability that a harvested foilage tile will regenerate each tick'''\n\n  RESOURCE_HARVEST_RESTORE_FRACTION   = 1.0\n  '''Fraction of maximum capacity restored upon collecting a resource'''\n\n  RESOURCE_HEALTH_REGEN_THRESHOLD     = 0.5\n  '''Fraction of maximum resource capacity required to regen health'''\n\n  RESOURCE_HEALTH_RESTORE_FRACTION    = 0.1\n  '''Fraction of health restored per tick when above half food+water'''\n\n\n# NOTE: Included self to be picklable (in torch.save) since lambdas are not picklable\ndef original_combat_damage_formula(self, offense, defense, multiplier, minimum_proportion):\n  # pylint: disable=unused-argument\n  return int(multiplier * (offense * (15 / (15 + defense))))\n\ndef alt_combat_damage_formula(self, offense, defense, multiplier, minimum_proportion):\n  # pylint: disable=unused-argument\n  return int(max(multiplier * offense - defense, offense * minimum_proportion))\n\nclass Combat:\n  '''Combat Game System'''\n\n  COMBAT_SYSTEM_ENABLED              = True\n  '''Game system flag'''\n\n  COMBAT_SPAWN_IMMUNITY              = 20\n  '''Agents older than this many ticks cannot attack agents younger than this many ticks'''\n\n  COMBAT_ALLOW_FLEXIBLE_STYLE        = True\n  '''Whether to allow agents to attack with any style in a given turn''' \n\n  COMBAT_STATUS_DURATION             = 3\n  '''Combat status lasts for this many ticks after the last combat event.\n     Combat events include both attacking and being attacked.'''\n\n  COMBAT_WEAKNESS_MULTIPLIER         = 1.5\n  '''Multiplier for super-effective attacks'''\n\n  COMBAT_MINIMUM_DAMAGE_PROPORTION   = 0.25\n  '''Minimum proportion of damage to inflict on a target'''\n\n  # NOTE: When using a custom function, include \"self\" as the first arg\n  COMBAT_DAMAGE_FORMULA = alt_combat_damage_formula\n  '''Damage formula'''\n\n  COMBAT_MELEE_DAMAGE                = 10\n  '''Melee attack damage'''\n\n  COMBAT_MELEE_REACH                 = 3\n  '''Reach of attacks using the Melee skill'''\n\n  COMBAT_RANGE_DAMAGE                = 10\n  '''Range attack damage'''\n\n  COMBAT_RANGE_REACH                 = 3\n  '''Reach of attacks using the Range skill'''\n\n  COMBAT_MAGE_DAMAGE                 = 10\n  '''Mage attack damage'''\n\n  COMBAT_MAGE_REACH                  = 3\n  '''Reach of attacks using the Mage skill'''\n\n\ndef default_exp_threshold(base_exp, max_level):\n  import math\n  additional_exp_per_level = [round(base_exp * math.sqrt(lvl))\n                              for lvl in range(1, max_level+1)]\n  return [sum(additional_exp_per_level[:lvl]) for lvl in range(max_level)]\n\nclass Progression:\n  '''Progression Game System'''\n\n  PROGRESSION_SYSTEM_ENABLED        = True\n  '''Game system flag'''\n\n  PROGRESSION_BASE_LEVEL            = 1\n  '''Initial skill level'''\n\n  PROGRESSION_LEVEL_MAX             = 10\n  '''Max skill level'''\n\n  PROGRESSION_EXP_THRESHOLD         = default_exp_threshold(90, PROGRESSION_LEVEL_MAX)\n  '''A list of experience thresholds for each level'''\n\n  PROGRESSION_COMBAT_XP_SCALE       = 6\n  '''Additional XP for each attack for skills Melee, Range, and Mage'''\n\n  PROGRESSION_AMMUNITION_XP_SCALE   = 15\n  '''Additional XP for each harvest for Prospecting, Carving, and Alchemy'''\n\n  PROGRESSION_CONSUMABLE_XP_SCALE   = 30\n  '''Multiplier XP for each harvest for Fishing and Herbalism'''\n\n  PROGRESSION_MELEE_BASE_DAMAGE     = 10\n  '''Base Melee attack damage'''\n\n  PROGRESSION_MELEE_LEVEL_DAMAGE    = 5\n  '''Bonus Melee attack damage per level'''\n\n  PROGRESSION_RANGE_BASE_DAMAGE     = 10\n  '''Base Range attack damage'''\n\n  PROGRESSION_RANGE_LEVEL_DAMAGE    = 5\n  '''Bonus Range attack damage per level'''\n\n  PROGRESSION_MAGE_BASE_DAMAGE      = 10\n  '''Base Mage attack damage '''\n\n  PROGRESSION_MAGE_LEVEL_DAMAGE     = 5\n  '''Bonus Mage attack damage per level'''\n\n  PROGRESSION_BASE_DEFENSE          = 0\n  '''Base defense'''\n\n  PROGRESSION_LEVEL_DEFENSE         = 5\n  '''Bonus defense per level'''\n\n\nclass NPC:\n  '''NPC Game System'''\n\n  NPC_SYSTEM_ENABLED                  = True\n  '''Game system flag'''\n\n  NPC_N                               = None\n  '''Maximum number of NPCs spawnable in the environment'''\n\n  NPC_DEFAULT_REFILL_DEAD_NPCS        = True\n  '''Whether to refill dead NPCs'''\n\n  NPC_SPAWN_ATTEMPTS                  = 25\n  '''Number of NPC spawn attempts per tick'''\n\n  NPC_SPAWN_AGGRESSIVE                = 0.80\n  '''Percentage distance threshold from spawn for aggressive NPCs'''\n\n  NPC_SPAWN_NEUTRAL                   = 0.50\n  '''Percentage distance threshold from spawn for neutral NPCs'''\n\n  NPC_SPAWN_PASSIVE                   = 0.00\n  '''Percentage distance threshold from spawn for passive NPCs'''\n\n  NPC_LEVEL_MIN                       = 1\n  '''Minimum NPC level'''\n\n  NPC_LEVEL_MAX                       = 10\n  '''Maximum NPC level'''\n\n  NPC_BASE_DEFENSE                    = 0\n  '''Base NPC defense'''\n\n  NPC_LEVEL_DEFENSE                   = 8\n  '''Bonus NPC defense per level'''\n\n  NPC_BASE_DAMAGE                     = 0\n  '''Base NPC damage'''\n\n  NPC_LEVEL_DAMAGE                    = 8\n  '''Bonus NPC damage per level'''\n\n  NPC_LEVEL_MULTIPLIER                = 1.0\n  '''Multiplier for NPC level damage and defense, for easier difficulty tuning'''\n\n  NPC_ALLOW_ATTACK_OTHER_NPCS         = False\n  '''Whether NPCs can attack other NPCs'''\n\n\nclass Item:\n  '''Inventory Game System'''\n\n  ITEM_SYSTEM_ENABLED                 = True\n  '''Game system flag'''\n\n  ITEM_N                              = 17\n  '''Number of unique base item classes'''\n\n  ITEM_INVENTORY_CAPACITY             = 12\n  '''Number of inventory spaces'''\n\n  ITEM_ALLOW_GIFT               = True\n  '''Whether agents can give gold/item to each other'''\n\n  @property\n  def INVENTORY_N_OBS(self):\n    '''Number of distinct item observations'''\n    return self.ITEM_INVENTORY_CAPACITY\n\n\nclass Equipment:\n  '''Equipment Game System'''\n\n  EQUIPMENT_SYSTEM_ENABLED             = True\n  '''Game system flag'''\n\n  WEAPON_DROP_PROB = 0.025\n  '''Chance of getting a weapon while harvesting ammunition'''\n\n  EQUIPMENT_WEAPON_BASE_DAMAGE         = 5\n  '''Base weapon damage'''\n\n  EQUIPMENT_WEAPON_LEVEL_DAMAGE        = 5\n  '''Added weapon damage per level'''\n\n  EQUIPMENT_AMMUNITION_BASE_DAMAGE     = 5\n  '''Base ammunition damage'''\n\n  EQUIPMENT_AMMUNITION_LEVEL_DAMAGE    = 10\n  '''Added ammunition damage per level'''\n\n  EQUIPMENT_TOOL_BASE_DEFENSE          = 15\n  '''Base tool defense'''\n\n  EQUIPMENT_TOOL_LEVEL_DEFENSE         = 0\n  '''Added tool defense per level'''\n\n  EQUIPMENT_ARMOR_BASE_DEFENSE         = 0\n  '''Base armor defense'''\n\n  EQUIPMENT_ARMOR_LEVEL_DEFENSE        = 3\n  '''Base equipment defense'''\n\n\nclass Profession:\n  '''Profession Game System'''\n\n  PROFESSION_SYSTEM_ENABLED           = True\n  '''Game system flag'''\n\n  PROFESSION_TREE_CAPACITY            = 1\n  '''Maximum number of harvests before a tree tile decays'''\n\n  PROFESSION_TREE_RESPAWN             = 0.105\n  '''Probability that a harvested tree tile will regenerate each tick'''\n\n  PROFESSION_ORE_CAPACITY             = 1\n  '''Maximum number of harvests before an ore tile decays'''\n\n  PROFESSION_ORE_RESPAWN              = 0.10\n  '''Probability that a harvested ore tile will regenerate each tick'''\n\n  PROFESSION_CRYSTAL_CAPACITY         = 1\n  '''Maximum number of harvests before a crystal tile decays'''\n\n  PROFESSION_CRYSTAL_RESPAWN          = 0.10\n  '''Probability that a harvested crystal tile will regenerate each tick'''\n\n  PROFESSION_HERB_CAPACITY            = 1\n  '''Maximum number of harvests before an herb tile decays'''\n\n  PROFESSION_HERB_RESPAWN             = 0.02\n  '''Probability that a harvested herb tile will regenerate each tick'''\n\n  PROFESSION_FISH_CAPACITY            = 1\n  '''Maximum number of harvests before a fish tile decays'''\n\n  PROFESSION_FISH_RESPAWN             = 0.02\n  '''Probability that a harvested fish tile will regenerate each tick'''\n\n  def PROFESSION_CONSUMABLE_RESTORE(self, level):\n    '''Amount of food/water restored by consuming a consumable item'''\n    return 50 + 5*level\n\n\nclass Exchange:\n  '''Exchange Game System'''\n\n  EXCHANGE_SYSTEM_ENABLED             = True\n  '''Game system flag'''\n\n  EXCHANGE_BASE_GOLD                  = 1\n  '''Initial gold amount'''\n\n  EXCHANGE_LISTING_DURATION           = 3\n  '''The number of ticks, during which the item is listed for sale'''\n\n  MARKET_N_OBS = 384  # this should be proportion to PLAYER_N\n  '''Number of distinct item observations'''\n\n  PRICE_N_OBS = 99  # make it different from PLAYER_N_OBS\n  '''Number of distinct price observations\n     This also determines the maximum price one can set for an item\n  '''\n\n\nclass Communication:\n  '''Exchange Game System'''\n\n  COMMUNICATION_SYSTEM_ENABLED         = True\n  '''Game system flag'''\n\n  COMMUNICATION_N_OBS                  = 32\n  '''Number of players that share the same communication obs, i.e. the same team'''\n\n  COMMUNICATION_NUM_TOKENS             = 127\n  '''Number of distinct COMM tokens'''\n\n\nclass AllGameSystems(\n  Terrain, Resource, Combat, NPC, Progression, Item,\n  Equipment, Profession, Exchange, Communication):\n  pass\n\n\n############################################################################\n### Config presets\nclass Small(Config):\n  '''A small config for debugging and experiments with an expensive outer loop'''\n\n  PATH_MAPS                    = 'maps/small'\n\n  PLAYER_N                     = 64\n\n  MAP_PREVIEW_DOWNSCALE        = 4\n  MAP_SIZE                     = 64\n  MAP_CENTER                   = 32\n\n  TERRAIN_LOG_INTERPOLATE_MIN  = 0\n\n  NPC_N                        = 32\n  NPC_LEVEL_MAX                = 5\n  NPC_LEVEL_SPREAD             = 1\n\n  PROGRESSION_SPAWN_CLUSTERS   = 4\n  PROGRESSION_SPAWN_UNIFORMS   = 16\n\n  HORIZON                      = 128\n\n\nclass Medium(Config):\n  '''A medium config suitable for most academic-scale research'''\n\n  PATH_MAPS                    = 'maps/medium'\n\n  PLAYER_N                     = 128\n\n  MAP_PREVIEW_DOWNSCALE        = 16\n  MAP_SIZE                     = 160\n  MAP_CENTER                   = 128\n\n  NPC_N                        = 128\n  NPC_LEVEL_MAX                = 10\n  NPC_LEVEL_SPREAD             = 1\n\n  PROGRESSION_SPAWN_CLUSTERS   = 64\n  PROGRESSION_SPAWN_UNIFORMS   = 256\n\n  HORIZON                      = 1024\n\n\nclass Large(Config):\n  '''A large config suitable for large-scale research or fast models'''\n\n  PATH_MAPS                    = 'maps/large'\n\n  PLAYER_N                     = 1024\n\n  MAP_PREVIEW_DOWNSCALE        = 64\n  MAP_SIZE                     = 1056\n  MAP_CENTER                   = 1024\n\n  NPC_N                        = 1024\n  NPC_LEVEL_MAX                = 15\n  NPC_LEVEL_SPREAD             = 3\n\n  PROGRESSION_SPAWN_CLUSTERS   = 1024\n  PROGRESSION_SPAWN_UNIFORMS   = 4096\n\n  HORIZON                 = 8192\n\n\nclass Default(Medium, AllGameSystems):\n  pass\n"
  },
  {
    "path": "nmmo/core/env.py",
    "content": "import os\nimport functools\nfrom typing import Any, Dict, List, Callable\nfrom collections import defaultdict\nfrom copy import deepcopy\n\nimport gymnasium as gym\nimport dill\nimport numpy as np\nfrom pettingzoo.utils.env import AgentID, ParallelEnv\n\nimport nmmo\nfrom nmmo.core import realm\nfrom nmmo.core import game_api\nfrom nmmo.core.config import Default\nfrom nmmo.core.observation import Observation\nfrom nmmo.core.tile import Tile\nfrom nmmo.entity.entity import Entity\nfrom nmmo.systems.item import Item\nfrom nmmo.task.game_state import GameStateGenerator\nfrom nmmo.lib import seeding\n\nclass Env(ParallelEnv):\n  # Environment wrapper for Neural MMO using the Parallel PettingZoo API\n\n  #pylint: disable=no-value-for-parameter\n  def __init__(self,\n               config: Default = nmmo.config.Default(),\n               seed = None):\n    '''Initializes the Neural MMO environment.\n\n    Args:\n      config (Default, optional): Configuration object for the environment.\n      Defaults to nmmo.config.Default().\n      seed (int, optional): Random seed for the environment. Defaults to None.\n    '''\n    self._np_random = None\n    self._np_seed = None\n    self._reset_required = True\n    self.seed(seed)\n    super().__init__()\n\n    self.config = config\n    self.config.env_initialized = True\n\n    # Generate maps if they do not exist\n    config.MAP_GENERATOR(config).generate_all_maps(self._np_seed)\n    self.realm = realm.Realm(config, self._np_random)\n    self.tile_map = None\n    self.tile_obs_shape = None\n\n    self.possible_agents = self.config.POSSIBLE_AGENTS\n    self._alive_agents = None\n    self._current_agents = None\n    self._dead_this_tick = None\n    self.scripted_agents = set()\n\n    self.obs = {agent_id: Observation(self.config, agent_id)\n                for agent_id in self.possible_agents}\n    self._dummy_task_embedding = np.zeros(self.config.TASK_EMBED_DIM, dtype=np.float16)\n    self._dummy_obs = Observation(self.config, 0).empty_obs\n    self._comm_obs = {}\n\n    self._gamestate_generator = GameStateGenerator(self.realm, self.config)\n    self.game_state = None\n    self.tasks = None\n    self.agent_task_map = {}\n\n    # curriculum file path, if provided, should exist\n    self.curriculum_file_path = config.CURRICULUM_FILE_PATH\n    if self.curriculum_file_path is not None:\n      # try to open the file to check if it exists\n      with open(self.curriculum_file_path, 'rb') as f:\n        dill.load(f)\n      f.close()\n\n    self.game = None\n    # NOTE: The default game runs with the full provided config and unmodded realm.reset()\n    self.default_game = game_api.DefaultGame(self)\n    self.game_packs: List[game_api.Game] = None\n    if config.GAME_PACKS:  # assume List[Tuple(class, weight)]\n      self.game_packs = [game_cls(self, weight) for game_cls, weight in config.GAME_PACKS]\n\n  @functools.cached_property\n  def _obs_space(self):\n    def box(rows, cols):\n      return gym.spaces.Box(\n          low=-2**15, high=2**15-1,\n          shape=(rows, cols),\n          dtype=np.int16)\n    def mask_box(length):\n      return gym.spaces.Box(low=0, high=1, shape=(length,), dtype=np.int8)\n\n    # NOTE: obs space-related config attributes must NOT be changed after init\n    num_tile_attributes = len(Tile.State.attr_name_to_col)\n    num_tile_attributes += 1 if self.config.original[\"PROVIDE_DEATH_FOG_OBS\"] else 0\n    obs_space = {\n      \"CurrentTick\": gym.spaces.Discrete(self.config.MAX_HORIZON),\n      \"AgentId\": gym.spaces.Discrete(self.config.PLAYER_N+1),\n      \"Tile\": box(self.config.MAP_N_OBS, num_tile_attributes),\n      \"Entity\": box(self.config.PLAYER_N_OBS, Entity.State.num_attributes),\n      \"Task\": gym.spaces.Box(low=-2**15, high=2**15-1,\n                             shape=(self.config.TASK_EMBED_DIM,),\n                             dtype=np.float16),\n    }\n\n    # NOTE: cannot turn on a game system that was not enabled during env init\n    if self.config.original[\"ITEM_SYSTEM_ENABLED\"]:\n      obs_space[\"Inventory\"] = box(self.config.INVENTORY_N_OBS, Item.State.num_attributes)\n\n    if self.config.original[\"EXCHANGE_SYSTEM_ENABLED\"]:\n      obs_space[\"Market\"] = box(self.config.MARKET_N_OBS, Item.State.num_attributes)\n\n    if self.config.original[\"COMMUNICATION_SYSTEM_ENABLED\"]:\n      # Comm obs cols: id, row, col, message\n      obs_space[\"Communication\"] = box(self.config.COMMUNICATION_N_OBS, 4)\n\n    if self.config.original[\"PROVIDE_ACTION_TARGETS\"]:\n      mask_spec = deepcopy(self._atn_space)\n      for atn_str in mask_spec:\n        for arg_str in mask_spec[atn_str]:\n          mask_spec[atn_str][arg_str] = mask_box(self._atn_space[atn_str][arg_str].n)\n      obs_space[\"ActionTargets\"] = mask_spec\n\n    return gym.spaces.Dict(obs_space)\n\n  # pylint: disable=method-cache-max-size-none\n  @functools.lru_cache(maxsize=None)\n  def observation_space(self, agent: AgentID):\n    '''Neural MMO Observation Space\n\n      Args:\n        agent (AgentID): The ID of the agent.\n        \n      Returns:\n        gym.spaces.Dict: The observation space for the agent.\n    '''\n    return self._obs_space\n\n  # NOTE: make sure this runs once during trainer init and does NOT change afterwards\n  @functools.cached_property\n  def _atn_space(self):\n    actions = {}\n    for atn in sorted(nmmo.Action.edges(self.config)):\n      if atn.enabled(self.config):\n        actions[atn.__name__] = {}  # use the string key\n        for arg in sorted(atn.edges):\n          n = arg.N(self.config)\n          actions[atn.__name__][arg.__name__] = gym.spaces.Discrete(n)\n        actions[atn.__name__] = gym.spaces.Dict(actions[atn.__name__])\n    return gym.spaces.Dict(actions)\n\n  @functools.cached_property\n  def _str_atn_map(self):\n    '''Map action and argument names to their corresponding objects'''\n    str_map = {}\n    for atn in nmmo.Action.edges(self.config):\n      str_map[atn.__name__] = atn\n      for arg in atn.edges:\n        str_map[arg.__name__] = arg\n    return str_map\n\n  # pylint: disable=method-cache-max-size-none\n  @functools.lru_cache(maxsize=None)\n  def action_space(self, agent: AgentID):\n    '''Neural MMO Action Space\n\n      Args:\n        agent (AgentID): The ID of the agent.\n\n      Returns:\n        gym.spaces.Dict: The action space for the agent.\n    '''\n    return self._atn_space\n\n  ############################################################################\n  # Core API\n\n  def reset(self, seed=None, options=None,  # PettingZoo API args\n            map_id=None,\n            make_task_fn: Callable=None,\n            game: game_api.Game=None):\n    '''Resets the environment and returns the initial observations.\n\n      Args:\n        seed (int, optional): Random seed for the environment. Defaults to None.\n        options (dict, optional): Additional options for resetting the environment.\n          Defaults to None.\n        map_id (int, optional): The ID of the map to load. Defaults to None.\n        make_task_fn (callable, optional): Function to create tasks. Defaults to None.\n        game (Game, optional): The game to be played. Defaults to None.\n\n      Returns:\n        tuple: A tuple containing:\n          - obs (dict): Dictionary mapping agent IDs to their initial observations.\n          - info (dict): Dictionary containing additional information.\n    '''\n    # If options are provided, override the kwargs\n    if options is not None:\n      map_id = options.get('map_id', None) or map_id\n      make_task_fn = options.get('make_task_fn', None) or make_task_fn\n      game = options.get('game', None) or game\n\n    self.seed(seed)\n    map_dict = self._load_map_file(map_id)\n\n    # Choose and reset the game, realm, and tasks\n    if make_task_fn is not None:\n      # Use the provided tasks with the default game (full config, unmodded realm)\n      self.tasks = make_task_fn()\n      self.game = self.default_game\n      self.game.reset(self._np_random, map_dict, self.tasks)  # also does realm.reset()\n    elif game is not None:\n      # Use the provided game, which comes with its own tasks\n      self.game = game\n      self.game.reset(self._np_random, map_dict)\n      self.tasks = self.game.tasks\n    elif self.curriculum_file_path is not None or self.game_packs is not None:\n      # Assume training -- pick a random game from the game packs\n      self.game = self.default_game\n      if self.game_packs:\n        weights = [game.sampling_weight for game in self.game_packs]\n        self.game = self._np_random.choice(self.game_packs, p=weights/np.sum(weights))\n      self.game.reset(self._np_random, map_dict)\n      # use the sampled tasks from self.game\n      self.tasks = self.game.tasks\n    else:\n      # Just reset the same game and tasks as before\n      self.game = self.default_game  # full config, unmodded realm\n      self.game.reset(self._np_random, map_dict, self.tasks)  # use existing tasks\n      if self.tasks is None:\n        self.tasks = self.game.tasks\n      else:\n        for task in self.tasks:\n          task.reset()\n\n    # Reset the agent vars\n    self._alive_agents = self.possible_agents\n    self._dead_this_tick = {}\n    self._map_task_to_agent()\n    self._current_agents = self.possible_agents  # tracking alive + dead_this_tick\n\n    # Check scripted agents\n    self.scripted_agents.clear()\n    for eid, ent in self.realm.players.items():\n      if isinstance(ent.agent, nmmo.Scripted):\n        self.scripted_agents.add(eid)\n        ent.agent.set_rng(self._np_random)\n\n    # Tile map placeholder, to reduce redudunt obs computation\n    self.tile_map = Tile.Query.get_map(self.realm.datastore, self.config.MAP_SIZE)\n    if self.config.PROVIDE_DEATH_FOG_OBS:\n      fog_map = np.round(self.realm.fog_map)[:,:,np.newaxis].astype(np.int16)\n      self.tile_map = np.concatenate((self.tile_map, fog_map), axis=-1)\n    self.tile_obs_shape = (self.config.PLAYER_VISION_DIAMETER**2, self.tile_map.shape[-1])\n\n    # Reset the obs, game state generator\n    infos = {}\n    for agent_id in self.possible_agents:\n      # NOTE: the tasks for each agent is in self.agent_task_map, and task embeddings are\n      #   available in each task instance, via task.embedding\n      #   For now, each agent is assigned to a single task, so we just use the first task\n      # TODO: can the embeddings of multiple tasks be superposed while preserving the\n      #   task-specific information? This needs research\n      task_embedding = self._dummy_task_embedding\n      if agent_id in self.agent_task_map:\n        task_embedding = self.agent_task_map[agent_id][0].embedding\n        infos[agent_id] = {\"task\": self.agent_task_map[agent_id][0].name}\n      self.obs[agent_id].reset(self.realm.map.habitable_tiles, task_embedding)\n    self._compute_observations()\n    self._gamestate_generator = GameStateGenerator(self.realm, self.config)\n    if self.game_state is not None:\n      self.game_state.clear_cache()\n      self.game_state = None\n\n    self._reset_required = False\n\n    return {a: o.to_gym() for a,o in self.obs.items()}, infos\n\n  def _load_map_file(self, map_id: int=None):\n    '''Loads a map file, which is a 2D numpy array'''\n    map_dict= {}\n    map_id = map_id or self._np_random.integers(self.config.MAP_N) + 1\n    map_file_path = os.path.join(self.config.PATH_CWD, self.config.PATH_MAPS,\n                                 self.config.PATH_MAP_SUFFIX.format(map_id))\n    map_dict[\"map\"] = np.load(map_file_path)\n    if self.config.MAP_RESET_FROM_FRACTAL:\n      fractal_file_path = os.path.join(self.config.PATH_CWD, self.config.PATH_MAPS,\n                                       self.config.PATH_FRACTAL_SUFFIX.format(map_id))\n      map_dict[\"fractal\"] = np.load(fractal_file_path).astype(float)\n    return map_dict\n\n  def _map_task_to_agent(self):\n    self.agent_task_map.clear()\n    for agent_id in self.agents:\n      self.realm.players[agent_id].my_task = None\n    for task in self.tasks:\n      if task.embedding is None:\n        task.set_embedding(self._dummy_task_embedding)\n      # map task to agents\n      for agent_id in task.assignee:\n        if agent_id in self.agent_task_map:\n          self.agent_task_map[agent_id].append(task)\n        else:\n          self.agent_task_map[agent_id] = [task]\n\n    # for now we only support one task per agent\n    if self.config.ALLOW_MULTI_TASKS_PER_AGENT is False:\n      for agent_id, agent_tasks in self.agent_task_map.items():\n        assert len(agent_tasks) == 1, \"Only one task per agent is supported\"\n        self.realm.players[agent_id].my_task = agent_tasks[0]\n\n  def step(self, actions: Dict[int, Dict[str, Dict[str, Any]]]):\n    '''Performs one step in the environment given the provided actions.\n\n      Args:\n        actions (dict): Dictionary mapping agent IDs to their actions.\n\n      Returns:\n        tuple: A tuple containing:\n          - obs (dict): Dictionary mapping agent IDs to their new observations.\n          - rewards (dict): Dictionary mapping agent IDs to their rewards.\n          - terminated (dict): Dictionary mapping agent IDs to whether they reached \n            a terminal state.\n          - truncated (dict): Dictionary mapping agent IDs to whether the episode was\n            truncated (e.g. reached maximum number of steps).\n          - infos (dict): Dictionary containing additional information.\n    '''\n    assert not self._reset_required, 'step() called before reset'\n    # Add in scripted agents' actions, if any\n    if self.scripted_agents:\n      actions = self._compute_scripted_agent_actions(actions)\n\n    # Drop invalid actions of BOTH neural and scripted agents\n    #   we don't need _deserialize_scripted_actions() anymore\n    actions = self._validate_actions(actions)\n    # Execute actions\n    self._dead_this_tick, dead_npcs = self.realm.step(actions)\n    self._alive_agents = list(self.realm.players.keys())\n    self._current_agents = list(set(self._alive_agents + list(self._dead_this_tick.keys())))\n\n    terminated = {}\n    for agent_id in self._current_agents:\n      if agent_id in self._dead_this_tick:\n        # NOTE: Even though players can be resurrected, the time of death must be marked.\n        terminated[agent_id] = True\n      else:\n        terminated[agent_id] = False\n\n    if self.realm.tick >= self.config.HORIZON:\n      self._alive_agents = []  # pettingzoo requires agents to be empty\n\n    # Update the game stats, determine winners, etc.\n    # Also, resurrect dead agents and/or spawn new npcs if the game allows it\n    self.game.update(terminated, self._dead_this_tick, dead_npcs)\n\n    # Some games do additional player cull during update(), so process truncated here\n    truncated = {}\n    for agent_id in self._current_agents:\n      if self.realm.tick >= self.config.HORIZON:\n        truncated[agent_id] = agent_id in self.realm.players\n      else:\n        truncated[agent_id] = False\n\n    # Store the observations, since actions reference them\n    self._compute_observations()\n    gym_obs = {a: self.obs[a].to_gym() for a in self._current_agents}\n\n    rewards, infos = self._compute_rewards()\n\n    # NOTE: all obs, rewards, dones, infos have data for each agent in self.agents\n    return gym_obs, rewards, terminated, truncated, infos\n\n  @property\n  def dead_this_tick(self):\n    return self._dead_this_tick\n\n  def _validate_actions(self, actions: Dict[int, Dict[str, Dict[str, Any]]]):\n    '''Deserialize action arg values and validate actions\n       For now, it does a basic validation (e.g., value is not none).\n    '''\n    validated_actions = {}\n\n    for ent_id, atns in actions.items():\n      if ent_id not in self.realm.players:\n        #assert ent_id in self.realm.players, f'Entity {ent_id} not in realm'\n        continue # Entity not in the realm -- invalid actions\n\n      entity = self.realm.players[ent_id]\n      if not entity.alive:\n        #assert entity.alive, f'Entity {ent_id} is dead'\n        continue # Entity is dead -- invalid actions\n\n      validated_actions[ent_id] = {}\n\n      for atn_key, args in sorted(atns.items()):\n        action_valid = True\n        deserialized_action = {}\n\n        # If action/system is not enabled, it's not in self._str_atn_map\n        if isinstance(atn_key, str) and atn_key not in self._str_atn_map:\n          action_valid = False\n          continue\n\n        atn = self._str_atn_map[atn_key] if isinstance(atn_key, str) else atn_key\n        if not atn.enabled(self.config):  # This can change from episode to episode\n          action_valid = False\n          continue\n\n        for arg_key, val in sorted(args.items()):\n          arg = self._str_atn_map[arg_key] if isinstance(arg_key, str) else arg_key\n          obj = arg.deserialize(self.realm, entity, val, self.obs[ent_id])\n          if obj is None:\n            action_valid = False\n            break\n          deserialized_action[arg] = obj\n\n        if action_valid:\n          validated_actions[ent_id][atn] = deserialized_action\n\n    return validated_actions\n\n  def _compute_scripted_agent_actions(self, actions: Dict[int, Dict[str, Dict[str, Any]]]):\n    '''Compute actions for scripted agents and add them into the action dict'''\n    dead_agents = set()\n    for agent_id in self.scripted_agents:\n      if agent_id in self.realm.players:\n        # override the provided scripted agents' actions\n        actions[agent_id] = self.realm.players[agent_id].agent(self.obs[agent_id])\n      else:\n        dead_agents.add(agent_id)\n\n    # remove the dead scripted agent from the list\n    self.scripted_agents -= dead_agents\n\n    return actions\n\n  def _compute_observations(self):\n    radius = self.config.PLAYER_VISION_RADIUS\n    market = Item.Query.for_sale(self.realm.datastore) \\\n      if self.config.EXCHANGE_SYSTEM_ENABLED else None\n    self._update_comm_obs()\n    if self.config.PROVIDE_DEATH_FOG_OBS:\n      self.tile_map[:, :, -1] = np.round(self.realm.fog_map)\n\n    for agent_id in self._current_agents:\n      if agent_id not in self.realm.players:\n        self.obs[agent_id].set_agent_dead()\n      else:\n        r, c = self.realm.players.get(agent_id).pos\n        visible_entities = Entity.Query.window(self.realm.datastore, r, c, radius)\n        visible_tiles = self.tile_map[r-radius:r+radius+1,\n                                      c-radius:c+radius+1, :].reshape(self.tile_obs_shape)\n        inventory = Item.Query.owned_by(self.realm.datastore, agent_id) \\\n          if self.config.ITEM_SYSTEM_ENABLED else None\n        comm_obs = self._comm_obs[agent_id] \\\n          if self.config.COMMUNICATION_SYSTEM_ENABLED else None\n        self.obs[agent_id].update(self.realm.tick, visible_tiles, visible_entities,\n                                  inventory=inventory, market=market, comm=comm_obs)\n\n  def _update_comm_obs(self):\n    if not self.config.COMMUNICATION_SYSTEM_ENABLED:\n      return\n    comm_obs = Entity.Query.comm_obs(self.realm.datastore)\n    agent_ids = comm_obs[:, Entity.State.attr_name_to_col['id']]\n    self._comm_obs.clear()\n    for agent_id in self.realm.players:\n      if agent_id not in self._comm_obs:\n        my_team = [agent_id] if agent_id not in self.agent_task_map \\\n          else self.agent_task_map[agent_id][0].assignee  # NOTE: first task only\n        team_obs = [comm_obs[agent_ids == eid] for eid in my_team]\n        if len(team_obs) == 1:\n          team_obs = team_obs[0]\n        else:\n          team_obs = np.concatenate(team_obs, axis=0)\n        for eid in my_team:\n          self._comm_obs[eid] = team_obs\n\n  def _compute_rewards(self):\n    # Initialization\n    agents = set(self._current_agents)\n    infos = {agent_id: {'task': {}} for agent_id in agents}\n    rewards = defaultdict(int)\n\n    # Clean up unnecessary game state, which cause memory leaks\n    if self.game_state is not None:\n      self.game_state.clear_cache()\n      self.game_state = None\n\n    # Compute Rewards and infos\n    self.game_state = self._gamestate_generator.generate(self.realm, self.obs)\n    for task in self.tasks:\n      if agents.intersection(task.assignee): # evaluate only if the agents are current\n        task_rewards, task_infos = task.compute_rewards(self.game_state)\n        for agent_id, reward in task_rewards.items():\n          if agent_id in agents:\n            rewards[agent_id] = rewards.get(agent_id,0) + reward\n            infos[agent_id]['task'][task.name] = task_infos[agent_id] # include progress, etc.\n      else:\n        task.close()  # To prevent memory leak\n\n    # Reward for frozen agents (recon, resurrected, frozen) is 0 because they cannot act\n    for agent_id, agent in self.realm.players.items():\n      if agent.status.frozen:\n        rewards[agent_id] = 0\n\n    # Reward for dead agents is defined by the game\n    # NOTE: Resurrected agents are frozen and in the realm.players, so run through\n    # self._dead_this_tick to give out the dead reward\n    if self.game.assign_dead_reward:\n      for agent_id in self._dead_this_tick:\n        rewards[agent_id] = -1\n\n    return rewards, infos\n\n  ############################################################################\n  # PettingZoo API\n  ############################################################################\n\n  def render(self, mode='human'):\n    '''For conformity with the PettingZoo API only; rendering is external'''\n\n  @property\n  def agents(self) -> List[AgentID]:\n    '''For conformity with the PettingZoo API; retuning only the alive agents'''\n    return self._alive_agents\n\n  def close(self):\n    '''For conformity with the PettingZoo API only; rendering is external'''\n\n  def seed(self, seed=None):\n    '''Reseeds the environment. reset() must be called after seed(), and before step().\n       - self._np_seed is None: seed() has not been called, e.g. __init__() -> new RNG\n       - self._np_seed is set, and seed is not None: seed() or reset() with seed -> new RNG\n\n       If self._np_seed is set, but seed is None\n         probably called from reset() without seed, so don't change the RNG\n    '''\n    if self._np_seed is None or seed is not None:\n      self._np_random, self._np_seed = seeding.np_random(seed)\n      self._reset_required = True\n\n  def state(self) -> np.ndarray:\n    raise NotImplementedError\n\n  metadata = {'render.modes': ['human'], 'name': 'neural-mmo'}\n"
  },
  {
    "path": "nmmo/core/game_api.py",
    "content": "# pylint: disable=no-member,bare-except\nfrom abc import ABC, abstractmethod\nfrom typing import Dict\nfrom collections import deque\nimport dill\nimport numpy as np\n\nfrom nmmo.task import task_api, task_spec, base_predicates\nfrom nmmo.lib import team_helper, utils\n\nGAME_MODE = [\"agent_training\", \"team_training\", \"team_battle\"]\n\n\nclass Game(ABC):\n  game_mode = None\n\n  def __init__(self, env, sampling_weight=None):\n    self.config = env.config\n    self.realm = env.realm\n    self._np_random = env._np_random\n    self.sampling_weight = sampling_weight or 1.0\n    self.tasks = None\n    self.assign_dead_reward = True\n    self._next_tasks = None\n    self._agent_stats = {}\n    self._winners = None\n    self._game_done = False\n    self.history: deque[Dict] = deque(maxlen=100)\n    assert self.is_compatible(), \"Game is not compatible with the config\"\n\n  @abstractmethod\n  def is_compatible(self):\n    \"\"\"Check if the game is compatible with the config (e.g., required systems)\"\"\"\n    raise NotImplementedError\n\n  @property\n  def name(self):\n    return self.__class__.__name__\n\n  @property\n  def winners(self):\n    return self._winners\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      # CHECK ME: should we return the winners\" tasks\" reward multiplier?\n      return 1.0  # default score for task completion\n    return 0.0\n\n  def reset(self, np_random, map_dict, tasks=None):\n    self._np_random = np_random\n    self._set_config()\n    self._set_realm(map_dict)\n    if tasks:\n      # tasks comes from env.reset()\n      self.tasks = tasks\n    elif self._next_tasks:\n      # env.reset() cannot take both game and tasks\n      # so set next_tasks in the game first\n      self.tasks = self._next_tasks\n      self._next_tasks = None\n    else:\n      self.tasks = self._define_tasks()\n    self._post_setup()\n    self._reset_stats()\n\n  def _set_config(self):  # pylint: disable=unused-argument\n    \"\"\"Set config for the episode. Can customize config using config.set_for_episode()\"\"\"\n    self.config.reset()\n\n  def _set_realm(self, map_dict):\n    \"\"\"Set up the realm for the episode. Can customize map and spawn\"\"\"\n    self.realm.reset(self._np_random, map_dict, custom_spawn=False)\n\n  def _post_setup(self):\n    \"\"\"Post-setup processes, e.g., attach team tags, etc.\"\"\"\n\n  def _reset_stats(self):\n    \"\"\"Reset stats for the episode\"\"\"\n    self._agent_stats.clear()\n    self._winners = None\n    self._game_done = False\n    # result = False means the game ended without a winner\n    self.history.append({\"result\": False, \"winners\": None, \"winning_score\": None})\n\n  @abstractmethod\n  def _define_tasks(self):\n    \"\"\"Define tasks for the episode.\"\"\"\n    # NOTE: Task embeddings should be provided somehow, e.g., from curriculum file.\n    # Otherwise, policies cannot be task-conditioned.\n    raise NotImplementedError\n\n  def set_next_tasks(self, tasks):\n    \"\"\"Set the next task to be completed\"\"\"\n    self._next_tasks = tasks\n\n  def update(self, terminated, dead_players, dead_npcs):\n    \"\"\"Process dead players/npcs, update the game stats, winners, etc.\"\"\"\n    self._process_dead_players(terminated, dead_players)\n    self._process_dead_npcs(dead_npcs)\n    self._winners = self._check_winners(terminated)\n    if self._winners and not self._game_done:\n      self._game_done = self.history[-1][\"result\"] = True\n      self.history[-1][\"winners\"] = self._winners\n      self.history[-1][\"winning_score\"] = self.winning_score\n      self.history[-1][\"winning_tick\"] = self.realm.tick\n      self.history[-1].update(self.get_episode_stats())\n\n  def _process_dead_players(self, terminated, dead_players):\n    for agent_id in terminated:\n      if terminated[agent_id]:\n        agent = dead_players[agent_id] if agent_id in dead_players\\\n                                       else self.realm.players[agent_id]\n        self._agent_stats[agent_id] = {\"time_alive\": self.realm.tick,\n                                       \"progress_to_center\": agent.history.exploration}\n\n  def _process_dead_npcs(self, dead_npcs):\n    if self.config.NPC_SYSTEM_ENABLED and self.config.NPC_DEFAULT_REFILL_DEAD_NPCS:\n      for npc in dead_npcs.values():\n        if npc.spawn_danger:\n          self.realm.npcs.spawn_dangers.append(npc.spawn_danger)\n      # refill npcs to target config.NPC_N, within config.NPC_SPAWN_ATTEMPTS\n      self.realm.npcs.default_spawn()\n\n  def _check_winners(self, terminated):\n    # Determine winners for the default task\n    if self.realm.num_players == 1:  # only one survivor\n      return list(self.realm.players.keys())\n    if all(terminated.values()):\n      # declare all winners when they died at the same time\n      return list(terminated.keys())\n    if self.realm.tick >= self.config.HORIZON:\n      # declare all survivors as winners when the time is up\n      return [agent_id for agent_id, done in terminated.items() if not done]\n    return None\n\n  @property\n  def is_over(self):\n    return self.winners is not None or self.realm.num_players == 0 or \\\n           self.realm.tick >= self.config.HORIZON\n\n  def get_episode_stats(self):\n    \"\"\"A helper function for trainers\"\"\"\n    total_agent_steps = 0\n    progress_to_center = 0\n    max_progress = self.config.PLAYER_N * self.config.MAP_SIZE // 2\n    for stat in self._agent_stats.values():\n      total_agent_steps += stat[\"time_alive\"]\n      progress_to_center += stat[\"progress_to_center\"]\n    return {\n      \"total_agent_steps\": total_agent_steps,\n      \"norm_progress_to_center\": float(progress_to_center) / max_progress\n    }\n\n  ############################\n  # Helper functions for Game\n  def _who_completed_task(self):\n    # Return all assignees who completed their tasks\n    winners = []\n    for task in self.tasks:\n      if task.completed:\n        winners += task.assignee\n    return winners or None\n\n\nclass DefaultGame(Game):\n  \"\"\"The default NMMO game\"\"\"\n  game_mode = \"agent_training\"\n\n  def is_compatible(self):\n    return True\n\n  def _define_tasks(self):\n    return task_api.nmmo_default_task(self.config.POSSIBLE_AGENTS)\n\nclass AgentTraining(Game):\n  \"\"\"Game setting for agent training tasks\"\"\"\n  game_mode = \"agent_training\"\n\n  @property\n  def winning_score(self):\n    return 0.0\n\n  def is_compatible(self):\n    try:\n      # Check is the curriculum file exists and opens\n      with open(self.config.CURRICULUM_FILE_PATH, \"rb\") as f:\n        dill.load(f) # a list of TaskSpec\n    except:\n      return False\n    return True\n\n  def _define_tasks(self):\n    with open(self.config.CURRICULUM_FILE_PATH, \"rb\") as f:\n      # curriculum file may have been changed, so read the file when sampling\n      curriculum = dill.load(f) # a list of TaskSpec\n    cand_specs = [spec for spec in curriculum if spec.reward_to == \"agent\"]\n    assert len(cand_specs) > 0, \"No agent task is defined in the curriculum file\"\n\n    sampling_weights = [spec.sampling_weight for spec in cand_specs]\n    sampled_spec = self._np_random.choice(cand_specs, size=self.config.PLAYER_N,\n                                          p=sampling_weights/np.sum(sampling_weights))\n    return task_spec.make_task_from_spec(self.config.POSSIBLE_AGENTS, sampled_spec)\n\nclass TeamGameTemplate(Game):\n  \"\"\"A helper class with common utils for team games\"\"\"\n  assign_dead_reward = False  # Do NOT always assign -1 to dead agents\n\n  def is_compatible(self):\n    try:\n      assert self.config.TEAMS is not None, \"Team game requires TEAMS to be defined\"\n      num_agents = sum(len(v) for v in self.config.TEAMS.values())\n      assert self.config.PLAYER_N == num_agents,\\\n        \"PLAYER_N must match the number of agents in TEAMS\"\n      # Check is the curriculum file exists and opens\n      with open(self.config.CURRICULUM_FILE_PATH, \"rb\") as f:\n        dill.load(f) # a list of TaskSpec\n    except:\n      return False\n    return True\n\n  def _set_realm(self, map_dict):\n    self.realm.reset(self._np_random, map_dict, custom_spawn=True)\n    # Custom spawning\n    team_loader = team_helper.TeamLoader(self.config, self._np_random)\n    self.realm.players.spawn(team_loader)\n    self.realm.npcs.default_spawn()\n\n  def _post_setup(self):\n    self._attach_team_tag()\n\n  @property\n  def teams(self):\n    return self.config.TEAMS\n\n  def _attach_team_tag(self):\n    # setup team names\n    for team_id, members in self.teams.items():\n      if isinstance(team_id, int):\n        team_id = f\"Team{team_id:02d}\"\n      for idx, agent_id in enumerate(members):\n        self.realm.players[agent_id].name = f\"{team_id}_{agent_id}\"\n        if idx == 0:\n          self.realm.players[agent_id].name = f\"{team_id}_leader\"\n\n  def _get_cand_team_tasks(self, num_tasks, tags=None):\n    # NOTE: use different file to store different set of tasks?\n    with open(self.config.CURRICULUM_FILE_PATH, \"rb\") as f:\n      curriculum = dill.load(f) # a list of TaskSpec\n    cand_specs = [spec for spec in curriculum if spec.reward_to == \"team\"]\n    if tags:\n      cand_specs = [spec for spec in cand_specs if tags in spec.tags]\n    assert len(cand_specs) > 0, \"No team task is defined in the curriculum file\"\n\n    sampling_weights = [spec.sampling_weight for spec in cand_specs]\n    sampled_spec = self._np_random.choice(cand_specs, size=num_tasks,\n                                          p=sampling_weights/np.sum(sampling_weights))\n    return sampled_spec\n\nclass TeamTraining(TeamGameTemplate):\n  \"\"\"Game setting for team training tasks\"\"\"\n  game_mode = \"team_training\"\n\n  def _define_tasks(self):\n    sampled_spec = self._get_cand_team_tasks(len(self.config.TEAMS))\n    return task_spec.make_task_from_spec(self.config.TEAMS, sampled_spec)\n\ndef team_survival_task(num_tick, embedding=None):\n  return task_spec.TaskSpec(\n    eval_fn=base_predicates.TickGE,\n    eval_fn_kwargs={\"num_tick\": num_tick},\n    reward_to=\"team\",\n    embedding=embedding)\n\nclass TeamBattle(TeamGameTemplate):\n  \"\"\"Game setting for team battle\"\"\"\n  game_mode = \"team_battle\"\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n    self.task_embedding = utils.get_hash_embedding(base_predicates.TickGE,\n                                                   self.config.TASK_EMBED_DIM)\n\n  def is_compatible(self):\n    assert self.config.are_systems_enabled([\"COMBAT\"]), \"Combat system must be enabled\"\n    assert self.config.TEAMS is not None, \"Team battle mode requires TEAMS to be defined\"\n    num_agents = sum(len(v) for v in self.config.TEAMS.values())\n    assert self.config.PLAYER_N == num_agents,\\\n      \"PLAYER_N must match the number of agents in TEAMS\"\n    return True\n\n  def _define_tasks(self):\n    # NOTE: Teams can win by eliminating all other teams,\n    # or fully cooperating to survive for the entire episode\n    survive_task = team_survival_task(self.config.HORIZON, self.task_embedding)\n    return task_spec.make_task_from_spec(self.config.TEAMS,\n                                         [survive_task] * len(self.config.TEAMS))\n\n  def _check_winners(self, terminated):\n    # A team is won, when their task is completed first or only one team remains\n    current_teams = self._check_remaining_teams()\n    if len(current_teams) == 1:\n      winner_team = list(current_teams.keys())[0]\n      return self.config.TEAMS[winner_team]\n\n    # Return all assignees who completed their tasks\n    # Assuming the episode gets ended externally\n    return self._who_completed_task()\n\n  def _check_remaining_teams(self):\n    current_teams = {}\n    for team_id, team in self.config.TEAMS.items():\n      alive_members = [agent_id for agent_id in team if agent_id in self.realm.players]\n      if len(alive_members) > 0:\n        current_teams[team_id] = alive_members\n    return current_teams\n\nclass ProtectTheKing(TeamBattle):\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n    self.team_helper = team_helper.TeamHelper(self.config.TEAMS)\n    self.task_embedding = utils.get_hash_embedding(base_predicates.ProtectLeader,\n                                                   self.config.TASK_EMBED_DIM)\n\n  def _define_tasks(self):\n    protect_task = task_spec.TaskSpec(\n      eval_fn=base_predicates.ProtectLeader,\n      eval_fn_kwargs={\n        \"target_protect\": \"my_team_leader\",\n        \"target_destroy\": \"all_foe_leaders\",\n      },\n      reward_to=\"team\"\n    )\n    return task_spec.make_task_from_spec(self.config.TEAMS,\n                                         [protect_task] * len(self.config.TEAMS))\n\n  def update(self, terminated, dead_players, dead_npcs):\n    # If a team's leader is dead, the whole team is dead\n    for team_id, members in self.config.TEAMS.items():\n      if self.team_helper.get_target_agent(team_id, \"my_team_leader\") in dead_players:\n        for agent_id in members:\n          if agent_id in self.realm.players:\n            self.realm.players[agent_id].health.update(0)\n\n    # Addition dead players cull\n    for agent in [agent for agent in self.realm.players.values() if not agent.alive]:\n      agent_id = agent.ent_id\n      self.realm.players.dead_this_tick[agent_id] = agent\n      self.realm.players.cull_entity(agent)\n      agent.datastore_record.delete()\n      terminated[agent_id] = True\n\n    super().update(terminated, dead_players, dead_npcs)\n"
  },
  {
    "path": "nmmo/core/map.py",
    "content": "from typing import List, Tuple\nimport numpy as np\nfrom ordered_set import OrderedSet\n\nfrom nmmo.core.tile import Tile\nfrom nmmo.lib import material, utils\nfrom nmmo.core.terrain import (\n  fractal_to_material,\n  process_map_border,\n  spawn_profession_resources,\n  scatter_extra_resources,\n)\n\n\nclass Map:\n  '''Map object representing a list of tiles\n\n  Also tracks a sparse list of tile updates\n  '''\n  def __init__(self, config, realm, np_random):\n    self.config = config\n    self._repr  = None\n    self.realm  = realm\n    self.update_list = None\n    self.pathfinding_cache = {} # Avoid recalculating A*, paths don't move\n\n    sz          = config.MAP_SIZE\n    self.tiles  = np.zeros((sz,sz), dtype=object)\n    self.habitable_tiles = np.zeros((sz,sz), dtype=np.int8)\n\n    for r in range(sz):\n      for c in range(sz):\n        self.tiles[r, c] = Tile(realm, r, c, np_random)\n\n    # the map center, and the centers in each quadrant are important targets\n    self.dist_border_center = None\n    self.center_coord = None\n    self.quad_centers = None\n    self.seize_targets: List[Tuple] = None  # a list of (r, c) coords\n\n    # used to place border\n    self.l1 = utils.l1_map(sz)\n\n  @property\n  def packet(self):\n    '''Packet of degenerate resource states'''\n    missing_resources = []\n    for e in self.update_list:\n      missing_resources.append(e.pos)\n    return missing_resources\n\n  @property\n  def repr(self):\n    '''Flat matrix of tile material indices'''\n    if not self._repr:\n      self._repr = [[t.material.index for t in row] for row in self.tiles]\n    return self._repr\n\n  def reset(self, map_dict, np_random, seize_targets=None):\n    '''Reuse the current tile objects to load a new map'''\n    config = self.config\n    assert map_dict[\"map\"].shape == (config.MAP_SIZE,config.MAP_SIZE),\\\n      \"Map shape is inconsistent with config.MAP_SIZE\"\n\n    # NOTE: MAP_CENTER and MAP_BORDER can change from episode to episode\n    self.center_coord = (config.MAP_SIZE//2, config.MAP_SIZE//2)\n    self.dist_border_center = config.MAP_CENTER // 2\n    half_dist = self.dist_border_center // 2\n    self.quad_centers = {\n      \"first\": (self.center_coord[0] + half_dist, self.center_coord[1] + half_dist),\n      \"second\": (self.center_coord[0] - half_dist, self.center_coord[1] + half_dist),\n      \"third\": (self.center_coord[0] - half_dist, self.center_coord[1] - half_dist),\n      \"fourth\": (self.center_coord[0] + half_dist, self.center_coord[1] - half_dist),\n    }\n    assert config.MAP_BORDER > config.PLAYER_VISION_RADIUS,\\\n      \"MAP_BORDER must be greater than PLAYER_VISION_RADIUS\"\n\n    self._repr = None\n    self.update_list = OrderedSet() # critical for determinism\n    self.seize_targets = []\n    if seize_targets:\n      assert isinstance(seize_targets, list), \"seize_targets must be a list of reserved words\"\n      for target in seize_targets:\n        # pylint: disable=consider-iterating-dictionary\n        assert target in list(self.quad_centers.keys()) + [\"center\"], \"Invalid seize target\"\n        self.seize_targets.append(self.center_coord if target == \"center\"\n                                  else self.quad_centers[target])\n\n    # process map_np_array according to config\n    matl_map = self._process_map(map_dict, np_random)\n    if \"mark_center\" in map_dict and map_dict[\"mark_center\"]:\n      self._mark_tile(matl_map, *self.center_coord)\n    for r, c in self.seize_targets:\n      self._mark_tile(matl_map, r, c)\n\n    # reset tiles with new materials\n    materials = {mat.index: mat for mat in material.All}\n    for r, row in enumerate(matl_map):\n      for c, idx in enumerate(row):\n        mat = materials[idx]\n        tile = self.tiles[r, c]\n        tile.reset(mat, config, np_random)\n        self.habitable_tiles[r, c] = tile.habitable\n\n  def _process_map(self, map_dict, np_random):\n    map_np_array = map_dict[\"map\"]\n    if not self.config.TERRAIN_SYSTEM_ENABLED:\n      map_np_array[:] = material.Grass.index\n    else:\n      if self.config.MAP_RESET_FROM_FRACTAL:\n        map_tiles = fractal_to_material(self.config, map_dict[\"fractal\"],\n                                        self.config.TERRAIN_RESET_TO_GRASS)\n        # Place materials here, before converting map_tiles into an int array\n        if self.config.PROFESSION_SYSTEM_ENABLED:\n          spawn_profession_resources(self.config, map_tiles, np_random)\n        if self.config.TERRAIN_SCATTER_EXTRA_RESOURCES:\n          scatter_extra_resources(self.config, map_tiles, np_random)\n        map_np_array = map_tiles.astype(int)\n\n      # Disable materials here\n      if self.config.TERRAIN_DISABLE_STONE:\n        map_np_array[map_np_array == material.Stone.index] = material.Grass.index\n\n    # Make the edge tiles habitable, and place the void tiles outside the border\n    map_np_array = process_map_border(self.config, map_np_array, self.l1)\n    return map_np_array\n\n  @staticmethod\n  def _mark_tile(map_np_array, row, col, dist=2):\n    map_np_array[row-dist:row+dist+1,col-dist:col+dist+1] = material.Grass.index\n    map_np_array[row,col] = material.Herb.index\n\n  def step(self):\n    '''Evaluate updatable tiles'''\n    for tile in self.update_list.copy():\n      if not tile.depleted:\n        self.update_list.remove(tile)\n      tile.step()\n    if self.seize_targets:\n      for r, c in self.seize_targets:\n        self.tiles[r, c].update_seize()\n\n  def harvest(self, r, c, deplete=True):\n    '''Called by actions that harvest a resource tile'''\n    if deplete:\n      self.update_list.add(self.tiles[r, c])\n    return self.tiles[r, c].harvest(deplete)\n\n  def is_valid_pos(self, row, col):\n    '''Check if a position is valid'''\n    return 0 <= row < self.config.MAP_SIZE and 0 <= col < self.config.MAP_SIZE\n\n  def make_spawnable(self, row, col, radius=2):\n    '''Make the area centered around row, col spawnable'''\n    assert self._repr is None, \"Cannot make spawnable after map is generated\"\n    assert radius > 0, \"Radius must be positive\"\n    assert self.config.MAP_BORDER < row-radius and self.config.MAP_BORDER < col-radius \\\n           and row+radius < self.config.MAP_SIZE-self.config.MAP_BORDER \\\n           and col+radius < self.config.MAP_SIZE-self.config.MAP_BORDER,\\\n            \"Cannot make spawnable near the border\"\n    for r in range(row-radius, row+radius+1):\n      for c in range(col-radius, col+radius+1):\n        tile = self.tiles[r, c]\n        # pylint: disable=protected-access\n        tile.reset(material.Grass, self.config, self.realm._np_random)\n        self.habitable_tiles[r, c] = tile.habitable  # must be true\n\n  @property\n  def seize_status(self):\n    if self.seize_targets is None:\n      return {}\n    return {\n      (r, c): self.tiles[r, c].seize_history[-1]\n      for r, c in self.seize_targets\n      if self.tiles[r, c].seize_history\n    }\n"
  },
  {
    "path": "nmmo/core/observation.py",
    "content": "# pylint: disable=no-member,c-extension-no-member\nfrom functools import lru_cache\nimport numpy as np\n\nfrom nmmo.core.tile import TileState\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.systems.item import ItemState\nimport nmmo.systems.item as item_system\nfrom nmmo.core import action\nfrom nmmo.lib import material\nimport nmmo.lib.cython_helper as chp\n\nROW_DELTA = np.array([-1, 1, 0, 0], dtype=np.int64)\nCOL_DELTA = np.array([0, 0, 1, -1], dtype=np.int64)\nEMPTY_TILE = TileState.parse_array(\n  np.array([0, 0, material.Void.index], dtype=np.int16))\n\n\nclass BasicObs:\n  def __init__(self, id_col, obs_dim):\n    self.values = None\n    self.ids = None\n    self.id_col = id_col\n    self.obs_dim = obs_dim\n\n  def reset(self):\n    self.values = None\n    self.ids = None\n\n  def update(self, values):\n    self.values = values[:self.obs_dim]\n    self.ids = values[:, self.id_col]\n\n  @property\n  def len(self):\n    return self.ids.shape[0]\n\n  def id(self, i):\n    return self.ids[i] if i < self.len else None\n\n  def index(self, val):\n    return np.nonzero(self.ids == val)[0][0] if val in self.ids else None\n\nclass InventoryObs(BasicObs):\n  def __init__(self, id_col, obs_dim):\n    super().__init__(id_col, obs_dim)\n    self.inv_type = None\n    self.inv_level = None\n\n  def update(self, values):\n    super().update(values)\n    self.inv_type = self.values[:,ItemState.State.attr_name_to_col[\"type_id\"]]\n    self.inv_level = self.values[:,ItemState.State.attr_name_to_col[\"level\"]]\n\n  def sig(self, item: item_system.Item, level: int):\n    idx = np.nonzero((self.inv_type == item.ITEM_TYPE_ID) & (self.inv_level == level))[0]\n    return idx[0] if len(idx) else None\n\nclass GymObs:\n  keys_to_clear = [\"Tile\", \"Entity\", \"Inventory\", \"Market\", \"Communication\"]\n\n  def __init__(self, config, agent_id):\n    self.config = config\n    self.agent_id = agent_id\n    self.values = self._make_empty_obs()\n\n  def reset(self, task_embedding=None):\n    self.clear()\n    self.values[\"Task\"][:] = 0 if task_embedding is None else task_embedding\n\n  def clear(self, tick=None):\n    self.values[\"CurrentTick\"] = tick or 0\n    for key in self.keys_to_clear:\n      if key in self.values:\n        if key == \"Inventory\" and not self.config.ITEM_SYSTEM_ENABLED:\n          continue\n        if key == \"Market\" and not self.config.EXCHANGE_SYSTEM_ENABLED:\n          continue\n        if key == \"Communication\" and not self.config.COMMUNICATION_SYSTEM_ENABLED:\n          continue\n        self.values[key][:] = 0\n\n  def _make_empty_obs(self):\n    num_tile_attributes = TileState.State.num_attributes\n    num_tile_attributes += 1 if self.config.original[\"PROVIDE_DEATH_FOG_OBS\"] else 0\n    gym_obs = {\n      \"CurrentTick\": 0,\n      \"AgentId\": self.agent_id,\n      \"Task\": np.zeros(self.config.TASK_EMBED_DIM, dtype=np.float16),\n      \"Tile\": np.zeros((self.config.MAP_N_OBS, num_tile_attributes), dtype=np.int16),\n      \"Entity\": np.zeros((self.config.PLAYER_N_OBS,\n                          EntityState.State.num_attributes), dtype=np.int16)}\n    if self.config.original[\"ITEM_SYSTEM_ENABLED\"]:\n      gym_obs[\"Inventory\"] = np.zeros((self.config.INVENTORY_N_OBS,\n                                       ItemState.State.num_attributes), dtype=np.int16)\n    if self.config.original[\"EXCHANGE_SYSTEM_ENABLED\"]:\n      gym_obs[\"Market\"] = np.zeros((self.config.MARKET_N_OBS,\n                                    ItemState.State.num_attributes), dtype=np.int16)\n    if self.config.original[\"COMMUNICATION_SYSTEM_ENABLED\"]:\n      gym_obs[\"Communication\"] = np.zeros((self.config.COMMUNICATION_N_OBS,\n                                           len(EntityState.State.comm_attr_map)),\n                                           dtype=np.int16)\n    return gym_obs\n\n  def set_arr_values(self, key, values):\n    obs_shape = self.values[key].shape\n    self.values[key][:values.shape[0], :] = values[:, :obs_shape[1]]\n\n  def export(self):\n    return self.values.copy()  # shallow copy\n\nclass ActionTargets:\n  no_op_keys = [\"Direction\", \"Target\", \"InventoryItem\", \"MarketItem\"]\n  all_ones = [\"Style\", \"Price\", \"Token\"]\n\n  def __init__(self, config):\n    self.config = config\n    if not self.config.original[\"PROVIDE_ACTION_TARGETS\"]:\n      return\n\n    self._no_op = 1 if config.original[\"PROVIDE_NOOP_ACTION_TARGET\"] else 0\n    self.values = self._make_empty_targets()\n    self.keys_to_clear = None\n    self.clear(reset=True)  # to set the no-op option to 1, if needed\n\n  def _get_keys_to_clear(self):\n    keys = []\n    if self.config.COMBAT_SYSTEM_ENABLED:\n      keys.append(\"Attack\")\n    if self.config.ITEM_SYSTEM_ENABLED:\n      keys.extend([\"Use\", \"Give\", \"Destroy\"])\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      keys.extend([\"Sell\", \"Buy\", \"GiveGold\"])\n    if self.config.COMMUNICATION_SYSTEM_ENABLED:\n      keys.append(\"Comm\")\n    return keys\n\n  def reset(self):\n    if not self.config.original[\"PROVIDE_ACTION_TARGETS\"]:\n      return\n    self.keys_to_clear = self._get_keys_to_clear()\n    self.clear(reset=True)\n\n  def clear(self, reset=False):\n    if not self.config.original[\"PROVIDE_ACTION_TARGETS\"]:\n      return\n    for key, mask in self.values.items():\n      if reset is True or key in self.keys_to_clear:\n        for sub_key in mask:\n          mask[sub_key][:] = 1 if sub_key in self.all_ones else 0\n          if self._no_op > 0 and sub_key in self.no_op_keys:\n            mask[sub_key][-1] = 1  # set the no-op option to 1\n\n  def _make_empty_targets(self):\n    masks = {}\n    masks[\"Move\"] = {\"Direction\": np.zeros(len(action.Direction.edges), dtype=np.int8)}\n    if self.config.original[\"COMBAT_SYSTEM_ENABLED\"]:\n      masks[\"Attack\"] = {\n        \"Style\": np.ones(len(action.Style.edges), dtype=np.int8),\n        \"Target\": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)}\n    if self.config.original[\"ITEM_SYSTEM_ENABLED\"]:\n      masks[\"Use\"] = {\n        \"InventoryItem\": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8)}\n      masks[\"Give\"] = {\n        \"InventoryItem\": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8),\n        \"Target\": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)}\n      masks[\"Destroy\"] = {\n        \"InventoryItem\": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8)}\n    if self.config.original[\"EXCHANGE_SYSTEM_ENABLED\"]:\n      masks[\"Sell\"] = {\n        \"InventoryItem\": np.zeros(self.config.INVENTORY_N_OBS + self._no_op, dtype=np.int8),\n        \"Price\": np.ones(self.config.PRICE_N_OBS, dtype=np.int8)}\n      masks[\"Buy\"] = {\n        \"MarketItem\": np.zeros(self.config.MARKET_N_OBS + self._no_op, dtype=np.int8)}\n      masks[\"GiveGold\"] = {\n        \"Price\": np.ones(self.config.PRICE_N_OBS, dtype=np.int8),\n        \"Target\": np.zeros(self.config.PLAYER_N_OBS + self._no_op, dtype=np.int8)}\n    if self.config.original[\"COMMUNICATION_SYSTEM_ENABLED\"]:\n      masks[\"Comm\"] = {\"Token\": np.ones(self.config.COMMUNICATION_NUM_TOKENS, dtype=np.int8)}\n    return masks\n\nclass Observation:\n  def __init__(self, config, agent_id: int) -> None:\n    self.config = config\n    self.agent_id = agent_id\n    self.agent = None\n\n    self.current_tick = None\n    self._is_agent_dead = None\n    self.habitable_tiles = None\n    self.agent_in_combat = None\n    self.gym_obs = GymObs(config, agent_id)\n    self.empty_obs = GymObs(config, agent_id).export()\n    self.action_targets = ActionTargets(config)\n    if self.config.original[\"PROVIDE_ACTION_TARGETS\"]:\n      self.empty_obs[\"ActionTargets\"] = ActionTargets(config).values\n\n    self.vision_radius = self.config.PLAYER_VISION_RADIUS\n    self.vision_diameter = self.config.PLAYER_VISION_DIAMETER\n    self._noop_action = 1 if config.original[\"PROVIDE_NOOP_ACTION_TARGET\"] else 0\n    self.tiles = None\n    self.entities = BasicObs(EntityState.State.attr_name_to_col[\"id\"],\n                             config.PLAYER_N_OBS)\n    self.inventory = InventoryObs(ItemState.State.attr_name_to_col[\"id\"],\n                                  config.INVENTORY_N_OBS) \\\n      if config.original[\"ITEM_SYSTEM_ENABLED\"] else None\n    self.market = BasicObs(ItemState.State.attr_name_to_col[\"id\"],\n                           config.MARKET_N_OBS) \\\n      if config.original[\"EXCHANGE_SYSTEM_ENABLED\"] else None\n    self.comm = BasicObs(EntityState.State.attr_name_to_col[\"id\"],\n                         config.COMMUNICATION_N_OBS) \\\n      if config.original[\"COMMUNICATION_SYSTEM_ENABLED\"] else None\n\n  def reset(self, habitable_tiles, task_embedding=None):\n    self.gym_obs.reset(task_embedding)\n    self.action_targets.reset()\n    self.habitable_tiles = habitable_tiles\n    self._is_agent_dead = False\n    self.agent_in_combat = None\n\n    self.current_tick = 0\n    self.tiles = None\n    self.entities.reset()\n    if self.config.ITEM_SYSTEM_ENABLED:\n      self.inventory.reset()\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      self.market.reset()\n    if self.config.COMMUNICATION_SYSTEM_ENABLED:\n      self.comm.reset()\n    return self\n\n  @property\n  def return_dummy_obs(self):\n    return self._is_agent_dead\n\n  def set_agent_dead(self):\n    self._is_agent_dead = True\n\n  def update(self, tick, visible_tiles, visible_entities,\n             inventory=None, market=None, comm=None):\n    if self._is_agent_dead:\n      return\n\n    # cache has previous tick's data, so clear it\n    self.clear_cache()\n\n    # update the obs\n    self.current_tick = tick\n    self.tiles = visible_tiles  # assert len(visible_tiles) == self.config.MAP_N_OBS\n    self.entities.update(visible_entities)\n    if self.config.ITEM_SYSTEM_ENABLED:\n      assert inventory is not None, \"Inventory must be provided if ITEM_SYSTEM_ENABLED\"\n      self.inventory.update(inventory)\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      assert market is not None, \"Market must be provided if EXCHANGE_SYSTEM_ENABLED\"\n      self.market.update(market)\n    if self.config.COMMUNICATION_SYSTEM_ENABLED:\n      assert comm is not None, \"Comm must be provided if COMMUNICATION_SYSTEM_ENABLED\"\n      self.comm.update(comm)\n\n    # update helper vars\n    self.agent = self.entity(self.agent_id)\n    if self.config.COMBAT_SYSTEM_ENABLED:\n      latest_combat_tick = self.agent.latest_combat_tick\n      self.agent_in_combat = False if latest_combat_tick == 0 else \\\n        (tick - latest_combat_tick) < self.config.COMBAT_STATUS_DURATION\n    else:\n      self.agent_in_combat = False\n\n  @lru_cache\n  def tile(self, r_delta, c_delta):\n    '''Return the array object corresponding to a nearby tile\n\n    Args:\n        r_delta: row offset from current agent\n        c_delta: col offset from current agent\n\n    Returns:\n        Vector corresponding to the specified tile\n    '''\n    idx_1d = (self.vision_radius+r_delta)*self.vision_diameter + self.vision_radius+c_delta\n    try:\n      return TileState.parse_array(self.tiles[idx_1d])\n    except IndexError:\n      return EMPTY_TILE\n\n  @lru_cache\n  def entity(self, entity_id):\n    rows = self.entities.values[self.entities.ids == entity_id]\n    if rows.shape[0] == 0:\n      return None\n    return EntityState.parse_array(rows[0])\n\n  def clear_cache(self):\n    # clear the outdated cache\n    self.entity.cache_clear()\n    self.tile.cache_clear()\n\n  def to_gym(self):\n    '''Convert the observation to a format that can be used by OpenAI Gym'''\n    if self.return_dummy_obs:\n      return self.empty_obs\n    self.gym_obs.clear(self.current_tick)\n    # NOTE: assume that all len(self.tiles) == self.config.MAP_N_OBS\n    self.gym_obs.set_arr_values('Tile', self.tiles)\n    self.gym_obs.set_arr_values('Entity', self.entities.values)\n    if self.config.ITEM_SYSTEM_ENABLED:\n      self.gym_obs.set_arr_values('Inventory', self.inventory.values)\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      self.gym_obs.set_arr_values('Market', self.market.values)\n    if self.config.COMMUNICATION_SYSTEM_ENABLED:\n      self.gym_obs.set_arr_values('Communication', self.comm.values)\n    gym_obs = self.gym_obs.export()\n\n    if self.config.PROVIDE_ACTION_TARGETS:\n      gym_obs[\"ActionTargets\"] = self._make_action_targets()\n\n    return gym_obs\n\n  def _make_action_targets(self):\n    self.action_targets.clear()\n    masks = self.action_targets.values\n    self._make_move_mask(masks[\"Move\"])\n    if self.config.COMBAT_SYSTEM_ENABLED:\n      # Test below. see tests/core/test_observation_tile.py, test_action_target_consts()\n      # assert len(action.Style.edges) == 3\n      self._make_attack_mask(masks[\"Attack\"])\n    if self.config.ITEM_SYSTEM_ENABLED:\n      self._make_use_mask(masks[\"Use\"])\n      self._make_destroy_item_mask(masks[\"Destroy\"])\n      self._make_give_mask(masks[\"Give\"])\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      self._make_sell_mask(masks[\"Sell\"])\n      self._make_give_gold_mask(masks[\"GiveGold\"])\n      self._make_buy_mask(masks[\"Buy\"])\n    return masks\n\n  def _make_move_mask(self, move_mask, use_cython=None):\n    use_cython = use_cython or self.config.USE_CYTHON\n    if use_cython:\n      chp.make_move_mask(move_mask[\"Direction\"], self.habitable_tiles,\n                         self.agent.row, self.agent.col, ROW_DELTA, COL_DELTA)\n      return\n    move_mask[\"Direction\"][:4] = self.habitable_tiles[self.agent.row+ROW_DELTA,\n                                                      self.agent.col+COL_DELTA]\n\n  def _make_attack_mask(self, attack_mask, use_cython=None):\n    if self.config.COMBAT_ALLOW_FLEXIBLE_STYLE:\n      # NOTE: if the style is flexible, then the reach of all styles should be the same\n      assert self.config.COMBAT_MELEE_REACH == self.config.COMBAT_RANGE_REACH\n      assert self.config.COMBAT_MELEE_REACH == self.config.COMBAT_MAGE_REACH\n      assert self.config.COMBAT_RANGE_REACH == self.config.COMBAT_MAGE_REACH\n\n    if not self.config.COMBAT_SYSTEM_ENABLED or self.return_dummy_obs:\n      return\n\n    use_cython = use_cython or self.config.USE_CYTHON\n    if use_cython:\n      chp.make_attack_mask(\n        attack_mask[\"Target\"], self.entities.values, EntityState.State.attr_name_to_col,\n        {\"agent_id\": self.agent_id, \"row\": self.agent.row, \"col\": self.agent.col,\n         \"immunity\": self.config.COMBAT_SPAWN_IMMUNITY,\n         \"attack_range\": self.config.COMBAT_RANGE_REACH})\n      return\n\n    # allow friendly fire but no self shooting\n    targetable = self.entities.ids != self.agent.id\n\n    # NOTE: this is a hack. Only target \"normal\" agents, which has npc_type of 0, 1, 2, 3\n    # For example, immortal \"scout\" agents has npc_type of -1\n    targetable &= self.entities.values[:,EntityState.State.attr_name_to_col[\"npc_type\"]] >= 0\n\n    immunity = self.config.COMBAT_SPAWN_IMMUNITY\n    if self.agent.time_alive < immunity:\n      # NOTE: CANNOT attack players during immunity, thus mask should set to 0\n      targetable &= ~(self.entities.ids > 0)  # ids > 0 equals entity.is_player\n\n    within_range = np.maximum( # calculating the l-inf dist\n        np.abs(self.entities.values[:,EntityState.State.attr_name_to_col[\"row\"]] - self.agent.row),\n        np.abs(self.entities.values[:,EntityState.State.attr_name_to_col[\"col\"]] - self.agent.col)\n      ) <= self.config.COMBAT_MELEE_REACH\n\n    attack_mask[\"Target\"][:self.entities.len] = targetable & within_range\n    if np.count_nonzero(attack_mask[\"Target\"][:self.entities.len]):\n      # Mask the no-op option, since there should be at least one allowed move\n      # NOTE: this will make agents always attack if there is a valid target\n      attack_mask[\"Target\"][-1] = 0\n\n  def _make_use_mask(self, use_mask):\n    # empty inventory -- nothing to use\n    if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0)\\\n        or self.return_dummy_obs or self.agent_in_combat:\n      return\n\n    item_skill = self._item_skill()\n    not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col[\"listed_price\"]] == 0\n    item_type = self.inventory.values[:,ItemState.State.attr_name_to_col[\"type_id\"]]\n    item_level = self.inventory.values[:,ItemState.State.attr_name_to_col[\"level\"]]\n\n    # level limits are differently applied depending on item types\n    type_flt = np.tile(np.array(list(item_skill.keys())), (self.inventory.len,1))\n    level_flt = np.tile(np.array(list(item_skill.values())), (self.inventory.len,1))\n    item_type = np.tile(np.transpose(np.atleast_2d(item_type)), (1,len(item_skill)))\n    item_level = np.tile(np.transpose(np.atleast_2d(item_level)), (1,len(item_skill)))\n    level_satisfied = np.any((item_type==type_flt) & (item_level<=level_flt), axis=1)\n    use_mask[\"InventoryItem\"][:self.inventory.len] = not_listed & level_satisfied\n\n  def _item_skill(self):\n    agent = self.agent\n\n    # the minimum agent level is 1\n    level = max(1, agent.melee_level, agent.range_level, agent.mage_level,\n                agent.fishing_level, agent.herbalism_level, agent.prospecting_level,\n                agent.carving_level, agent.alchemy_level)\n    return {\n      item_system.Hat.ITEM_TYPE_ID: level,\n      item_system.Top.ITEM_TYPE_ID: level,\n      item_system.Bottom.ITEM_TYPE_ID: level,\n      item_system.Spear.ITEM_TYPE_ID: agent.melee_level,\n      item_system.Bow.ITEM_TYPE_ID: agent.range_level,\n      item_system.Wand.ITEM_TYPE_ID: agent.mage_level,\n      item_system.Rod.ITEM_TYPE_ID: agent.fishing_level,\n      item_system.Gloves.ITEM_TYPE_ID: agent.herbalism_level,\n      item_system.Pickaxe.ITEM_TYPE_ID: agent.prospecting_level,\n      item_system.Axe.ITEM_TYPE_ID: agent.carving_level,\n      item_system.Chisel.ITEM_TYPE_ID: agent.alchemy_level,\n      item_system.Whetstone.ITEM_TYPE_ID: agent.melee_level,\n      item_system.Arrow.ITEM_TYPE_ID: agent.range_level,\n      item_system.Runes.ITEM_TYPE_ID: agent.mage_level,\n      item_system.Ration.ITEM_TYPE_ID: level,\n      item_system.Potion.ITEM_TYPE_ID: level\n    }\n\n  def _make_destroy_item_mask(self, destroy_mask):\n    # empty inventory -- nothing to destroy\n    if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0)\\\n        or self.return_dummy_obs or self.agent_in_combat:\n      return\n    # not equipped items in the inventory can be destroyed\n    not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col[\"equipped\"]] == 0\n    destroy_mask[\"InventoryItem\"][:self.inventory.len] = not_equipped\n\n  def _make_give_mask(self, give_mask):\n    if not self.config.ITEM_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat\\\n       or self.inventory.len == 0:\n      return\n\n    # InventoryItem\n    not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col[\"equipped\"]] == 0\n    not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col[\"listed_price\"]] == 0\n    give_mask[\"InventoryItem\"][:self.inventory.len] = not_equipped & not_listed\n\n    # Give Target\n    # NOTE: Allow give to entities within visual range. So no distance check is needed\n    # entities_pos = self.entities.values[:,[EntityState.State.attr_name_to_col[\"row\"],\n    #                                        EntityState.State.attr_name_to_col[\"col\"]]]\n    # same_tile = utils.linf(entities_pos, (self.agent.row, self.agent.col)) == 0\n\n    not_me = self.entities.ids != self.agent_id\n    player = (self.entities.values[:,EntityState.State.attr_name_to_col[\"npc_type\"]] == 0)\n    give_mask[\"Target\"][:self.entities.len] = player & not_me\n\n  def _make_sell_mask(self, sell_mask):\n    # empty inventory -- nothing to sell\n    if not (self.config.EXCHANGE_SYSTEM_ENABLED and self.inventory.len > 0) \\\n      or self.return_dummy_obs or self.agent_in_combat:\n      return\n\n    not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col[\"equipped\"]] == 0\n    not_listed = self.inventory.values[:,ItemState.State.attr_name_to_col[\"listed_price\"]] == 0\n    sell_mask[\"InventoryItem\"][:self.inventory.len] = not_equipped & not_listed\n\n  def _make_give_gold_mask(self, give_mask):\n    if not self.config.EXCHANGE_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat\\\n       or int(self.agent.gold) <= 2:  # NOTE: this is a hack to reduce mask computation\n      return\n\n    # GiveGold Target\n    # NOTE: Allow give to entities within visual range. So no distance check is needed\n    # entities_pos = self.entities.values[:,[EntityState.State.attr_name_to_col[\"row\"],\n    #                                        EntityState.State.attr_name_to_col[\"col\"]]]\n    # same_tile = utils.linf(entities_pos, (self.agent.row, self.agent.col)) == 0\n    not_me = self.entities.ids != self.agent_id\n    player = (self.entities.values[:,EntityState.State.attr_name_to_col[\"npc_type\"]] == 0)\n    give_mask[\"Target\"][:self.entities.len] = player & not_me\n\n    # GiveGold Amount (Price)\n    gold = int(self.agent.gold)\n    give_mask[\"Price\"][gold:] = 0 # NOTE: Price masks starts with all ones\n\n  def _make_buy_mask(self, buy_mask):\n    if not self.config.EXCHANGE_SYSTEM_ENABLED or self.return_dummy_obs or self.agent_in_combat \\\n       or self.market.len == 0:\n      return\n\n    market_items = self.market.values\n    not_mine = market_items[:,ItemState.State.attr_name_to_col[\"owner_id\"]] != self.agent_id\n    # if the inventory is full, one can only buy existing ammo stack\n    #   otherwise, one can buy anything owned by other, having enough money\n    if self.inventory.len >= self.config.ITEM_INVENTORY_CAPACITY:\n      exist_ammo_listings = self._existing_ammo_listings()\n      if not np.any(exist_ammo_listings):\n        return\n      not_mine &= exist_ammo_listings\n\n    enough_gold = market_items[:,ItemState.State.attr_name_to_col[\"listed_price\"]] \\\n                    <= self.agent.gold\n    buy_mask[\"MarketItem\"][:self.market.len] = not_mine & enough_gold\n\n  def _existing_ammo_listings(self):\n    sig_col = (ItemState.State.attr_name_to_col[\"type_id\"],\n               ItemState.State.attr_name_to_col[\"level\"])\n    ammo_id = [ammo.ITEM_TYPE_ID for ammo in\n              [item_system.Whetstone, item_system.Arrow, item_system.Runes]]\n\n    # search ammo stack from the inventory\n    type_flt = np.tile(np.array(ammo_id), (self.inventory.len,1))\n    item_type = np.tile(\n      np.transpose(np.atleast_2d(self.inventory.values[:,sig_col[0]])),\n      (1, len(ammo_id)))\n    exist_ammo = self.inventory.values[np.any(item_type == type_flt, axis=1)]\n\n    # self does not have ammo\n    if exist_ammo.shape[0] == 0:\n      return np.zeros(self.market.len, dtype=bool)\n\n    # search the existing ammo stack from the market that's not mine\n    type_flt = np.tile(np.array(exist_ammo[:,sig_col[0]]), (self.market.len,1))\n    level_flt = np.tile(np.array(exist_ammo[:,sig_col[1]]), (self.market.len,1))\n    item_type = np.tile(np.transpose(np.atleast_2d(self.market.values[:,sig_col[0]])),\n                        (1, exist_ammo.shape[0]))\n    item_level = np.tile(np.transpose(np.atleast_2d(self.market.values[:,sig_col[1]])),\n                         (1, exist_ammo.shape[0]))\n    exist_ammo_listings = np.any((item_type==type_flt) & (item_level==level_flt), axis=1)\n\n    not_mine = self.market.values[:,ItemState.State.attr_name_to_col[\"owner_id\"]] != self.agent_id\n\n    return exist_ammo_listings & not_mine\n"
  },
  {
    "path": "nmmo/core/realm.py",
    "content": "from __future__ import annotations\nfrom collections import defaultdict\nfrom typing import Dict\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core.map import Map\nfrom nmmo.core.tile import TileState\nfrom nmmo.core.action import Action, Buy, Comm\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.entity.entity_manager import PlayerManager\nfrom nmmo.entity.npc_manager import NPCManager\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom nmmo.systems.exchange import Exchange\nfrom nmmo.systems.item import ItemState\nfrom nmmo.lib.event_log import EventLogger, EventState\nfrom nmmo.render.replay_helper import ReplayHelper\n\ndef prioritized(entities: Dict, merged: Dict):\n  \"\"\"Sort actions into merged according to priority\"\"\"\n  for idx, actions in entities.items():\n    for atn, args in actions.items():\n      merged[atn.priority].append((idx, (atn, args.values())))\n  return merged\n\n\nclass Realm:\n  \"\"\"Top-level world object\"\"\"\n\n  def __init__(self, config, np_random):\n    self.config = config\n    self._np_random = np_random # rng\n    assert isinstance(\n        config, nmmo.config.Config\n    ), f\"Config {config} is not a config instance (did you pass the class?)\"\n\n    Action.hook(config)\n\n    self.datastore = NumpyDatastore()\n    for s in [TileState, EntityState, ItemState, EventState]:\n      self.datastore.register_object_type(s._name, s.State.num_attributes)\n\n    self.tick = None # to use as a \"reset\" checker\n\n    # Load the world file\n    self.map = Map(config, self, self._np_random)\n    self.fog_map = np.zeros((config.MAP_SIZE, config.MAP_SIZE), dtype=np.float16)\n\n    # Event logger\n    self.event_log = EventLogger(self)\n\n    # Entity handlers\n    self.players = PlayerManager(self, self._np_random)\n    self.npcs = NPCManager(self, self._np_random)\n\n    # Global item registry\n    self.items = {}\n\n    # Global item exchange\n    self.exchange = Exchange(self)\n\n    # Replay helper\n    self._replay_helper = None\n\n    # Initialize actions\n    nmmo.Action.init(config)\n\n  def reset(self, np_random, map_dict,\n            custom_spawn=False,\n            seize_targets=None,\n            delete_dead_player=True):\n    \"\"\"Reset the sub-systems and load the provided map\"\"\"\n    self._np_random = np_random\n    self.tick = 0\n    self.update_fog_map(reset=True)\n    #self.event_log.reset()\n    self.items.clear()\n    self.exchange.reset()\n    if self._replay_helper is not None:\n      self._replay_helper.reset()\n\n    # Load the map np array into the map, tiles and reset\n    self.map.reset(map_dict, self._np_random, seize_targets)\n\n    # EntityState and ItemState tables must be empty after players/npcs.reset()\n    self.players.reset(self._np_random, delete_dead_player)\n    self.npcs.reset(self._np_random)\n    # assert EntityState.State.table(self.datastore).is_empty(), \\\n    #     \"EntityState table is not empty\"\n    # assert ItemState.State.table(self.datastore).is_empty(), \\\n    #     \"ItemState table is not empty\"\n\n    # DataStore id allocator must be reset to be deterministic\n    EntityState.State.table(self.datastore).reset()\n    ItemState.State.table(self.datastore).reset()\n\n    self.event_log.reset()  # reset this last for debugging\n\n    if custom_spawn is False:\n      # NOTE: custom spawning npcs and agents can be done outside, after reset()\n      self.npcs.default_spawn()\n      self.players.spawn()\n\n  def packet(self):\n    \"\"\"Client packet\"\"\"\n    return {\n      \"environment\": self.map.repr,\n      \"border\": self.config.MAP_BORDER,\n      \"size\": self.config.MAP_SIZE,\n      \"resource\": self.map.packet,\n      \"player\": self.players.packet,\n      \"npc\": self.npcs.packet,\n      \"market\": self.exchange.packet,\n  }\n\n  @property\n  def num_players(self):\n    \"\"\"Number of alive player agents\"\"\"\n    return len(self.players.entities)\n\n  @property\n  def seize_status(self):\n    return self.map.seize_status\n\n  def entity(self, ent_id):\n    e = self.entity_or_none(ent_id)\n    assert e is not None, f\"Entity {ent_id} does not exist\"\n    return e\n\n  def entity_or_none(self, ent_id):\n    if ent_id is None:\n      return None\n\n    \"\"\"Get entity by ID\"\"\"\n    if ent_id < 0:\n      return self.npcs.get(ent_id)\n\n    return self.players.get(ent_id)\n\n  def step(self, actions):\n    \"\"\"Run game logic for one tick\n\n    Args:\n        actions: Dict of agent actions\n\n    Returns:\n        dead: List of dead agents\n    \"\"\"\n    # Prioritize actions\n    npc_actions = self.npcs.actions()\n    merged = defaultdict(list)\n    prioritized(actions, merged)\n    prioritized(npc_actions, merged)\n\n    # Update entities and perform actions\n    self.players.update(actions)\n    self.npcs.update(npc_actions)\n\n    # Execute actions -- CHECK ME the below priority\n    #  - 10: Use - equip ammo, restore HP, etc.\n    #  - 20: Buy - exchange while sellers, items, buyers are all intact\n    #  - 30: Give, GiveGold - transfer while both are alive and at the same tile\n    #  - 40: Destroy - use with SELL/GIVE, if not gone, destroy and recover space\n    #  - 50: Attack\n    #  - 60: Move\n    #  - 70: Sell - to guarantee the listed items are available to buy\n    #  - 99: Comm\n\n    for priority in sorted(merged):\n      # TODO: we should be randomizing these, otherwise the lower ID agents\n      # will always go first. --> ONLY SHUFFLE BUY\n      if priority == Buy.priority:\n        self._np_random.shuffle(merged[priority])\n\n      # CHECK ME: do we need this line?\n      # ent_id, (atn, args) = merged[priority][0]\n      for ent_id, (atn, args) in merged[priority]:\n        ent = self.entity(ent_id)\n        if (ent.alive and not ent.status.frozen) or \\\n           (ent.is_recon and priority == Comm.priority):  # recons can always comm\n          atn.call(self, ent, *args)\n    dead_players = self.players.cull()\n    dead_npcs = self.npcs.cull()\n\n    self.tick += 1\n\n    # These require the updated tick\n    self.map.step()\n    self.update_fog_map()\n    self.exchange.step()\n    self.event_log.update()\n    if self._replay_helper is not None:\n      self._replay_helper.update()\n\n    return dead_players, dead_npcs\n\n  def update_fog_map(self, reset=False):\n    fog_start_tick = self.config.DEATH_FOG_ONSET\n    if fog_start_tick is None:\n      return\n\n    fog_speed = self.config.DEATH_FOG_SPEED\n    center = self.config.MAP_SIZE // 2\n    safe = self.config.DEATH_FOG_FINAL_SIZE\n\n    if reset:\n      dist = -self.config.MAP_BORDER\n      for i in range(center):\n        l, r = i, self.config.MAP_SIZE - i\n        # positive value represents the poison strength\n        # negative value represents the shortest distance to poison area\n        self.fog_map[l:r, l:r] = -dist\n        dist += 1\n      # mark the safe area\n      self.fog_map[center-safe:center+safe+1, center-safe:center+safe+1] = -self.config.MAP_SIZE\n      return\n\n    # consider the map border so that the fog can hit the border at fog_start_tick\n    if self.tick >= fog_start_tick:\n      self.fog_map += fog_speed\n      # mark the safe area\n      self.fog_map[center-safe:center+safe+1, center-safe:center+safe+1] = -self.config.MAP_SIZE\n\n  def record_replay(self, replay_helper: ReplayHelper) -> ReplayHelper:\n    self._replay_helper = replay_helper\n    self._replay_helper.set_realm(self)\n    return replay_helper\n"
  },
  {
    "path": "nmmo/core/terrain.py",
    "content": "import os\nimport logging\n\nimport numpy as np\nfrom imageio.v2 import imread, imsave\nfrom scipy import stats\n\nfrom nmmo.lib import material, seeding, utils, vec_noise\n\n\ndef sharp(noise):\n  '''Exponential noise sharpener for perlin ridges'''\n  return 2 * (0.5 - abs(0.5 - noise))\n\nclass Save:\n  '''Save utility for map files'''\n  @staticmethod\n  def render(mats, lookup, path):\n    '''Render tiles to png'''\n    images = [[lookup[e] for e in l] for l in mats]\n    image = np.vstack([np.hstack(e) for e in images])\n    imsave(path, image)\n\n  @staticmethod\n  def fractal(terrain, path):\n    '''Save fractal to both png and npy'''\n    imsave(os.path.join(path, 'fractal.png'), (256*terrain).astype(np.uint8))\n    np.save(os.path.join(path, 'fractal.npy'), terrain.astype(np.float16))\n\n  @staticmethod\n  def as_numpy(mats, path):\n    '''Save map to .npy'''\n    path = os.path.join(path, 'map.npy')\n    np.save(path, mats.astype(int))\n\n# pylint: disable=E1101:no-member\n# Terrain uses setattr()\nclass Terrain:\n  '''Terrain material class; populated at runtime'''\n  @staticmethod\n  def generate_terrain(config, map_id, interpolaters):\n    center      = config.MAP_CENTER\n    border      = config.MAP_BORDER\n    size        = config.MAP_SIZE\n    frequency   = config.TERRAIN_FREQUENCY\n    offset      = config.TERRAIN_FREQUENCY_OFFSET\n    octaves     = center // config.TERRAIN_TILES_PER_OCTAVE\n\n    #Compute a unique seed based on map index\n    #Flip seed used to ensure train/eval maps are different\n    seed = map_id + 1\n    if config.TERRAIN_FLIP_SEED:\n      seed = -seed\n\n    #Log interpolation factor\n    if not interpolaters:\n      interpolaters = np.logspace(config.TERRAIN_LOG_INTERPOLATE_MIN,\n              config.TERRAIN_LOG_INTERPOLATE_MAX, config.MAP_N)\n\n    interpolate = interpolaters[map_id]\n\n    #Data buffers\n    val   = np.zeros((size, size, octaves))\n    scale = np.zeros((size, size, octaves))\n    s     = np.arange(size)\n    X, Y  = np.meshgrid(s, s)\n\n    #Compute noise over logscaled octaves\n    start = frequency\n    end   = min(start, start - np.log2(center) + offset)\n    for idx, freq in enumerate(np.logspace(start, end, octaves, base=2)):\n      val[:, :, idx] = vec_noise.snoise2(seed*size + freq*X, idx*size + freq*Y)\n\n    #Compute L1 distance\n    l1     = utils.l1_map(size)\n\n    #Interpolation Weights\n    rrange = np.linspace(-1, 1, 2*octaves-1)\n    pdf    = stats.norm.pdf(rrange, 0, interpolate)\n    pdf    = pdf / max(pdf)\n    high   = center / 2\n    delta  = high / octaves\n\n    #Compute perlin mask\n    noise  = np.zeros((size, size))\n    X, Y   = np.meshgrid(s, s)\n    expand = int(np.log2(center)) - 2\n    for idx, octave in enumerate(range(expand, 1, -1)):\n      freq, mag = 1 / 2**octave, 1 / 2**idx\n      noise    += mag * vec_noise.snoise2(seed*size + freq*X, idx*size + freq*Y)\n\n    noise -= np.min(noise)\n    noise = octaves * noise / np.max(noise) - 1e-12\n    noise = noise.astype(int)\n\n    #Compute L1 and Perlin scale factor\n    for i in range(octaves):\n      start             = octaves - i - 1\n      scale[l1 <= high] = np.arange(start, start + octaves)\n      high             -= delta\n\n    start   = noise - 1\n    l1_scale = np.clip(l1, 0, size//2 - border - 2)\n    l1_scale = l1_scale / np.max(l1_scale)\n    for i in range(octaves):\n      idxs           = l1_scale*scale[:, :, i] + (1-l1_scale)*(start + i)\n      scale[:, :, i] = pdf[idxs.astype(int)]\n\n    #Blend octaves\n    std = np.std(val)\n    val = val / std\n    val = scale * val\n    val = np.sum(scale * val, -1)\n    val = std * val / np.std(val)\n    val = 0.5 + np.clip(val, -1, 1)/2\n\n    # Transform fractal noise to terrain\n    matl = fractal_to_material(config, val)\n    matl = process_map_border(config, matl, l1)\n\n    return val, matl, interpolaters\n\ndef fractal_to_material(config, fractal, all_grass=False):\n  size = config.MAP_SIZE\n  matl_map = np.zeros((size, size), dtype=np.int16)\n  for y in range(size):\n    for x in range(size):\n      if all_grass:\n        matl_map[y, x] = Terrain.GRASS\n        continue\n\n      v = fractal[y, x]\n      if v <= config.TERRAIN_WATER:\n        mat = Terrain.WATER\n      elif v <= config.TERRAIN_GRASS:\n        mat = Terrain.GRASS\n      elif v <= config.TERRAIN_FOILAGE:\n        mat = Terrain.FOILAGE\n      else:\n        mat = Terrain.STONE\n      matl_map[y, x] = mat\n  return matl_map\n\ndef process_map_border(config, matl_map, l1=None):\n  size = config.MAP_SIZE\n  border = config.MAP_BORDER\n  if l1 is None:\n    l1 = utils.l1_map(size)\n\n  # Void and grass border\n  matl_map[l1 > size/2 - border] = material.Void.index\n  matl_map[l1 == size//2 - border] = material.Grass.index\n  edge = l1 == size//2 - border - 1\n  stone = (matl_map == material.Stone.index) | (matl_map == material.Water.index)\n  matl_map[edge & stone] = material.Foilage.index\n  return matl_map\n\ndef place_fish(tiles, mmin, mmax, np_random, num_fish):\n  placed = 0\n\n  # if USE_CYTHON:\n  #   water_loc = chp.tile_where(tiles, Terrain.WATER, mmin, mmax)\n  # else:\n  water_loc = np.where(tiles == Terrain.WATER)\n  water_loc = [(r, c) for r, c in zip(water_loc[0], water_loc[1])\n              if mmin < r < mmax and mmin < c < mmax]\n  if len(water_loc) < num_fish:\n    raise RuntimeError('Not enough water tiles to place fish.')\n\n  np_random.shuffle(water_loc)\n\n  allow = {Terrain.GRASS}  # Fish should be placed adjacent to grass\n  for r, c in water_loc:\n    if tiles[r-1, c] in allow or tiles[r+1, c] in allow or \\\n       tiles[r, c-1] in allow or tiles[r, c+1] in allow:\n      tiles[r, c] = Terrain.FISH\n      placed += 1\n    if placed == num_fish:\n      break\n\n  if placed < num_fish:\n    raise RuntimeError('Could not find the water tile to place fish.')\n\ndef uniform(config, tiles, mat, mmin, mmax, np_random):\n  r = np_random.integers(mmin, mmax)\n  c = np_random.integers(mmin, mmax)\n\n  if tiles[r, c] not in {Terrain.GRASS}:\n    uniform(config, tiles, mat, mmin, mmax, np_random)\n  else:\n    tiles[r, c] = mat\n\ndef cluster(config, tiles, mat, mmin, mmax, np_random):\n  mmin = mmin + 1\n  mmax = mmax - 1\n\n  r = np_random.integers(mmin, mmax)\n  c = np_random.integers(mmin, mmax)\n\n  matls = {Terrain.GRASS}\n  if tiles[r, c] not in matls:\n    cluster(config, tiles, mat, mmin-1, mmax+1, np_random)\n    return\n\n  tiles[r, c] = mat\n  if tiles[r-1, c] in matls:\n    tiles[r-1, c] = mat\n  if tiles[r+1, c] in matls:\n    tiles[r+1, c] = mat\n  if tiles[r, c-1] in matls:\n    tiles[r, c-1] = mat\n  if tiles[r, c+1] in matls:\n    tiles[r, c+1] = mat\n\ndef spawn_profession_resources(config, tiles, np_random=None):\n  if np_random is None:\n    np_random = np.random\n\n  mmin = config.MAP_BORDER + 1\n  mmax = config.MAP_SIZE - config.MAP_BORDER - 1\n\n  for _ in range(config.PROGRESSION_SPAWN_CLUSTERS):\n    cluster(config, tiles, Terrain.ORE, mmin, mmax, np_random)\n    cluster(config, tiles, Terrain.TREE, mmin, mmax, np_random)\n    cluster(config, tiles, Terrain.CRYSTAL, mmin, mmax, np_random)\n\n  for _ in range(config.PROGRESSION_SPAWN_UNIFORMS):\n    uniform(config, tiles, Terrain.HERB, mmin, mmax, np_random)\n  place_fish(tiles, mmin, mmax, np_random,\n             config.PROGRESSION_SPAWN_UNIFORMS)\n\ndef try_add_tile(map_tiles, row, col, tile_to_add):\n  if map_tiles[row, col] == Terrain.GRASS:\n    map_tiles[row, col] = tile_to_add\n    return True\n  return False\n\ndef scatter_extra_resources(config, tiles, np_random=None,\n                            density_factor=6):\n  if np_random is None:\n    np_random = np.random\n  center = config.MAP_CENTER\n  mmin = config.MAP_BORDER + 1\n  mmax = config.MAP_SIZE - config.MAP_BORDER - 1\n\n  water_to_add, water_added = (center//density_factor)**2, 0\n  food_to_add, food_added  = (center//density_factor)**2, 0\n  while True:\n    if water_added >= water_to_add and food_added >= food_to_add:\n      break\n    r, c = tuple(np_random.integers(mmin, mmax, size=(2,)))\n    if water_added < water_to_add:\n      water_added += 1 if try_add_tile(tiles, r, c, Terrain.WATER) else 0\n    if food_added < food_to_add:\n      food_added += 1 if try_add_tile(tiles, r, c, Terrain.FOILAGE) else 0\n\n\nclass MapGenerator:\n  '''Procedural map generation'''\n  def __init__(self, config):\n    self.config = config\n    self.load_textures()\n    self.interpolaters = None\n\n  def load_textures(self):\n    '''Called during setup; loads and resizes tile pngs'''\n    lookup = {}\n    path   = self.config.PATH_TILE\n    scale  = self.config.MAP_PREVIEW_DOWNSCALE\n    for mat in material.All:\n      key = mat.tex\n      tex = imread(path.format(key))\n      lookup[mat.index] = tex[:, :, :3][::scale, ::scale]\n      setattr(Terrain, key.upper(), mat.index)\n    self.textures = lookup\n\n  def generate_all_maps(self, seed=None):\n    '''Generates MAP_N maps according to generate_map\n\n    Provides additional utilities for saving to .npy and rendering png previews'''\n\n    config = self.config\n    np_random, _ = seeding.np_random(seed)\n\n    #Only generate if maps are not cached\n    path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS)\n    os.makedirs(path_maps, exist_ok=True)\n\n    existing_maps = set(map_dir + '/map.npy' for map_dir in os.listdir(path_maps))\n    if not config.MAP_FORCE_GENERATION and existing_maps:\n      required_maps = {\n        f'map{idx}/map.npy' for idx in range(1, config.MAP_N+1)\n      }\n      missing = required_maps - existing_maps\n      if not missing:\n        return\n\n    if __debug__:\n      logging.info('Generating %s maps', str(config.MAP_N))\n\n    for idx in range(config.MAP_N):\n      path = path_maps + '/map' + str(idx+1)\n      os.makedirs(path, exist_ok=True)\n\n      terrain, tiles = self.generate_map(idx, np_random)\n\n      #Save/render\n      Save.as_numpy(tiles, path)\n      Save.fractal(terrain, path)\n      if config.MAP_GENERATE_PREVIEWS:\n        b = config.MAP_BORDER\n        tiles = [e[b:-b+1] for e in tiles][b:-b+1]\n        Save.render(tiles, self.textures, path+'/map.png')\n\n  def generate_map(self, idx, np_random=None):\n    '''Generate a single map\n\n    The default method is a relatively complex multiscale perlin noise method.\n    This is not just standard multioctave noise -- we are seeding multioctave noise\n    itself with perlin noise to create localized deviations in scale, plus additional\n    biasing to decrease terrain frequency towards the center of the map\n\n    We found that this creates more visually interesting terrain and more deviation in\n    required planning horizon across different parts of the map. This is by no means a\n    gold-standard: you are free to override this method and create customized terrain\n    generation more suitable for your application. Simply pass MAP_GENERATOR=YourMapGenClass\n    as a config argument.'''\n    config = self.config\n    if config.TERRAIN_SYSTEM_ENABLED:\n      if not hasattr(self, 'interpolaters'):\n        self.interpolaters = None\n      terrain, tiles, _ = Terrain.generate_terrain(config, idx, self.interpolaters)\n    else:\n      size    = config.MAP_SIZE\n      terrain = np.zeros((size, size))\n      tiles   = np.zeros((size, size), dtype=object)\n\n      for r in range(size):\n        for c in range(size):\n          linf = max(abs(r - size//2), abs(c - size//2))\n          if linf <= size//2 - config.MAP_BORDER:\n            tiles[r, c] = Terrain.GRASS\n          else:\n            tiles[r, c] = Terrain.VOID\n\n    if config.PROFESSION_SYSTEM_ENABLED:\n      spawn_profession_resources(config, tiles, np_random)\n\n    return terrain, tiles\n"
  },
  {
    "path": "nmmo/core/tile.py",
    "content": "from types import SimpleNamespace\n\nfrom nmmo.datastore.serialized import SerializedState\nfrom nmmo.lib import material, event_code\n\n# pylint: disable=no-member,protected-access\nTileState = SerializedState.subclass(\n  \"Tile\", [\n    \"row\",\n    \"col\",\n    \"material_id\",\n  ])\n\nTileState.Limits = lambda config: {\n  \"row\": (0, config.MAP_SIZE-1),\n  \"col\": (0, config.MAP_SIZE-1),\n  \"material_id\": (0, config.MAP_N_TILE),\n}\n\nTileState.Query = SimpleNamespace(\n  window=lambda ds, r, c, radius: ds.table(\"Tile\").window(\n    TileState.State.attr_name_to_col[\"row\"],\n    TileState.State.attr_name_to_col[\"col\"],\n    r, c, radius),\n  get_map=lambda ds, map_size:\n    ds.table(\"Tile\")._data[1:(map_size*map_size+1)]\n                    .reshape((map_size,map_size,len(TileState.State.attr_name_to_col)))\n)\n\nclass Tile(TileState):\n  def __init__(self, realm, r, c, np_random):\n    super().__init__(realm.datastore, TileState.Limits(realm.config))\n    self.realm = realm\n    self.config = realm.config\n    self._np_random = np_random\n\n    self.row.update(r)\n    self.col.update(c)\n\n    self.state = None\n    self.material = None\n    self.depleted = False\n    self.entities = {}\n    self.seize_history = []\n\n  @property\n  def occupied(self):\n    # NOTE: ONLY players consider whether the tile is occupied or not\n    # NPCs can move into occupied tiles.\n    # Surprisingly, this has huge effect on training, so be careful.\n    # Tried this -- \"sum(1 for ent_id in self.entities if ent_id > 0) > 0\"\n    return len(self.entities) > 0\n\n  @property\n  def repr(self):\n    return ((self.row.val, self.col.val))\n\n  @property\n  def pos(self):\n    return self.row.val, self.col.val\n\n  @property\n  def habitable(self):\n    return self.material in material.Habitable\n\n  @property\n  def impassible(self):\n    return self.material in material.Impassible\n\n  @property\n  def void(self):\n    return self.material == material.Void\n\n  @property\n  def tex(self):\n    return self.state.tex\n\n  def reset(self, mat, config, np_random):\n    self._np_random = np_random # reset the RNG\n    self.entities = {}\n    self.seize_history.clear()\n    self.material = mat(config)\n    self._respawn()\n\n  def set_depleted(self):\n    self.depleted = True\n    self.state = self.material.deplete\n    self.material_id.update(self.state.index)\n\n  def _respawn(self):\n    self.depleted = False\n    self.state = self.material\n    self.material_id.update(self.state.index)\n\n  def add_entity(self, ent):\n    assert ent.ent_id not in self.entities\n    self.entities[ent.ent_id] = ent\n\n  def remove_entity(self, ent_id):\n    assert ent_id in self.entities\n    self.entities.pop(ent_id)\n\n  def step(self):\n    if not self.depleted or self.material.respawn == 0:\n      return\n    if self._np_random.random() < self.material.respawn:\n      self._respawn()\n\n  def harvest(self, deplete):\n    assert not self.depleted, f'{self.state} is depleted'\n    assert self.state in material.Harvestable, f'{self.state} not harvestable'\n    if deplete:\n      self.set_depleted()\n    return self.material.harvest()\n\n  def update_seize(self):\n    if len(self.entities) != 1:  # only one entity can seize a tile\n      return\n    ent_id, entity = list(self.entities.items())[0]\n    if ent_id < 0:  # not counting npcs\n      return\n    team_members = entity.my_task.assignee  # NOTE: only one task per player\n    if self.seize_history and self.seize_history[-1][0] in team_members:\n      # no need to add another entry if the last entry is from the same team (incl. self)\n      return\n    self.seize_history.append((ent_id, self.realm.tick))\n    if self.realm.event_log:\n      self.realm.event_log.record(event_code.EventCode.SEIZE_TILE, entity, tile=self.pos)\n"
  },
  {
    "path": "nmmo/datastore/__init__.py",
    "content": ""
  },
  {
    "path": "nmmo/datastore/datastore.py",
    "content": "from __future__ import annotations\nfrom typing import Dict, List\nfrom nmmo.datastore.id_allocator import IdAllocator\n\n\"\"\"\nThis code defines a data storage system that allows for the\ncreation, manipulation, and querying of records.\n\nThe DataTable class serves as the foundation for the data\nstorage, providing methods for updating and retrieving data,\nas well as filtering and querying records.\n\nThe DatastoreRecord class represents a single record within\na table and provides a simple interface for interacting with\nthe data. The Datastore class serves as the main entry point\nfor the data storage system, allowing for the creation and\nmanagement of tables and records.\n\nThe implementation of the DataTable class is left to the\ndeveloper, but the DatastoreRecord and Datastore classes\nshould be sufficient for most use cases.\n\nSee numpy_datastore.py for an implementation.\n\"\"\"\nclass DataTable:\n  def __init__(self, num_columns: int):\n    self._num_columns = num_columns\n    self._id_allocator = IdAllocator(100)\n\n  def reset(self):\n    self._id_allocator = IdAllocator(100)\n\n  def update(self, row_id: int, col: int, value):\n    raise NotImplementedError\n\n  def get(self, ids: List[id]):\n    raise NotImplementedError\n\n  def where_in(self, col: int, values: List):\n    raise NotImplementedError\n\n  def where_eq(self, col: str, value):\n    raise NotImplementedError\n\n  def where_neq(self, col: str, value):\n    raise NotImplementedError\n\n  def window(self, row_idx: int, col_idx: int, row: int, col: int, radius: int):\n    raise NotImplementedError\n\n  def remove_row(self, row_id: int):\n    raise NotImplementedError\n\n  def add_row(self) -> int:\n    raise NotImplementedError\n\n  def is_empty(self) -> bool:\n    raise NotImplementedError\n\nclass DatastoreRecord:\n  def __init__(self, datastore, table: DataTable, row_id: int) -> None:\n    self.datastore = datastore\n    self.table = table\n    self.id = row_id\n\n  def update(self, col: int, value):\n    self.table.update(self.id, col, value)\n\n  def get(self, col: int):\n    return self.table.get(self.id)[col]\n\n  def delete(self):\n    self.table.remove_row(self.id)\n\nclass Datastore:\n  def __init__(self) -> None:\n    self._tables: Dict[str, DataTable] = {}\n\n  def register_object_type(self, object_type: str, num_colums: int):\n    if object_type not in self._tables:\n      self._tables[object_type] = self._create_table(num_colums)\n\n  def create_record(self, object_type: str) -> DatastoreRecord:\n    table = self._tables[object_type]\n    row_id = table.add_row()\n    return DatastoreRecord(self, table, row_id)\n\n  def table(self, object_type: str) -> DataTable:\n    return self._tables[object_type]\n\n  def _create_table(self, num_columns: int) -> DataTable:\n    raise NotImplementedError\n"
  },
  {
    "path": "nmmo/datastore/id_allocator.py",
    "content": "from ordered_set import OrderedSet\n\nclass IdAllocator:\n  def __init__(self, max_id):\n    # Key 0 is reserved as padding\n    self.max_id = 1\n    self.free = OrderedSet()\n    self.expand(max_id)\n\n  def full(self):\n    return len(self.free) == 0\n\n  def remove(self, row_id):\n    self.free.add(row_id)\n\n  def allocate(self):\n    return self.free.pop(0)\n\n  def expand(self, max_id):\n    self.free.update(range(self.max_id, max_id))\n    self.max_id = max_id\n"
  },
  {
    "path": "nmmo/datastore/numpy_datastore.py",
    "content": "from typing import List\n\nimport numpy as np\n\nfrom nmmo.datastore.datastore import Datastore, DataTable\n\n\nclass NumpyTable(DataTable):\n  def __init__(self, num_columns: int, initial_size: int, dtype=np.int16):\n    super().__init__(num_columns)\n    self._dtype  = dtype\n    self._initial_size = initial_size\n    self._max_rows = 0\n    self._data = np.zeros((0, self._num_columns), dtype=self._dtype)\n    self._expand(self._initial_size)\n\n  def reset(self):\n    super().reset() # resetting _id_allocator\n    self._max_rows = 0\n    self._data = np.zeros((0, self._num_columns), dtype=self._dtype)\n    self._expand(self._initial_size)\n\n  def update(self, row_id: int, col: int, value):\n    self._data[row_id, col] = value\n\n  def get(self, ids: List[int]):\n    return self._data[ids]\n\n  def where_eq(self, col: int, value):\n    return self._data[self._data[:,col] == value]\n\n  def where_neq(self, col: int, value):\n    return self._data[self._data[:,col] != value]\n\n  def where_gt(self, col: int, value):\n    return self._data[self._data[:,col] > value]\n\n  def where_in(self, col: int, values: List):\n    return self._data[np.in1d(self._data[:,col], values)]\n\n  def window(self, row_idx: int, col_idx: int, row: int, col: int, radius: int):\n    return self._data[(\n      (np.abs(self._data[:,row_idx] - row) <= radius) &\n      (np.abs(self._data[:,col_idx] - col) <= radius)\n    ).ravel()]\n\n  def add_row(self) -> int:\n    if self._id_allocator.full():\n      self._expand(self._max_rows * 2)\n    row_id = self._id_allocator.allocate()\n    return row_id\n\n  def remove_row(self, row_id: int) -> int:\n    self._id_allocator.remove(row_id)\n    self._data[row_id] = 0\n\n  def _expand(self, max_rows: int):\n    assert max_rows > self._max_rows\n    data = np.zeros((max_rows, self._num_columns), dtype=self._dtype)\n    data[:self._max_rows] = self._data\n    self._max_rows = max_rows\n    self._id_allocator.expand(max_rows)\n    self._data = data\n\n  def is_empty(self) -> bool:\n    all_data_zero = np.all(self._data == 0)\n    # 0th row is reserved as padding, so # of free ids is _max_rows-1\n    all_id_free = len(self._id_allocator.free) == self._max_rows-1\n    return all_data_zero and all_id_free\n\nclass NumpyDatastore(Datastore):\n  def _create_table(self, num_columns: int) -> DataTable:\n    return NumpyTable(num_columns, 100)\n"
  },
  {
    "path": "nmmo/datastore/serialized.py",
    "content": "# pylint: disable=bare-except,c-extension-no-member\nfrom __future__ import annotations\nfrom ast import Tuple\n\nimport math\nfrom types import SimpleNamespace\nfrom typing import Dict, List\nfrom nmmo.datastore.datastore import Datastore, DatastoreRecord\ntry:\n  import nmmo.lib.cython_helper as chp\n  USE_CYTHON = True\nexcept:\n  USE_CYTHON = False\n\n\"\"\"\nThis code defines classes for serializing and deserializing data\nin a structured way.\n\nThe SerializedAttribute class represents a single attribute of a\nrecord and provides methods for updating and querying its value,\nas well as enforcing minimum and maximum bounds on the value.\n\nThe SerializedState class serves as a base class for creating\nserialized representations of specific types of data, using a\nlist of attribute names to define the structure of the data.\nThe subclass method is a factory method for creating subclasses\nof SerializedState that are tailored to specific types of data.\n\"\"\"\n\nclass SerializedAttribute():\n  def __init__(self,\n      name: str,\n      datastore_record: DatastoreRecord,\n      column: int, min_val=-math.inf, max_val=math.inf) -> None:\n    self._name = name\n    self.datastore_record = datastore_record\n    self._column = column\n    self._min = min_val\n    self._max = max_val\n    self._val = 0\n\n  @property\n  def val(self):\n    return self._val\n\n  def update(self, value):\n    if value > self._max:\n      value = self._max\n    elif value < self._min:\n      value = self._min\n    self.datastore_record.update(self._column, value)\n    self._val = value\n\n  @property\n  def min(self):\n    return self._min\n\n  @property\n  def max(self):\n    return self._max\n\n  def increment(self, val=1, max_v=math.inf):\n    self.update(min(max_v, self.val + val))\n    return self\n\n  def decrement(self, val=1, min_v=-math.inf):\n    self.update(max(min_v, self.val - val))\n    return self\n\n  @property\n  def empty(self):\n    return self.val == 0\n\n  def __eq__(self, other):\n    return self.val == other\n\n  def __ne__(self, other):\n    return self.val != other\n\n  def __lt__(self, other):\n    return self.val < other\n\n  def __le__(self, other):\n    return self.val <= other\n\n  def __gt__(self, other):\n    return self.val > other\n\n  def __ge__(self, other):\n    return self.val >= other\n\nclass SerializedState():\n  @staticmethod\n  def subclass(name: str, attributes: List[str]):\n    class Subclass(SerializedState):\n      _name = name\n      State = SimpleNamespace(\n        attr_name_to_col = {a: i for i, a in enumerate(attributes)},\n        num_attributes = len(attributes),\n        table = lambda ds: ds.table(name)\n      )\n\n      def __init__(self, datastore: Datastore,\n                   limits: Dict[str, Tuple[float, float]] = None):\n\n        limits = limits or {}\n        self.datastore_record = datastore.create_record(name)\n\n        for attr, col in self.State.attr_name_to_col.items():\n          try:\n            setattr(self, attr,\n              SerializedAttribute(attr, self.datastore_record, col,\n                *limits.get(attr, (-math.inf, math.inf))))\n          except Exception as exc:\n            raise RuntimeError('Failed to set attribute \"' + attr + '\"') from exc\n\n      @classmethod\n      def parse_array(cls, data) -> SimpleNamespace:\n        # Takes in a data array and returns a SimpleNamespace object with\n        # attribute names as keys and corresponding values from the input\n        # data array.\n        assert len(data) == cls.State.num_attributes, \\\n          f\"Expected {cls.State.num_attributes} attributes, got {len(data)}\"\n\n        if USE_CYTHON:\n          return chp.parse_array(data, cls.State.attr_name_to_col)\n\n        return SimpleNamespace(**{\n          attr: data[col] for attr, col in cls.State.attr_name_to_col.items()\n        })\n\n    return Subclass\n"
  },
  {
    "path": "nmmo/entity/__init__.py",
    "content": "from nmmo.entity.entity import Entity\nfrom nmmo.entity.player import Player\n"
  },
  {
    "path": "nmmo/entity/entity.py",
    "content": "import math\nfrom types import SimpleNamespace\nimport numpy as np\n\nfrom nmmo.datastore.serialized import SerializedState\nfrom nmmo.systems import inventory\nfrom nmmo.lib.event_code import EventCode\n\n# pylint: disable=no-member\nEntityState = SerializedState.subclass(\n  \"Entity\", [\n    \"id\",\n    \"npc_type\", # 1 - passive, 2 - neutral, 3 - aggressive\n    \"row\",\n    \"col\",\n\n    # Status\n    \"damage\",\n    \"time_alive\",\n    \"freeze\",\n    \"item_level\",\n    \"attacker_id\",\n    \"latest_combat_tick\",\n    \"message\",\n\n    # Resources\n    \"gold\",\n    \"health\",\n    \"food\",\n    \"water\",\n\n    # Combat Skills\n    \"melee_level\",\n    \"melee_exp\",\n    \"range_level\",\n    \"range_exp\",\n    \"mage_level\",\n    \"mage_exp\",\n\n    # Harvest Skills\n    \"fishing_level\",\n    \"fishing_exp\",\n    \"herbalism_level\",\n    \"herbalism_exp\",\n    \"prospecting_level\",\n    \"prospecting_exp\",\n    \"carving_level\",\n    \"carving_exp\",\n    \"alchemy_level\",\n    \"alchemy_exp\",\n  ])\n\nEntityState.Limits = lambda config: {\n  **{\n    \"id\": (-math.inf, math.inf),\n    \"npc_type\": (-1, 3),  # -1 for immortal\n    \"row\": (0, config.MAP_SIZE-1),\n    \"col\": (0, config.MAP_SIZE-1),\n    \"damage\": (0, math.inf),\n    \"time_alive\": (0, math.inf),\n    \"freeze\": (0, math.inf),\n    \"item_level\": (0, math.inf),\n    \"attacker_id\": (-np.inf, math.inf),\n    \"latest_combat_tick\": (0, math.inf),\n    \"health\": (0, config.PLAYER_BASE_HEALTH),\n  },\n  **({\n    \"message\": (0, config.COMMUNICATION_NUM_TOKENS),\n  } if config.COMMUNICATION_SYSTEM_ENABLED else {}),\n  **({\n    \"gold\": (0, math.inf),\n    \"food\": (0, config.RESOURCE_BASE),\n    \"water\": (0, config.RESOURCE_BASE),\n  } if config.RESOURCE_SYSTEM_ENABLED else {}),\n  **({\n    \"melee_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"melee_exp\": (0, math.inf),\n    \"range_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"range_exp\": (0, math.inf),\n    \"mage_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"mage_exp\": (0, math.inf),\n    \"fishing_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"fishing_exp\": (0, math.inf),\n    \"herbalism_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"herbalism_exp\": (0, math.inf),\n    \"prospecting_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"prospecting_exp\": (0, math.inf),\n    \"carving_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"carving_exp\": (0, math.inf),\n    \"alchemy_level\": (0, config.PROGRESSION_LEVEL_MAX),\n    \"alchemy_exp\": (0, math.inf),\n  } if config.PROGRESSION_SYSTEM_ENABLED else {}),\n}\n\nEntityState.State.comm_attr_map = {name: EntityState.State.attr_name_to_col[name]\n                                   for name in [\"id\", \"row\", \"col\", \"message\"]}\nCommAttr = np.array(list(EntityState.State.comm_attr_map.values()), dtype=np.int64)\n\nEntityState.Query = SimpleNamespace(\n  # Whole table\n  table=lambda ds: ds.table(\"Entity\").where_neq(\n    EntityState.State.attr_name_to_col[\"id\"], 0),\n\n  # Single entity\n  by_id=lambda ds, id: ds.table(\"Entity\").where_eq(\n    EntityState.State.attr_name_to_col[\"id\"], id)[0],\n\n  # Multiple entities\n  by_ids=lambda ds, ids: ds.table(\"Entity\").where_in(\n    EntityState.State.attr_name_to_col[\"id\"], ids),\n\n  # Entities in a radius\n  window=lambda ds, r, c, radius: ds.table(\"Entity\").window(\n    EntityState.State.attr_name_to_col[\"row\"],\n    EntityState.State.attr_name_to_col[\"col\"],\n    r, c, radius),\n\n  # Communication obs\n  comm_obs=lambda ds: ds.table(\"Entity\").where_gt(\n    EntityState.State.attr_name_to_col[\"id\"], 0)[:, CommAttr]\n)\n\n\nclass Resources:\n  def __init__(self, ent, config):\n    self.config = config\n    self.health = ent.health\n    self.water = ent.water\n    self.food = ent.food\n    self.health_restore = 0\n    self.resilient = False\n\n    self.health.update(config.PLAYER_BASE_HEALTH)\n    if config.RESOURCE_SYSTEM_ENABLED:\n      self.water.update(config.RESOURCE_BASE)\n      self.food.update(config.RESOURCE_BASE)\n\n  def update(self, immortal=False):\n    if not self.config.RESOURCE_SYSTEM_ENABLED or immortal:\n      return\n\n    regen = self.config.RESOURCE_HEALTH_RESTORE_FRACTION\n    thresh = self.config.RESOURCE_HEALTH_REGEN_THRESHOLD\n\n    food_thresh = self.food > thresh * self.config.RESOURCE_BASE\n    water_thresh = self.water > thresh * self.config.RESOURCE_BASE\n\n    org_health = self.health.val\n    if food_thresh and water_thresh:\n      restore = np.floor(self.health.max * regen)\n      self.health.increment(restore)\n\n    if self.food.empty:\n      starvation_damage = self.config.RESOURCE_STARVATION_RATE\n      if self.resilient:\n        starvation_damage *= self.config.RESOURCE_DAMAGE_REDUCTION\n      self.health.decrement(int(starvation_damage))\n\n    if self.water.empty:\n      dehydration_damage = self.config.RESOURCE_DEHYDRATION_RATE\n      if self.resilient:\n        dehydration_damage *= self.config.RESOURCE_DAMAGE_REDUCTION\n      self.health.decrement(int(dehydration_damage))\n\n    # records both increase and decrease in health due to food and water\n    self.health_restore = self.health.val - org_health\n\n  def packet(self):\n    data = {}\n    data['health'] = { 'val': self.health.val, 'max': self.config.PLAYER_BASE_HEALTH }\n    data['food'] = data['water'] = { 'val': 0, 'max': 0 }\n    if self.config.RESOURCE_SYSTEM_ENABLED:\n      data['food'] = { 'val': self.food.val, 'max': self.config.RESOURCE_BASE }\n      data['water'] = { 'val': self.water.val, 'max': self.config.RESOURCE_BASE }\n    return data\n\n\nclass Status:\n  def __init__(self, ent):\n    self.freeze = ent.freeze\n\n  def update(self):\n    if self.frozen:\n      self.freeze.decrement(1)\n\n  def packet(self):\n    data = {}\n    data['freeze'] = self.freeze.val\n    return data\n\n  @property\n  def frozen(self):\n    return self.freeze.val > 0\n\n\n# NOTE: History.packet() is actively used in visulazing attacks\nclass History:\n  def __init__(self, ent):\n    self.actions = {}\n    self.attack = None\n\n    self.starting_position = ent.pos\n    self.exploration = 0\n    self.player_kills = 0\n\n    self.damage_received = 0\n    self.damage_inflicted = 0\n\n    self.damage = ent.damage\n    self.time_alive = ent.time_alive\n\n    self.last_pos = None\n\n  def update(self, entity, actions):\n    self.attack = None\n    self.damage.update(0)\n\n    self.actions = {}\n    if entity.ent_id in actions:\n      self.actions = actions[entity.ent_id]\n\n    self.time_alive.increment()\n\n  def packet(self):\n    data = {}\n    data['damage'] = self.damage.val\n    data['timeAlive'] = self.time_alive.val\n    data['damage_inflicted'] = self.damage_inflicted\n    data['damage_received'] = self.damage_received\n    if self.attack is not None:\n      data['attack'] = self.attack\n\n    # NOTE: the client seems to use actions for visualization\n    #   but produces errors with the new actions. So we comment out these for now\n    # actions = {}\n    # for atn, args in self.actions.items():\n    #   atn_packet = {}\n\n    #   # Avoid recursive player packet\n    #   if atn.__name__ == 'Attack':\n    #     continue\n\n    #   for key, val in args.items():\n    #     if hasattr(val, 'packet'):\n    #       atn_packet[key.__name__] = val.packet\n    #     else:\n    #       atn_packet[key.__name__] = val.__name__\n    #   actions[atn.__name__] = atn_packet\n    # data['actions'] = actions\n    data['actions'] = {}\n\n    return data\n\n# pylint: disable=no-member\nclass Entity(EntityState):\n  def __init__(self, realm, pos, entity_id, name):\n    super().__init__(realm.datastore, EntityState.Limits(realm.config))\n\n    self.realm = realm\n    self.config = realm.config\n    # TODO: do not access realm._np_random directly\n    #   related to the whole NPC, scripted logic\n    # pylint: disable=protected-access\n    self._np_random = realm._np_random\n    self.policy = name\n    self.repr = None\n    self.name = name + str(entity_id)\n\n    self._pos = None\n    self.set_pos(*pos)\n    self.ent_id = entity_id\n    self.id.update(entity_id)\n\n    self.vision = self.config.PLAYER_VISION_RADIUS\n\n    self.attacker = None\n    self.target = None\n    self.closest = None\n    self.spawn_pos = pos\n    self._immortal = False  # used for testing/player recon\n    self._recon = False\n\n    # Submodules\n    self.status = Status(self)\n    self.history = History(self)\n    self.resources = Resources(self, self.config)\n    self.inventory = inventory.Inventory(realm, self)\n\n  # @property\n  # def ent_id(self):\n  #   return self.id.val\n\n  def packet(self):\n    data = {}\n    data['status'] = self.status.packet()\n    data['history'] = self.history.packet()\n    data['inventory'] = self.inventory.packet()\n    data['alive'] = self.alive\n    data['base'] = {\n      'r': self.pos[0],\n      'c': self.pos[1],\n      'name': self.name,\n      'level': self.attack_level,\n      'item_level': self.item_level.val,}\n    return data\n\n  def update(self, realm, actions):\n    '''Update occurs after actions, e.g. does not include history'''\n    self._pos = None\n\n    if self.history.damage == 0:\n      self.attacker = None\n      self.attacker_id.update(0)\n\n    if realm.config.EQUIPMENT_SYSTEM_ENABLED:\n      self.item_level.update(self.equipment.total(lambda e: e.level))\n\n    self.status.update()\n    self.history.update(self, actions)\n\n  # Returns True if the entity is alive\n  def receive_damage(self, source, dmg):\n    self.history.damage_received += dmg\n    self.history.damage.update(dmg)\n    self.resources.health.decrement(dmg)\n\n    if self.alive:\n      return True\n\n    # at this point, self is dead\n    if source:\n      source.history.player_kills += 1\n      self.realm.event_log.record(EventCode.PLAYER_KILL, source, target=self)\n\n    # if self is dead, unlist its items from the market regardless of looting\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      for item in list(self.inventory.items):\n        self.realm.exchange.unlist_item(item)\n\n    # if self is dead but no one can loot, destroy its items\n    if source is None or not source.is_player: # nobody or npcs cannot loot\n      if self.config.ITEM_SYSTEM_ENABLED:\n        for item in list(self.inventory.items):\n          item.destroy()\n      return False\n\n    # now, source can loot the dead self\n    return False\n\n  # pylint: disable=unused-argument\n  def apply_damage(self, dmg, style):\n    self.history.damage_inflicted += dmg\n\n  @property\n  def pos(self):\n    if self._pos is None:\n      self._pos = (self.row.val, self.col.val)\n    return self._pos\n\n  def set_pos(self, row, col):\n    self._pos = (row, col)\n    self.row.update(row)\n    self.col.update(col)\n\n  @property\n  def alive(self):\n    return self.resources.health.val > 0\n\n  @property\n  def immortal(self):\n    return self._immortal\n\n  @property\n  def is_player(self) -> bool:\n    return False\n\n  @property\n  def is_npc(self) -> bool:\n    return False\n\n  @property\n  def is_recon(self):\n    return self._recon\n\n  @property\n  def attack_level(self) -> int:\n    melee = self.skills.melee.level.val\n    ranged = self.skills.range.level.val\n    mage = self.skills.mage.level.val\n    return int(max(melee, ranged, mage))\n\n  @property\n  def in_combat(self) -> bool:\n    # NOTE: the initial latest_combat_tick is 0, and valid values are greater than 0\n    if not self.config.COMBAT_SYSTEM_ENABLED or self.latest_combat_tick.val == 0:\n      return False\n    return (self.realm.tick - self.latest_combat_tick.val) < self.config.COMBAT_STATUS_DURATION\n"
  },
  {
    "path": "nmmo/entity/entity_manager.py",
    "content": "from collections.abc import Mapping\nfrom typing import Dict\n\nfrom nmmo.entity.entity import Entity, EntityState\nfrom nmmo.entity.player import Player\nfrom nmmo.lib import spawn, event_code\n\n\nclass EntityGroup(Mapping):\n  def __init__(self, realm, np_random):\n    self.datastore = realm.datastore\n    self.realm = realm\n    self.config = realm.config\n    self._np_random = np_random\n    self._entity_table = EntityState.Query.table(self.datastore)\n\n    self.entities: Dict[int, Entity] = {}\n    self.dead_this_tick: Dict[int, Entity] = {}\n    self._delete_dead_entity = True  # is default\n\n  def __len__(self):\n    return len(self.entities)\n\n  def __contains__(self, e):\n    return e in self.entities\n\n  def __getitem__(self, key) -> Entity:\n    return self.entities[key]\n\n  def __iter__(self) -> Entity:\n    yield from self.entities\n\n  def items(self):\n    return self.entities.items()\n\n  @property\n  def corporeal(self):\n    return {**self.entities, **self.dead_this_tick}\n\n  @property\n  def packet(self):\n    return {k: v.packet() for k, v in self.corporeal.items()}\n\n  def reset(self, np_random, delete_dead_entity=True):\n    self._np_random = np_random # reset the RNG\n    self._delete_dead_entity = delete_dead_entity\n    for ent in self.entities.values():\n      # destroy the items\n      if self.config.ITEM_SYSTEM_ENABLED:\n        for item in list(ent.inventory.items):\n          item.destroy()\n      ent.datastore_record.delete()\n\n    self.entities.clear()\n    self.dead_this_tick.clear()\n\n  def spawn_entity(self, entity):\n    pos, ent_id = entity.pos, entity.id.val\n    self.realm.map.tiles[pos].add_entity(entity)\n    self.entities[ent_id] = entity\n\n  def cull_entity(self, entity):\n    pos, ent_id = entity.pos, entity.id.val\n    self.realm.map.tiles[pos].remove_entity(ent_id)\n    self.entities.pop(ent_id)\n    # destroy the remaining items (of starved/dehydrated players)\n    #    of the agents who don't go through receive_damage()\n    if self.config.ITEM_SYSTEM_ENABLED:\n      for item in list(entity.inventory.items):\n        item.destroy()\n    if ent_id > 0:\n      self.realm.event_log.record(event_code.EventCode.AGENT_CULLED, entity)\n\n  def cull(self):\n    self.dead_this_tick.clear()\n    for ent in [ent for ent in self.entities.values() if not ent.alive]:\n      self.dead_this_tick[ent.ent_id] = ent\n      self.cull_entity(ent)\n      if self._delete_dead_entity:\n        ent.datastore_record.delete()\n    return self.dead_this_tick\n\n  def update(self, actions):\n    # # batch updates\n    # # time_alive, damage are from entity.py, History.update()\n    # ent_idx = self._entity_table[:, EntityState.State.attr_name_to_col[\"id\"]] != 0\n    # self._entity_table[ent_idx, EntityState.State.attr_name_to_col[\"time_alive\"]] += 1\n    # self._entity_table[ent_idx, EntityState.State.attr_name_to_col[\"damage\"]] = 0\n    # # freeze from entity.py, Status.update()\n    # freeze_idx = self._entity_table[:, EntityState.State.attr_name_to_col[\"freeze\"]] > 0\n    # self._entity_table[freeze_idx, EntityState.State.attr_name_to_col[\"freeze\"]] -= 1\n\n    for entity in self.entities.values():\n      entity.update(self.realm, actions)\n\nclass PlayerManager(EntityGroup):\n  def spawn(self, agent_loader: spawn.SequentialLoader = None):\n    if agent_loader is None:\n      agent_loader = self.config.PLAYER_LOADER(self.config, self._np_random)\n\n    # Check and assign the reslient flag\n    resilient_flag = [False] * self.config.PLAYER_N\n    if self.config.RESOURCE_SYSTEM_ENABLED:\n      num_resilient = round(self.config.RESOURCE_RESILIENT_POPULATION * self.config.PLAYER_N)\n      for idx in range(num_resilient):\n        resilient_flag[idx] = self.config.RESOURCE_DAMAGE_REDUCTION > 0\n      self._np_random.shuffle(resilient_flag)\n\n    # Spawn the players\n    for agent_id in self.config.POSSIBLE_AGENTS:\n      r, c = agent_loader.get_spawn_position(agent_id)\n\n      if agent_id in self.entities:\n        continue\n\n      # NOTE: put spawn_individual() here. Is a separate function necessary?\n      agent = next(agent_loader)  # get agent cls from config.PLAYERS\n      agent = agent(self.config, agent_id)\n      player = Player(self.realm, (r, c), agent, resilient_flag[agent_id-1])\n      super().spawn_entity(player)\n"
  },
  {
    "path": "nmmo/entity/npc.py",
    "content": "import numpy as np\nfrom nmmo.entity import entity\nfrom nmmo.core import action as Action\nfrom nmmo.systems import combat, droptable\nfrom nmmo.systems import item as Item\nfrom nmmo.systems import skill\nfrom nmmo.systems.inventory import EquipmentSlot\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.lib import utils, astar\n\n\nDIRECTIONS = [ # row delta, col delta, action\n      (-1, 0, Action.North),\n      (1, 0, Action.South),\n      (0, -1, Action.West),\n      (0, 1, Action.East)] * 2\nDELTA_TO_DIR = {(r, c): atn for r, c, atn in DIRECTIONS}\nDELTA_TO_DIR[(0, 0)] = None\n\ndef get_habitable_dir(ent):\n  r, c = ent.pos\n  is_habitable = ent.realm.map.habitable_tiles\n  start = ent._np_random.get_direction()  # pylint: disable=protected-access\n  for i in range(4):\n    delta_r, delta_c, direction = DIRECTIONS[start + i]\n    if is_habitable[r + delta_r, c + delta_c]:\n      return direction\n  return Action.North\n\ndef meander_toward(ent, goal, dist_crit=10, toward_weight=3):\n  r, c = ent.pos\n  delta_r, delta_c = goal[0] - r, goal[1] - c\n  abs_dr, abs_dc = abs(delta_r), abs(delta_c)\n  dist_l1 = abs_dr + abs_dc\n  # If close (less than dist_crit), use expensive aStar\n  if dist_l1 <= dist_crit:\n    delta = astar.aStar(ent.realm.map, ent.pos, goal)\n    return move_action(DELTA_TO_DIR[delta] if delta in DELTA_TO_DIR else None)\n\n  # Otherwise, use a weighted random walk\n  cand_dirs = []\n  weights = []\n  for i in range(4):\n    r_offset, c_offset, direction = DIRECTIONS[i]\n    if ent.realm.map.habitable_tiles[r + r_offset, c + c_offset]:\n      cand_dirs.append(direction)\n      weights.append(1)\n      if r_offset * delta_r > 0:\n        weights[-1] += toward_weight * abs_dr/dist_l1\n      if c_offset * delta_c > 0:\n        weights[-1] += toward_weight * abs_dc/dist_l1\n  if len(cand_dirs) == 0:\n    return move_action(Action.North)\n  if len(cand_dirs) == 1:\n    return move_action(cand_dirs[0])\n  weights = np.array(weights)\n  # pylint: disable=protected-access\n  return move_action(ent._np_random.choice(cand_dirs, p=weights/np.sum(weights)))\n\ndef move_action(direction):\n  return {Action.Move: {Action.Direction: direction}} if direction else {}\n\n\nclass Equipment:\n  def __init__(self, total,\n    melee_attack, range_attack, mage_attack,\n    melee_defense, range_defense, mage_defense):\n\n    self.level         = total\n    self.ammunition    = EquipmentSlot()\n\n    self.melee_attack  = melee_attack\n    self.range_attack  = range_attack\n    self.mage_attack   = mage_attack\n    self.melee_defense = melee_defense\n    self.range_defense = range_defense\n    self.mage_defense  = mage_defense\n\n  def total(self, getter):\n    return getter(self)\n\n  # pylint: disable=R0801\n  # Similar lines here and in inventory.py\n  @property\n  def packet(self):\n    packet = {}\n    packet[\"item_level\"]    = self.total\n    packet[\"melee_attack\"]  = self.melee_attack\n    packet[\"range_attack\"]  = self.range_attack\n    packet[\"mage_attack\"]   = self.mage_attack\n    packet[\"melee_defense\"] = self.melee_defense\n    packet[\"range_defense\"] = self.range_defense\n    packet[\"mage_defense\"]  = self.mage_defense\n    return packet\n\n\n# pylint: disable=no-member\nclass NPC(entity.Entity):\n  def __init__(self, realm, pos, iden, name, npc_type):\n    super().__init__(realm, pos, iden, name)\n    self.skills = skill.Combat(realm, self)\n    self.realm = realm\n    self.last_action = None\n    self.droptable = None\n    self.spawn_danger = None\n    self.equipment = None\n    self.npc_type.update(npc_type)\n\n  @property\n  def is_npc(self) -> bool:\n    return True\n\n  def update(self, realm, actions):\n    super().update(realm, actions)\n\n    if not self.alive:\n      return\n\n    self.resources.health.increment(1)\n    self.last_action = actions\n\n  def can_see(self, target):\n    if target is None or target.immortal:\n      return False\n    distance = utils.linf_single(self.pos, target.pos)\n    return distance <= self.vision\n\n  def _move_toward(self, goal):\n    delta = astar.aStar(self.realm.map, self.pos, goal)\n    return move_action(DELTA_TO_DIR[delta] if delta in DELTA_TO_DIR else None)\n\n  def _meander(self):\n    return move_action(get_habitable_dir(self))\n\n  def can_attack(self, target):\n    if target is None or not self.config.NPC_SYSTEM_ENABLED or target.immortal:\n      return False\n    if not self.config.NPC_ALLOW_ATTACK_OTHER_NPCS and target.is_npc:\n      return False\n    distance = utils.linf_single(self.pos, target.pos)\n    return distance <= self.skills.style.attack_range(self.realm.config)\n\n  def _has_target(self, search=False):\n    if self.target and (not self.target.alive or not self.can_see(self.target)):\n      self.target = None\n    # NOTE: when attacked by several agents, this will always target the last attacker\n    if self.attacker and self.target is None:\n      self.target = self.attacker\n    if self.target is None and search is True:\n      self.target = utils.identify_closest_target(self)\n    return self.target\n\n  def _add_attack_action(self, actions, target):\n    actions.update({Action.Attack: {Action.Style: self.skills.style, Action.Target: target}})\n\n  def _charge_toward(self, target):\n    actions = self._move_toward(target.pos)\n    if self.can_attack(target):\n      self._add_attack_action(actions, target)\n    return actions\n\n  # Returns True if the entity is alive\n  def receive_damage(self, source, dmg):\n    if super().receive_damage(source, dmg):\n      return True\n\n    # run the next lines if the npc is killed\n    # source receive gold & items in the droptable\n    # pylint: disable=no-member\n    if self.gold.val > 0:\n      source.gold.increment(self.gold.val)\n      self.realm.event_log.record(EventCode.LOOT_GOLD, source, amount=self.gold.val, target=self)\n      self.gold.update(0)\n\n    if self.droptable:\n      for item in self.droptable.roll(self.realm, self.attack_level):\n        if source.is_player and source.inventory.space:\n          # inventory.receive() returns True if the item is received\n          # if source does not have space, inventory.receive() destroys the item\n          if source.inventory.receive(item):\n            self.realm.event_log.record(EventCode.LOOT_ITEM, source, item=item, target=self)\n        else:\n          item.destroy()\n\n    return False\n\n  @staticmethod\n  def default_spawn(realm, pos, iden, np_random, danger=None):\n    config = realm.config\n\n    # check the position\n    if realm.map.tiles[pos].impassible:\n      return None\n\n    # Select AI Policy\n    danger = danger or combat.danger(config, pos)\n    if danger >= config.NPC_SPAWN_AGGRESSIVE:\n      ent = Aggressive(realm, pos, iden)\n    elif danger >= config.NPC_SPAWN_NEUTRAL:\n      ent = PassiveAggressive(realm, pos, iden)\n    elif danger >= config.NPC_SPAWN_PASSIVE:\n      ent = Passive(realm, pos, iden)\n    else:\n      return None\n\n    ent.spawn_danger = danger\n\n    # Select combat focus\n    style = np_random.integers(0,3)\n    if style == 0:\n      style = Action.Melee\n    elif style == 1:\n      style = Action.Range\n    else:\n      style = Action.Mage\n    ent.skills.style = style\n\n    # Compute level\n    level = 0\n    if config.PROGRESSION_SYSTEM_ENABLED:\n      level_min = config.NPC_LEVEL_MIN\n      level_max = config.NPC_LEVEL_MAX\n      level     = int(danger * (level_max - level_min) + level_min)\n\n      # Set skill levels\n      if style == Action.Melee:\n        ent.skills.melee.set_experience_by_level(level)\n      elif style == Action.Range:\n        ent.skills.range.set_experience_by_level(level)\n      elif style == Action.Mage:\n        ent.skills.mage.set_experience_by_level(level)\n\n    # Gold\n    if config.EXCHANGE_SYSTEM_ENABLED:\n      # pylint: disable=no-member\n      ent.gold.update(level)\n\n    ent.droptable = droptable.Standard()\n\n    # Equipment to instantiate\n    if config.EQUIPMENT_SYSTEM_ENABLED:\n      lvl     = level - np_random.random()\n      ilvl    = int(5 * lvl)\n\n      level_damage = config.NPC_LEVEL_DAMAGE * config.NPC_LEVEL_MULTIPLIER\n      level_defense = config.NPC_LEVEL_DEFENSE * config.NPC_LEVEL_MULTIPLIER\n\n      offense = int(config.NPC_BASE_DAMAGE + lvl * level_damage)\n      defense = int(config.NPC_BASE_DEFENSE + lvl * level_defense)\n\n      ent.equipment = Equipment(ilvl, offense, offense, offense, defense, defense, defense)\n\n      armor =  [Item.Hat, Item.Top, Item.Bottom]\n      ent.droptable.add(np_random.choice(armor))\n\n    if config.PROFESSION_SYSTEM_ENABLED:\n      tools =  [Item.Rod, Item.Gloves, Item.Pickaxe, Item.Axe, Item.Chisel]\n      ent.droptable.add(np_random.choice(tools))\n\n    return ent\n\n  def packet(self):\n    data = super().packet()\n    data[\"skills\"]   = self.skills.packet()\n    data[\"resource\"] = { \"health\": {\n      \"val\": self.resources.health.val, \"max\": self.config.PLAYER_BASE_HEALTH } }\n    return data\n\nclass Passive(NPC):\n  def __init__(self, realm, pos, iden, name=None):\n    super().__init__(realm, pos, iden, name or \"Passive\", 1)\n\n  def decide(self):\n    # Move only, no attack\n    return self._meander()\n\nclass PassiveAggressive(NPC):\n  def __init__(self, realm, pos, iden, name=None):\n    super().__init__(realm, pos, iden, name or \"Neutral\", 2)\n\n  def decide(self):\n    if self._has_target() is None:\n      return self._meander()\n    return self._charge_toward(self.target)\n\nclass Aggressive(NPC):\n  def __init__(self, realm, pos, iden, name=None):\n    super().__init__(realm, pos, iden, name or \"Hostile\", 3)\n\n  def decide(self):\n    if self._has_target(search=True) is None:\n      return self._meander()\n    return self._charge_toward(self.target)\n\nclass Soldier(NPC):\n  def __init__(self, realm, pos, iden, name, order):\n    super().__init__(realm, pos, iden, name or \"Soldier\", 3)  # Hostile with order\n    self.target_entity = None\n    self.rally_point = None\n    self._process_order(order)\n\n  def _process_order(self, order):\n    if order is None:\n      return\n    if \"destroy\" in order:  # destroy the specified entity id\n      self.target_entity = self.realm.entity(order[\"destroy\"])\n    if \"rally\" in order:\n      # rally until spotting an enemy\n      self.rally_point = order[\"rally\"]  # (row, col)\n\n  def _is_order_done(self, radius=5):\n    if self.target_entity and not self.target_entity.alive:\n      self.target_entity = None\n    if self.rally_point and utils.linf_single(self.pos, self.rally_point) <= radius:\n      self.rally_point = None\n\n  def decide(self):\n    self._is_order_done()\n    # NOTE: destroying the target entity is the highest priority\n    if self.target_entity is None and self._has_target(search=True):\n      if self.can_attack(self.target):\n        return self._charge_toward(self.target)\n\n    actions = self._decide_move_action()\n    self._decide_attack_action(actions)\n    return actions\n\n  def _decide_move_action(self):\n    # in the order of priority\n    if self.target_entity:\n      return self._move_toward(self.target_entity.pos)\n    if self.target:\n      # If it\"s close enough, it will use A*. Otherwise, random.\n      return meander_toward(self, self.target.pos)\n    if self.rally_point:\n      return meander_toward(self, self.rally_point)\n    return self._meander()\n\n  def _decide_attack_action(self, actions):\n    # The default is to attack the target entity, if within range\n    if self.target_entity and self.can_attack(self.target_entity):\n      self._add_attack_action(actions, self.target_entity)\n    elif self.can_attack(self.target):\n      self._add_attack_action(actions, self.target)\n"
  },
  {
    "path": "nmmo/entity/npc_manager.py",
    "content": "from typing import Callable\nfrom nmmo.entity.entity_manager import EntityGroup\nfrom nmmo.entity.npc import NPC, Soldier, Aggressive, PassiveAggressive, Passive\nfrom nmmo.core import action\nfrom nmmo.systems import combat\nfrom nmmo.lib import spawn\n\n\nclass NPCManager(EntityGroup):\n  def __init__(self, realm, np_random):\n    super().__init__(realm, np_random)\n    self.next_id = -1\n    self.spawn_dangers = []\n\n  def reset(self, np_random):\n    super().reset(np_random)\n    self.next_id = -1\n    self.spawn_dangers.clear()\n\n  def actions(self):\n    return {idx: entity.decide() for idx, entity in self.entities.items()}\n\n  def default_spawn(self):\n    config = self.config\n    if not config.NPC_SYSTEM_ENABLED:\n      return\n\n    for _ in range(config.NPC_SPAWN_ATTEMPTS):\n      if len(self.entities) >= config.NPC_N:\n        break\n\n      if len(self.spawn_dangers) > 0:\n        danger = self.spawn_dangers.pop(0)  # FIFO\n        r, c   = combat.spawn(config, danger, self._np_random)\n      else:\n        center = config.MAP_CENTER\n        border = self.config.MAP_BORDER\n        # pylint: disable=unbalanced-tuple-unpacking\n        r, c   = self._np_random.integers(border, center+border, 2).tolist()\n\n      npc = NPC.default_spawn(self.realm, (r, c), self.next_id, self._np_random)\n      if npc:\n        super().spawn_entity(npc)\n        self.next_id -= 1\n\n  def spawn_npc(self, r, c, danger=None, name=None, order=None,\n                apply_beta_to_danger=True):\n    if not self.realm.map.tiles[r, c].habitable:\n      return None\n\n    if danger and apply_beta_to_danger:\n      danger = min(1.0, max(0.0, danger))  # normalize\n      danger = self._np_random.beta(10*danger+0.01, 10.01-10*danger)  # beta cannot take 0\n    if danger is None:\n      npc = Soldier(self.realm, (r, c), self.next_id, name, order)\n    elif danger >= self.config.NPC_SPAWN_AGGRESSIVE:\n      npc = Aggressive(self.realm, (r, c), self.next_id, name)\n    elif danger >= self.config.NPC_SPAWN_NEUTRAL:\n      npc = PassiveAggressive(self.realm, (r, c), self.next_id, name)\n    elif danger >= self.config.NPC_SPAWN_PASSIVE:\n      npc = Passive(self.realm, (r, c), self.next_id, name)\n    else:\n      return None\n\n    if npc:\n      super().spawn_entity(npc)\n      self.next_id -= 1\n      # NOTE: randomly set the combat style. revisit later\n      npc.skills.style = self._np_random.choice([action.Melee, action.Range, action.Mage])\n    return npc\n\n  def area_spawn(self, r_min, r_max, c_min, c_max, num_spawn,\n                 npc_init_fn: Callable):\n    assert r_min < r_max and c_min < c_max, \"Invalid area\"\n    assert num_spawn > 0, \"Invalid number of spawns\"\n    while num_spawn > 0:\n      r = self._np_random.integers(r_min, r_max+1)\n      c = self._np_random.integers(c_min, c_max+1)\n      if npc_init_fn(r, c):\n        num_spawn -= 1\n\n  def edge_spawn(self, num_spawn, npc_init_fn: Callable):\n    assert num_spawn > 0, \"Invalid number of spawns\"\n    edge_locs = spawn.get_edge_tiles(self.config, self._np_random, shuffle=True)\n    assert len(edge_locs) >= num_spawn, \"Not enough edge locations\"\n    while num_spawn > 0:\n      r, c = edge_locs.pop()\n      npc = npc_init_fn(r, c)\n      if npc:\n        num_spawn -= 1\n"
  },
  {
    "path": "nmmo/entity/player.py",
    "content": "from nmmo.systems.skill import Skills\nfrom nmmo.entity import entity\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.lib import spawn\n\n# pylint: disable=no-member\nclass Player(entity.Entity):\n  def __init__(self, realm, pos, agent, resilient=False):\n    super().__init__(realm, pos, agent.iden, agent.policy)\n\n    self.agent    = agent\n    self._immortal = realm.config.IMMORTAL\n    self.resources.resilient = resilient\n    self.my_task = None\n    self._make_mortal_tick = None  # set to realm.tick when the player is made mortal\n\n    # Scripted hooks\n    self.target = None\n    self.vision = 7\n\n    # Logs\n    self.buys                     = 0\n    self.sells                    = 0\n    self.ration_consumed          = 0\n    self.poultice_consumed        = 0\n    self.ration_level_consumed    = 0\n    self.poultice_level_consumed  = 0\n\n    # initialize skills with the base level\n    self.skills = Skills(realm, self)\n    if realm.config.PROGRESSION_SYSTEM_ENABLED:\n      for skill in self.skills.skills:\n        skill.level.update(realm.config.PROGRESSION_BASE_LEVEL)\n\n    # Gold: initialize with 1 gold (EXCHANGE_BASE_GOLD).\n    # If the base amount is more than 1, alss check the npc's init gold.\n    if realm.config.EXCHANGE_SYSTEM_ENABLED:\n      self.gold.update(realm.config.EXCHANGE_BASE_GOLD)\n\n  @property\n  def serial(self):\n    return self.ent_id\n\n  @property\n  def is_player(self) -> bool:\n    return True\n\n  @property\n  def level(self) -> int:\n    # a player's level is the max of all skills\n    # CHECK ME: the initial level is 1 because of Basic skills,\n    #   which are harvesting food/water and don't progress\n    return max(e.level.val for e in self.skills.skills)\n\n  def _set_immortal(self, value=True, duration=None):\n    self._immortal = value\n    # NOTE: a hack to mark the player as immortal in action targets\n    self.npc_type.update(-1 if value else 0)\n\n    if value and duration is not None:\n      self._make_mortal_tick = self.realm.tick + duration\n    if value is False:\n      self._make_mortal_tick = None\n\n  def make_recon(self, new_pos=None):\n    # NOTE: scout cannot act and cannot die\n    self.status.freeze.update(self.config.MAX_HORIZON)\n    self._set_immortal()\n    self._recon = True\n    if new_pos is not None:\n      if self.ent_id in self.realm.map.tiles[self.pos].entities:\n        self.realm.map.tiles[self.pos].remove_entity(self.ent_id)\n      self.realm.map.tiles[new_pos].add_entity(self)\n      self.set_pos(*new_pos)\n\n  def apply_damage(self, dmg, style):\n    super().apply_damage(dmg, style)\n    self.skills.apply_damage(style)\n\n  # TODO(daveey): The returns for this function are a mess\n  def receive_damage(self, source, dmg):\n    if self.immortal:\n      return False\n\n    # super().receive_damage returns True if self is alive after taking dmg\n    if super().receive_damage(source, dmg):\n      return True\n\n    if not self.config.ITEM_SYSTEM_ENABLED:\n      return False\n\n    # starting from here, source receive gold & inventory items\n    if self.config.EXCHANGE_SYSTEM_ENABLED and source is not None:\n      if self.gold.val > 0:\n        source.gold.increment(self.gold.val)\n        self.realm.event_log.record(EventCode.LOOT_GOLD, source, amount=self.gold.val, target=self)\n        self.gold.update(0)\n\n    # TODO: make source receive the highest-level items first\n    #   because source cannot take it if the inventory is full\n    item_list = list(self.inventory.items)\n    self._np_random.shuffle(item_list)\n    for item in item_list:\n      self.inventory.remove(item)\n\n      # if source is None or NPC, destroy the item\n      if source.is_player:\n        # inventory.receive() returns True if the item is received\n        # if source doesn't have space, inventory.receive() destroys the item\n        if source.inventory.receive(item):\n          self.realm.event_log.record(EventCode.LOOT_ITEM, source, item=item, target=self)\n      else:\n        item.destroy()\n\n    # CHECK ME: this is an empty function. do we still need this?\n    self.skills.receive_damage(dmg)\n    return False\n\n  @property\n  def equipment(self):\n    return self.inventory.equipment\n\n  def packet(self):\n    data = super().packet()\n    data['entID']     = self.ent_id\n    data['resource']  = self.resources.packet()\n    data['skills']    = self.skills.packet()\n    data['inventory'] = self.inventory.packet()\n    # added for the 2.0 web client\n    data[\"metrics\"] = {\n      \"PlayerDefeats\": self.history.player_kills,\n      \"TimeAlive\": self.time_alive.val,\n      \"Gold\": self.gold.val,\n      \"DamageTaken\": self.history.damage_received,}\n    return data\n\n  def update(self, realm, actions):\n    '''Post-action update. Do not include history'''\n    super().update(realm, actions)\n\n    # Spawn battle royale style death fog\n    # Starts at 0 damage on the specified config tick\n    # Moves in from the edges by 1 damage per tile per tick\n    # So after 10 ticks, you take 10 damage at the edge and 1 damage\n    # 10 tiles in, 0 damage in farther\n    # This means all agents will be force killed around\n    # MAP_CENTER / 2 + 100 ticks after spawning\n    fog = self.config.DEATH_FOG_ONSET\n    if fog is not None and self.realm.tick >= fog:\n      dmg = self.realm.fog_map[self.pos]\n      if dmg > 0.5:  # fog_map has float values\n        self.receive_damage(None, round(dmg))\n\n    if not self.alive:\n      return\n\n    if self.config.PLAYER_HEALTH_INCREMENT > 0:\n      self.resources.health.increment(self.config.PLAYER_HEALTH_INCREMENT)\n    self.resources.update(self.immortal)\n    self.skills.update()\n\n    if self._make_mortal_tick is not None and self.realm.tick >= self._make_mortal_tick:\n      self._set_immortal(False)\n\n  def resurrect(self, health_prop=0.5, freeze_duration=10, edge_spawn=True):\n    # Respawn dead players at the edge\n    assert not self.alive, \"Player is not dead\"\n    self.status.freeze.update(freeze_duration)\n    self.resources.health.update(self.config.PLAYER_BASE_HEALTH*health_prop)\n    if self.config.RESOURCE_SYSTEM_ENABLED:\n      self.resources.water.update(self.config.RESOURCE_BASE)\n      self.resources.food.update(self.config.RESOURCE_BASE)\n\n    if edge_spawn:\n      new_spawn_pos = spawn.get_random_coord(self.config, self._np_random, edge=True)\n    else:\n      while True:\n        new_spawn_pos = spawn.get_random_coord(self.config, self._np_random, edge=False)\n        if self.realm.map.tiles[new_spawn_pos].habitable:\n          break\n\n    self.set_pos(*new_spawn_pos)\n    self.message.update(0)\n    self.realm.players.spawn_entity(self)  # put back to the system\n    self._set_immortal(duration=freeze_duration)\n    if self.my_task and len(self.my_task.assignee) == 1:\n      # NOTE: Only one task per agent is supported for now\n      # Agent's task progress need to be reset ONLY IF the task is an agent task\n      self.my_task.reset()\n"
  },
  {
    "path": "nmmo/lib/__init__.py",
    "content": ""
  },
  {
    "path": "nmmo/lib/astar.py",
    "content": "#pylint: disable=invalid-name\nimport heapq\nfrom nmmo.lib.utils import in_bounds\n\nCUTOFF = 100\n\ndef l1(start, goal):\n  sr, sc = start\n  gr, gc = goal\n  return abs(gr - sr) + abs(gc - sc)\n\ndef adjacentPos(pos):\n  r, c = pos\n  return [(r - 1, c), (r, c - 1), (r + 1, c), (r, c + 1)]\n\ndef aStar(realm_map, start, goal, cutoff = CUTOFF):\n  tiles = realm_map.tiles\n  if start == goal:\n    return (0, 0)\n  if (start, goal) in realm_map.pathfinding_cache:\n    return realm_map.pathfinding_cache[(start, goal)]\n  initial_goal = goal\n  pq = [(0, start)]\n\n  backtrace = {}\n  cost = {start: 0}\n\n  closestPos = start\n  closestHeuristic = l1(start, goal)\n  closestCost = closestHeuristic\n\n  while pq:\n    # Use approximate solution if budget exhausted\n    cutoff -= 1\n    if cutoff <= 0:\n      if goal not in backtrace:\n        goal = closestPos\n      break\n\n    priority, cur = heapq.heappop(pq)\n\n    if cur == goal:\n      break\n\n    for nxt in adjacentPos(cur):\n      if not in_bounds(*nxt, tiles.shape) or realm_map.habitable_tiles[nxt] == 0:\n        continue\n\n      newCost = cost[cur] + 1\n      if nxt not in cost or newCost < cost[nxt]:\n        cost[nxt] = newCost\n        heuristic = l1(goal, nxt)\n        priority = newCost + heuristic\n\n        # Compute approximate solution\n        if heuristic < closestHeuristic or (\n            heuristic == closestHeuristic and priority < closestCost):\n          closestPos = nxt\n          closestHeuristic = heuristic\n          closestCost = priority\n\n        heapq.heappush(pq, (priority, nxt))\n        backtrace[nxt] = cur\n\n  while goal in backtrace and backtrace[goal] != start:\n    gr, gc = goal\n    goal = backtrace[goal]\n    sr, sc = goal\n    realm_map.pathfinding_cache[(goal, initial_goal)] = (gr - sr, gc - sc)\n\n  sr, sc = start\n  gr, gc = goal\n  realm_map.pathfinding_cache[(start, initial_goal)] = (gr - sr, gc - sc)\n  return (gr - sr, gc - sc)\n# End A*\n"
  },
  {
    "path": "nmmo/lib/colors.py",
    "content": "# pylint: disable=all\n\n#Various Enums used for handling materials, entity types, etc.\n#Data texture pairs are used for enums that require textures.\n#These textures are filled in by the Render class at run time.\n\nimport numpy as np\nimport colorsys\n\ndef rgb(h):\n  h = h.lstrip('#')\n  return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))\n\ndef rgbNorm(h):\n  h = h.lstrip('#')\n  return tuple(int(h[i:i+2], 16)/255.0 for i in (0, 2, 4))\n\ndef makeColor(idx, h=1, s=1, v=1):\n   r, g, b = colorsys.hsv_to_rgb(h, s, v)\n   rgbval = tuple(int(255*e) for e in [r, g, b])\n   hexval = '%02x%02x%02x' % rgbval\n   return Color(str(idx), hexval)\n\nclass Color:\n    def __init__(self, name, hexVal):\n        self.name = name\n        self.hex = hexVal\n        self.rgb = rgb(hexVal)\n        self.norm = rgbNorm(hexVal)\n        self.value = self.rgb #Emulate enum\n\n    def packet(self):\n        return self.hex\n\nclass Color256:\n   def make256():\n      parh, parv = np.meshgrid(np.linspace(0.075, 1, 16), np.linspace(0.25, 1, 16)[::-1])\n      parh, parv = parh.T.ravel(), parv.T.ravel()\n      idxs = np.arange(256)\n      params = zip(idxs, parh, parv)\n      colors = [makeColor(idx, h=h, s=1, v=v) for idx, h, v in params]\n      return colors\n   colors = make256()\n\nclass Color16:\n   def make():\n      hues   = np.linspace(0, 1, 16)\n      idxs   = np.arange(256)\n      params = zip(idxs, hues)\n      colors = [makeColor(idx, h=h, s=1, v=1) for idx, h in params]\n      return colors\n   colors = make()\n\nclass Tier:\n   BLACK    = Color('BLACK', '#000000')\n   WOOD     = Color('WOOD', '#784d1d')\n   BRONZE   = Color('BRONZE', '#db4508')\n   SILVER   = Color('SILVER', '#dedede')\n   GOLD     = Color('GOLD', '#ffae00')\n   PLATINUM = Color('PLATINUM', '#cd75ff')\n   DIAMOND  = Color('DIAMOND', '#00bbbb')\n\nclass Swatch:\n   def colors():\n      '''Return list of swatch colors'''\n      return\n\n   def rand():\n      '''Return random swatch color'''\n      all_colors = Swatch.colors()\n      randInd = np.random.randint(0, len(all_colors))\n      return all_colors[randInd]\n\n\nclass Neon(Swatch):\n   RED      = Color('RED', '#ff0000')\n   ORANGE   = Color('ORANGE', '#ff8000')\n   YELLOW   = Color('YELLOW', '#ffff00')\n\n   GREEN    = Color('GREEN', '#00ff00')\n   MINT     = Color('MINT', '#00ff80')\n   CYAN     = Color('CYAN', '#00ffff')\n\n   BLUE     = Color('BLUE', '#0000ff')\n   PURPLE   = Color('PURPLE', '#8000ff')\n   MAGENTA  = Color('MAGENTA', '#ff00ff')\n\n   FUCHSIA  = Color('FUCHSIA', '#ff0080')\n   SPRING   = Color('SPRING', '#80ff80')\n   SKY      = Color('SKY', '#0080ff')\n\n   WHITE    = Color('WHITE', '#ffffff')\n   GRAY     = Color('GRAY', '#666666')\n   BLACK    = Color('BLACK', '#000000')\n\n   BLOOD    = Color('BLOOD', '#bb0000')\n   BROWN    = Color('BROWN', '#7a3402')\n   GOLD     = Color('GOLD', '#eec600')\n   SILVER   = Color('SILVER', '#b8b8b8')\n\n   TERM     = Color('TERM', '#41ff00')\n   MASK     = Color('MASK', '#d67fff')\n\n   def colors():\n      return (\n              Neon.CYAN, Neon.MINT, Neon.GREEN,\n              Neon.BLUE, Neon.PURPLE, Neon.MAGENTA,\n              Neon.FUCHSIA, Neon.SPRING, Neon.SKY,\n              Neon.RED, Neon.ORANGE, Neon.YELLOW)\n\nclass Solid(Swatch):\n   BLUE       = Color('BLUE', '#1f77b4')\n   ORANGE     = Color('ORANGE', '#ff7f0e')\n   GREEN      = Color('GREEN', '#2ca02c')\n\n   RED        = Color('RED',  '#D62728')\n   PURPLE     = Color('PURPLE', '#9467bd')\n   BROWN      = Color('BROWN', '#8c564b')\n\n   PINK       = Color('PINK', '#e377c2')\n   GREY       = Color('GREY', '#7f7f7f')\n   CHARTREUSE = Color('CHARTREUSE', '#bcbd22')\n\n   SKY        = Color('SKY', '#17becf')\n\n   def colors():\n      return (\n              Solid.BLUE, Solid.ORANGE, Solid.GREEN,\n              Solid.RED, Solid.PURPLE, Solid.BROWN,\n              Solid.PINK, Solid.CHARTREUSE, Solid.SKY,\n              Solid.GREY)\n\nclass Palette:\n   def __init__(self, initial_swatch=Neon):\n      self.colors = {}\n      for idx, color in enumerate(initial_swatch.colors()):\n          self.colors[idx] = color\n\n   def color(self, idx):\n      if idx in self.colors:\n           return self.colors[idx]\n\n      color = makeColor(idx, h=np.random.rand(), s=1, v=1)\n      self.colors[idx] = color\n      return color\n"
  },
  {
    "path": "nmmo/lib/cython_helper.pyx",
    "content": "#cython: boundscheck=True\n#cython: wraparound=True\n#cython: nonecheck=True\n\nfrom types import SimpleNamespace\nimport numpy as np\ncimport numpy as cnp\n\n# for array indexing\ncnp.import_array()\n\ndef make_move_mask(cnp.ndarray[cnp.int8_t] mask,\n                   cnp.ndarray[cnp.int8_t, ndim=2] habitable_tiles,\n                   short row, short col,\n                   cnp.ndarray[cnp.int64_t] row_delta,\n                   cnp.ndarray[cnp.int64_t] col_delta):\n  for i in range(4):\n    mask[i] = habitable_tiles[row_delta[i] + row, col_delta[i] + col]\n\n# NOTE: assume that incoming mask are all zeros\ndef make_attack_mask(cnp.ndarray[cnp.int8_t] mask,\n                     cnp.ndarray[cnp.int16_t, ndim=2] entities,\n                     dict entity_attr,\n                     dict my_info):\n  cdef short idx\n  cdef short num_valid_target = 0\n  cdef short attr_id = entity_attr[\"id\"]\n  cdef short attr_time_alive = entity_attr[\"time_alive\"]\n  cdef short attr_npc_type = entity_attr[\"npc_type\"]\n  cdef short attr_row = entity_attr[\"row\"]\n  cdef short attr_col = entity_attr[\"col\"]\n\n  for idx in range(len(entities)):\n    # skip empty row\n    if entities[idx, attr_id] == 0:\n      continue\n    # out of range\n    if abs(entities[idx, attr_row] - my_info[\"row\"]) > my_info[\"attack_range\"] or \\\n       abs(entities[idx, attr_col] - my_info[\"col\"]) > my_info[\"attack_range\"]:\n      continue\n    # cannot attack during immunity\n    if entities[idx, attr_id] > 0 and \\\n       entities[idx, attr_time_alive] < my_info[\"immunity\"]:\n      continue\n    # cannot attack self\n    if entities[idx, attr_id] == my_info[\"agent_id\"]:\n      continue\n    # npc_type must be 0, 1, 2, 3\n    if entities[idx, attr_npc_type] < 0:  # immortal (-1)\n      continue\n    mask[idx] = 1\n    num_valid_target += 1\n\n  # cython: wraparound need to be True\n  # if any valid target, set the no-op to 0\n  mask[-1] = 0 if num_valid_target > 0 else 1\n\ndef parse_array(short[:] data, dict attr_name_to_col):\n  cdef short col\n  cdef str attr\n  cdef dict result = {}\n  for attr, col in attr_name_to_col.items():\n    result[attr] = data[col]\n  return SimpleNamespace(**result)\n"
  },
  {
    "path": "nmmo/lib/event_code.py",
    "content": "class EventCode:\n  # Move\n  EAT_FOOD = 1\n  DRINK_WATER = 2\n  GO_FARTHEST = 3  # record when breaking the previous record\n  SEIZE_TILE = 4\n\n  # Attack\n  SCORE_HIT = 11\n  PLAYER_KILL = 12\n  FIRE_AMMO = 13\n\n  # Item\n  CONSUME_ITEM = 21\n  GIVE_ITEM = 22\n  DESTROY_ITEM = 23\n  HARVEST_ITEM = 24\n  EQUIP_ITEM = 25\n  LOOT_ITEM = 26\n\n  # Exchange\n  GIVE_GOLD = 31\n  LIST_ITEM = 32\n  EARN_GOLD = 33\n  BUY_ITEM = 34\n  LOOT_GOLD = 35\n\n  # Level up\n  LEVEL_UP = 41\n\n  # System-related\n  AGENT_CULLED = 91  # player is removed from the realm (culled)\n"
  },
  {
    "path": "nmmo/lib/event_log.py",
    "content": "from types import SimpleNamespace\nfrom typing import List\nfrom copy import deepcopy\nfrom collections import defaultdict\n\nimport numpy as np\n\nfrom nmmo.datastore.serialized import SerializedState\nfrom nmmo.entity import Entity\nfrom nmmo.systems.item import Item\nfrom nmmo.lib.event_code import EventCode\n\n# pylint: disable=no-member\nEventState = SerializedState.subclass(\"Event\", [\n  \"recorded\", # event_log is write-only, no update or delete, so no need for row id\n  \"ent_id\",\n  \"tick\",\n\n  \"event\",\n\n  \"type\",\n  \"level\",\n  \"number\",\n  \"gold\",\n  \"target_ent\",\n])\n\nEventAttr = EventState.State.attr_name_to_col\n\nEventState.Query = SimpleNamespace(\n  table=lambda ds: ds.table(\"Event\").where_eq(EventAttr[\"recorded\"], 1),\n  by_event=lambda ds, event_code: ds.table(\"Event\").where_eq(\n    EventAttr[\"event\"], event_code),\n  by_tick=lambda ds, tick: ds.table(\"Event\").where_eq(\n    EventAttr[\"tick\"], tick),\n)\n\n# defining col synoyms for different event types\nATTACK_COL_MAP = {\n  'combat_style': EventAttr['type'],\n  'damage': EventAttr['number']}\nITEM_COL_MAP = {\n  'item_type': EventAttr['type'],\n  'quantity': EventAttr['number'],\n  'price': EventAttr['gold'],\n  'item_id': EventAttr['target_ent']}\nLEVEL_COL_MAP = {'skill': EventAttr['type']}\nEXPLORE_COL_MAP = {'distance': EventAttr['number']}\nTILE_COL_MAP = {'tile_row': EventAttr['number'],\n                'tile_col': EventAttr['gold']}\n\n\nclass EventLogger(EventCode):\n  def __init__(self, realm):\n    self.realm = realm\n    self.config = realm.config\n    self.datastore = realm.datastore\n\n    self.valid_events = { val: evt for evt, val in EventCode.__dict__.items()\n                           if isinstance(val, int) }\n    self._data_by_tick = {}\n    self._last_tick = 0\n    self._empty_data = np.empty((0, len(EventAttr)))\n\n    # add synonyms to the attributes\n    self.attr_to_col = deepcopy(EventAttr)\n    self.attr_to_col.update(ATTACK_COL_MAP)\n    self.attr_to_col.update(ITEM_COL_MAP)\n    self.attr_to_col.update(LEVEL_COL_MAP)\n    self.attr_to_col.update(EXPLORE_COL_MAP)\n    self.attr_to_col.update(TILE_COL_MAP)\n\n  def reset(self):\n    EventState.State.table(self.datastore).reset()\n\n  # define event logging\n  def _create_event(self, entity: Entity, event_code: int):\n    log = EventState(self.datastore)\n    log.recorded.update(1)\n    log.ent_id.update(entity.ent_id)\n    # the tick increase by 1 after executing all actions\n    log.tick.update(self.realm.tick+1)\n    log.event.update(event_code)\n\n    return log\n\n  def record(self, event_code: int, entity: Entity, **kwargs):\n    if event_code in [EventCode.EAT_FOOD, EventCode.DRINK_WATER,\n                      EventCode.GIVE_ITEM, EventCode.DESTROY_ITEM,\n                      EventCode.GIVE_GOLD, EventCode.AGENT_CULLED]:\n      # Logs for these events are for counting only\n      self._create_event(entity, event_code)\n      return\n\n    if event_code == EventCode.GO_FARTHEST: # use EXPLORE_COL_MAP\n      if ('distance' in kwargs and kwargs['distance'] > 0):\n        log = self._create_event(entity, event_code)\n        log.number.update(kwargs['distance'])\n        return\n\n    if event_code == EventCode.SCORE_HIT:\n      # kwargs['combat_style'] should be Skill.CombatSkill\n      if ('combat_style' in kwargs and kwargs['combat_style'].SKILL_ID in [1, 2, 3]) & \\\n         ('target' in kwargs and isinstance(kwargs['target'], Entity)) & \\\n         ('damage' in kwargs and kwargs['damage'] >= 0):\n        log = self._create_event(entity, event_code)\n        log.type.update(kwargs['combat_style'].SKILL_ID)\n        log.number.update(kwargs['damage'])\n        log.target_ent.update(kwargs['target'].ent_id)\n        return\n\n    if event_code == EventCode.PLAYER_KILL:\n      if ('target' in kwargs and isinstance(kwargs['target'], Entity)):\n        target = kwargs['target']\n        log = self._create_event(entity, event_code)\n        log.target_ent.update(target.ent_id)\n        log.level.update(target.attack_level)\n        return\n\n    if event_code == EventCode.LOOT_ITEM:\n      if ('item' in kwargs and isinstance(kwargs['item'], Item)) & \\\n         ('target' in kwargs and isinstance(kwargs['target'], Entity)):\n        item = kwargs['item']\n        log = self._create_event(entity, event_code)\n        log.type.update(item.ITEM_TYPE_ID)\n        log.level.update(item.level.val)\n        log.number.update(item.quantity.val)\n        log.target_ent.update(item.id.val)\n        return\n\n    if event_code == EventCode.LOOT_GOLD:\n      if ('amount' in kwargs and kwargs['amount'] > 0) & \\\n         ('target' in kwargs and isinstance(kwargs['target'], Entity)):\n        log = self._create_event(entity, event_code)\n        log.gold.update(kwargs['amount'])\n        log.target_ent.update(kwargs['target'].ent_id)\n        return\n\n    if event_code in [EventCode.CONSUME_ITEM, EventCode.HARVEST_ITEM, EventCode.EQUIP_ITEM,\n                      EventCode.FIRE_AMMO]:\n      if ('item' in kwargs and isinstance(kwargs['item'], Item)):\n        item = kwargs['item']\n        log = self._create_event(entity, event_code)\n        log.type.update(item.ITEM_TYPE_ID)\n        log.level.update(item.level.val)\n        log.number.update(item.quantity.val)\n        log.target_ent.update(item.id.val)\n        return\n\n    if event_code in [EventCode.LIST_ITEM, EventCode.BUY_ITEM]:\n      if ('item' in kwargs and isinstance(kwargs['item'], Item)) & \\\n         ('price' in kwargs and kwargs['price'] > 0):\n        item = kwargs['item']\n        log = self._create_event(entity, event_code)\n        log.type.update(item.ITEM_TYPE_ID)\n        log.level.update(item.level.val)\n        log.number.update(item.quantity.val)\n        log.gold.update(kwargs['price'])\n        log.target_ent.update(item.id.val)\n        return\n\n    if event_code == EventCode.EARN_GOLD:\n      if ('amount' in kwargs and kwargs['amount'] > 0):\n        log = self._create_event(entity, event_code)\n        log.gold.update(kwargs['amount'])\n        return\n\n    if event_code == EventCode.LEVEL_UP:\n      # kwargs['skill'] should be Skill.Skill\n      if ('skill' in kwargs and kwargs['skill'].SKILL_ID in range(1,9)) & \\\n         ('level' in kwargs and kwargs['level'] >= 0):\n        log = self._create_event(entity, event_code)\n        log.type.update(kwargs['skill'].SKILL_ID)\n        log.level.update(kwargs['level'])\n        return\n\n    if event_code == EventCode.SEIZE_TILE:\n      if ('tile' in kwargs and isinstance(kwargs['tile'], tuple)):\n        log = self._create_event(entity, event_code)\n        log.number.update(kwargs['tile'][0])  # row\n        log.gold.update(kwargs['tile'][1])  # col\n        return\n\n    # If reached here, then something is wrong\n    # CHECK ME: The below should be commented out after debugging\n    raise ValueError(f\"Event code: {event_code}\", kwargs)\n\n  def update(self):\n    curr_tick = self.realm.tick\n    if curr_tick > self._last_tick:\n      self._data_by_tick[curr_tick] = EventState.Query.by_tick(self.datastore, curr_tick)\n      self._last_tick = curr_tick\n\n  def get_data(self, event_code=None, agents: List[int]=None, tick: int=None) -> np.ndarray:\n    if tick is not None:\n      if tick == -1:\n        tick = self._last_tick\n      if tick not in self._data_by_tick:\n        return self._empty_data\n      event_data = self._data_by_tick[tick]\n    else:\n      event_data = EventState.Query.table(self.datastore)\n\n    if event_data.shape[0] > 0:\n      if event_code is None:\n        flt_idx = event_data[:, EventAttr[\"event\"]] > 0\n      else:\n        flt_idx = event_data[:, EventAttr[\"event\"]] == event_code\n      if agents:\n        flt_idx &= np.in1d(event_data[:, EventAttr[\"ent_id\"]], agents)\n      return event_data[flt_idx]\n\n    return self._empty_data\n\n  def get_stat(self):\n    event_stat = defaultdict(lambda: defaultdict(int))\n    event_data = EventState.Query.table(self.datastore)\n    for row in event_data:\n      agent_id = row[EventAttr['ent_id']]\n      if agent_id > 0:\n        key = extract_event_key(row)\n        if key is None:\n          continue\n\n        if key[0] == EventCode.GO_FARTHEST:\n          event_stat[agent_id][key] = max(event_stat[agent_id][key],\n                                          row[EventAttr['number']])  # distance\n        elif key[0] in [EventCode.LEVEL_UP, EventCode.EQUIP_ITEM]:\n          event_stat[agent_id][key] = max(event_stat[agent_id][key],\n                                          row[EventAttr['level']])\n        elif key[0] == EventCode.AGENT_CULLED:\n          event_stat[agent_id][key] = row[EventAttr['tick']]  # lifespan\n        else:\n          event_stat[agent_id][key] += 1\n\n    return event_stat\n\ndef extract_event_key(event_row):\n  event_code = event_row[EventAttr['event']]\n\n  if event_code in [\n    EventCode.EAT_FOOD,\n    EventCode.DRINK_WATER,\n    EventCode.GO_FARTHEST,\n    EventCode.AGENT_CULLED,\n  ]:\n    return (event_code,)\n\n  if event_code in [\n    EventCode.SCORE_HIT,\n    EventCode.FIRE_AMMO,\n    EventCode.LEVEL_UP,\n    EventCode.HARVEST_ITEM,\n    EventCode.CONSUME_ITEM,\n    EventCode.EQUIP_ITEM,\n    EventCode.LIST_ITEM,\n    EventCode.BUY_ITEM,\n  ]:\n    return (event_code, event_row[EventAttr['type']])\n\n  if event_code == EventCode.PLAYER_KILL:\n    return (event_code, int(event_row[EventAttr['target_ent']] > 0))  # if target is agent or npc\n\n  return None\n"
  },
  {
    "path": "nmmo/lib/material.py",
    "content": "\nfrom nmmo.systems import item, droptable\n\nclass Material:\n  capacity = 0\n  tool = None\n  table = None\n  index = None\n  respawn = 0\n\n  def __init__(self, config):\n    pass\n\n  def __eq__(self, mtl):\n    return self.index == mtl.index\n\n  def __equals__(self, mtl):\n    return self == mtl\n\n  def harvest(self):\n    return self.__class__.table\n\nclass Void(Material):\n  tex   = 'void'\n  index = 0\n\nclass Water(Material):\n  tex   = 'water'\n  index = 1\n\n  table = droptable.Empty()\n\n  def __init__(self, config):\n    self.deplete = __class__\n    self.respawn  = 1.0\n\nclass Grass(Material):\n  tex   = 'grass'\n  index = 2\n\nclass Scrub(Material):\n  tex   = 'scrub'\n  index = 3\n\nclass Foilage(Material):\n  tex   = 'foilage'\n  index = 4\n\n  deplete = Scrub\n  table = droptable.Empty()\n\n  def __init__(self, config):\n    if config.RESOURCE_SYSTEM_ENABLED:\n      self.capacity = config.RESOURCE_FOILAGE_CAPACITY\n      self.respawn  = config.RESOURCE_FOILAGE_RESPAWN\n\nclass Stone(Material):\n  tex   = 'stone'\n  index = 5\n\nclass Slag(Material):\n  tex   = 'slag'\n  index = 6\n\nclass Ore(Material):\n  tex   = 'ore'\n  index = 7\n\n  deplete = Slag\n  tool    = item.Pickaxe\n\n  def __init__(self, config):\n    cls = self.__class__\n    if cls.table is None:\n      cls.table = droptable.Standard()\n      cls.table.add(item.Whetstone)\n\n      if config.EQUIPMENT_SYSTEM_ENABLED:\n        cls.table.add(item.Wand, prob=config.WEAPON_DROP_PROB)\n\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.capacity = config.PROFESSION_ORE_CAPACITY\n      self.respawn  = config.PROFESSION_ORE_RESPAWN\n\n  tool    = item.Pickaxe\n  deplete = Slag\n\nclass Stump(Material):\n  tex   = 'stump'\n  index = 8\n\nclass Tree(Material):\n  tex   = 'tree'\n  index = 9\n\n  deplete = Stump\n  tool    = item.Axe\n\n  def __init__(self, config):\n    cls = self.__class__\n    if cls.table is None:\n      cls.table = droptable.Standard()\n      cls.table.add(item.Arrow)\n      if config.EQUIPMENT_SYSTEM_ENABLED:\n        cls.table.add(item.Spear, prob=config.WEAPON_DROP_PROB)\n\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.capacity = config.PROFESSION_TREE_CAPACITY\n      self.respawn  = config.PROFESSION_TREE_RESPAWN\n\nclass Fragment(Material):\n  tex   = 'fragment'\n  index = 10\n\nclass Crystal(Material):\n  tex   = 'crystal'\n  index = 11\n\n  deplete = Fragment\n  tool    = item.Chisel\n\n  def __init__(self, config):\n    cls = self.__class__\n    if cls.table is None:\n      cls.table = droptable.Standard()\n      cls.table.add(item.Runes)\n      if config.EQUIPMENT_SYSTEM_ENABLED:\n        cls.table.add(item.Bow, prob=config.WEAPON_DROP_PROB)\n\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.capacity = config.PROFESSION_CRYSTAL_CAPACITY\n      self.respawn  = config.PROFESSION_CRYSTAL_RESPAWN\n\nclass Weeds(Material):\n  tex   = 'weeds'\n  index = 12\n\nclass Herb(Material):\n  tex   = 'herb'\n  index = 13\n\n  deplete = Weeds\n  tool    = item.Gloves\n\n  table   = droptable.Standard()\n  table.add(item.Potion)\n\n  def __init__(self, config):\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.capacity = config.PROFESSION_HERB_CAPACITY\n      self.respawn  = config.PROFESSION_HERB_RESPAWN\n\nclass Ocean(Material):\n  tex   = 'ocean'\n  index = 14\n\nclass Fish(Material):\n  tex   = 'fish'\n  index = 15\n\n  deplete = Ocean\n  tool    = item.Rod\n\n  table   = droptable.Standard()\n  table.add(item.Ration)\n\n  def __init__(self, config):\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.capacity = config.PROFESSION_FISH_CAPACITY\n      self.respawn  = config.PROFESSION_FISH_RESPAWN\n\n# TODO: Fix lint errors\n# pylint: disable=all\nclass Meta(type):\n  def __init__(self, name, bases, dict):\n    self.indices = {mtl.index for mtl in self.materials}\n\n  def __iter__(self):\n    yield from self.materials\n\n  def __contains__(self, mtl):\n    if isinstance(mtl, Material):\n      mtl = type(mtl)\n    if isinstance(mtl, type):\n      return mtl in self.materials\n    return mtl in self.indices\n\nclass All(metaclass=Meta):\n  '''List of all materials'''\n  materials = {\n    Void, Water, Grass, Scrub, Foilage,\n    Stone, Slag, Ore, Stump, Tree,\n    Fragment, Crystal, Weeds, Herb, Ocean, Fish}\n\nclass Impassible(metaclass=Meta):\n  '''Materials that agents cannot walk through'''\n  materials = {Void, Water, Stone, Ocean, Fish}\n\nclass Habitable(metaclass=Meta):\n  '''Materials that agents cannot walk on'''\n  materials = {Grass, Scrub, Foilage, Ore, Slag, Tree, Stump, Crystal, Fragment, Herb, Weeds}\n\nclass Harvestable(metaclass=Meta):\n  '''Materials that agents can harvest'''\n  materials = {Water, Foilage, Ore, Tree, Crystal, Herb, Fish}\n"
  },
  {
    "path": "nmmo/lib/seeding.py",
    "content": "# copied from https://github.com/openai/gym/blob/master/gym/utils/seeding.py\n\"\"\"Set of random number generator functions: seeding, generator, hashing seeds.\"\"\"\nfrom typing import Any, Optional, Tuple\nimport numpy as np\n\n\nclass RandomNumberGenerator(np.random.Generator):\n  def __init__(self, bit_generator):\n    super().__init__(bit_generator)\n    self._dir_seq_len = 1024\n    self._wrap = self._dir_seq_len - 1\n    self._dir_seq = list(self.integers(0, 4, size=self._dir_seq_len))\n    self._dir_idx = 0\n\n  # provide a random direction from the pre-generated sequence\n  def get_direction(self):\n    self._dir_idx = (self._dir_idx + 1) & self._wrap\n    return self._dir_seq[self._dir_idx]\n\ndef np_random(seed: Optional[int] = None) -> Tuple[np.random.Generator, Any]:\n  \"\"\"Generates a random number generator from the seed and returns the Generator and seed.\n\n  Args:\n      seed: The seed used to create the generator\n\n  Returns:\n      The generator and resulting seed\n\n  Raises:\n      Error: Seed must be a non-negative integer or omitted\n  \"\"\"\n  if seed is not None and not (isinstance(seed, int) and 0 <= seed):\n    raise ValueError(f\"Seed must be a non-negative integer or omitted, not {seed}\")\n\n  seed_seq = np.random.SeedSequence(seed)\n  np_seed = seed_seq.entropy\n  rng = RandomNumberGenerator(np.random.PCG64(seed_seq))\n  return rng, np_seed\n"
  },
  {
    "path": "nmmo/lib/spawn.py",
    "content": "from itertools import chain\n\nclass SequentialLoader:\n  '''config.PLAYER_LOADER that spreads out agent populations'''\n  def __init__(self, config, np_random,\n               candidate_spawn_pos=None):\n    items = config.PLAYERS\n\n    self.items = items\n    self.idx   = -1\n\n    if candidate_spawn_pos:\n      self.candidate_spawn_pos = candidate_spawn_pos\n    else:\n      # np_random is the env-level rng\n      self.candidate_spawn_pos = get_edge_tiles(config, np_random, shuffle=True)\n\n  def __iter__(self):\n    return self\n\n  def __next__(self):\n    self.idx = (self.idx + 1) % len(self.items)\n    return self.items[self.idx]\n\n  # pylint: disable=unused-argument\n  def get_spawn_position(self, agent_id):\n    # the basic SequentialLoader just provides a random spawn position\n    return self.candidate_spawn_pos.pop()\n\ndef get_random_coord(config, np_random, edge=True):\n  '''Generates spawn positions for new agents\n\n  Randomly selects spawn positions around\n  the borders of the square game map\n\n  Returns:\n      tuple(int, int):\n\n  position:\n      The position (row, col) to spawn the given agent\n  '''\n  mmax = config.MAP_CENTER + config.MAP_BORDER\n  mmin = config.MAP_BORDER\n\n  # np_random is the env-level RNG, a drop-in replacement of numpy.random\n  if edge:\n    var  = np_random.integers(mmin, mmax)\n    fixed = np_random.choice([mmin, mmax])\n    r, c = int(var), int(fixed)\n    if np_random.random() > 0.5:\n      r, c = c, r\n  else:\n    r, c = np_random.integers(mmin, mmax, 2).tolist()\n  return (r, c)\n\ndef get_edge_tiles(config, np_random=None, shuffle=False):\n  '''Returns a list of all edge tiles.\n     To shuffle the tile, provide a np_random object\n  '''\n  # Accounts for void borders in coord calcs\n  left = config.MAP_BORDER\n  right = config.MAP_CENTER + config.MAP_BORDER\n  lows = config.MAP_CENTER * [left]\n  highs = config.MAP_CENTER * [right]\n  inc = list(range(config.MAP_BORDER, config.MAP_CENTER+config.MAP_BORDER))\n\n  # All edge tiles in order\n  sides = []\n  sides.append(list(zip(lows, inc)))\n  sides.append(list(zip(inc, highs)))\n  sides.append(list(zip(highs, inc[::-1])))\n  sides.append(list(zip(inc[::-1], lows)))\n  tiles = list(chain(*sides))\n  if shuffle and np_random:\n    np_random.shuffle(tiles)\n  return tiles\n"
  },
  {
    "path": "nmmo/lib/team_helper.py",
    "content": "from typing import Any, Dict, List\nimport numpy.random\nfrom nmmo.lib import spawn\n\n\ndef make_teams(config, num_teams):\n  num_per_team = config.PLAYER_N // num_teams\n  teams = {}\n  for team_id in range(num_teams):\n    range_max = (team_id+1)*num_per_team+1 if team_id < num_teams-1 else config.PLAYER_N+1\n    teams[team_id] = list(range(team_id*num_per_team+1, range_max))\n  return teams\n\nclass TeamHelper:\n  def __init__(self, teams: Dict[Any, List[int]], np_random=None):\n    self.teams = teams\n    self.num_teams = len(teams)\n    self.team_list = list(teams.keys())\n    self.team_size = {}\n    self.team_and_position_for_agent = {}\n    self.agent_for_team_and_position = {}\n\n    for team_id, team in teams.items():\n      self.team_size[team_id] = len(team)\n      for position, agent_id in enumerate(team):\n        self.team_and_position_for_agent[agent_id] = (team_id, position)\n        self.agent_for_team_and_position[team_id, position] = agent_id\n\n    # Left/right team order is determined by team_list, so shuffling it\n    # TODO: check if this is correct\n    np_random = np_random or numpy.random\n    # np_random.shuffle(self.team_list)\n\n  def agent_position(self, agent_id: int) -> int:\n    return self.team_and_position_for_agent[agent_id][1]\n\n  def agent_id(self, team_id: Any, position: int) -> int:\n    return self.agent_for_team_and_position[team_id, position]\n\n  def is_agent_in_team(self, agent_id:int , team_id: Any) -> bool:\n    return agent_id in self.teams[team_id]\n\n  def get_team_idx(self, agent_id:int) -> int:\n    team_id, _ = self.team_and_position_for_agent[agent_id]\n    return self.team_list.index(team_id)\n\n  def get_target_agent(self, team_id: Any, target: str):\n    idx = self.team_list.index(team_id)\n    if target == \"left_team\":\n      target_id = self.team_list[(idx+1) % self.num_teams]\n      return self.teams[target_id]\n    if target == \"left_team_leader\":\n      target_id = self.team_list[(idx+1) % self.num_teams]\n      return self.teams[target_id][0]\n    if target == \"right_team\":\n      target_id = self.team_list[(idx-1) % self.num_teams]\n      return self.teams[target_id]\n    if target == \"right_team_leader\":\n      target_id = self.team_list[(idx-1) % self.num_teams]\n      return self.teams[target_id][0]\n    if target == \"my_team_leader\":\n      return self.teams[team_id][0]\n    if target == \"all_foes\":\n      all_foes = []\n      for foe_team_id in self.team_list:\n        if foe_team_id != team_id:\n          all_foes += self.teams[foe_team_id]\n      return all_foes\n    if target == \"all_foe_leaders\":\n      leaders = []\n      for foe_team_id in self.team_list:\n        if foe_team_id != team_id:\n          leaders.append(self.teams[foe_team_id][0])\n      return leaders\n    return None\n\nclass RefillPopper:\n  def __init__(self, original_list, np_random=None):\n    assert isinstance(original_list, list), \"original_list must be a list of (row, col) tuples\"\n    self._original_list = original_list\n    self._np_random = np_random or numpy.random\n    self._refill_list = list(original_list)  # copy\n\n  def pop(self):\n    if len(self._original_list) == 1:\n      return self._original_list[0]\n    if not self._refill_list:\n      self._refill_list = list(self._original_list)\n    pop_idx = self._np_random.integers(len(self._refill_list))\n    return self._refill_list.pop(pop_idx)\n\nclass TeamLoader(spawn.SequentialLoader):\n  def __init__(self, config, np_random,\n               candidate_spawn_pos: List[List] = None):\n    assert config.TEAMS is not None, \"config.TEAMS must be specified\"\n    self.team_helper = TeamHelper(config.TEAMS, np_random)\n    # Check if the team specification is valid for spawning\n    assert len(self.team_helper.team_and_position_for_agent.keys()) == config.PLAYER_N,\\\n      \"Number of agents in config.TEAMS must be equal to config.PLAYER_N\"\n    for agent_id in range(1, config.PLAYER_N + 1):\n      assert agent_id in self.team_helper.team_and_position_for_agent,\\\n        f\"Agent id {agent_id} is not specified in config.TEAMS\"\n    super().__init__(config, np_random)\n\n    if candidate_spawn_pos is None:\n      candidate_spawn_pos = spawn_team_together(config, self.team_helper.num_teams)\n    elif not isinstance(candidate_spawn_pos[0], list):\n      # candidate_spawn_pos for teams should be List[List]\n      candidate_spawn_pos = [[pos] for pos in candidate_spawn_pos]\n\n    np_random.shuffle(candidate_spawn_pos)\n    self.candidate_spawn_pos = [RefillPopper(pos_list, np_random)\n                                for pos_list in candidate_spawn_pos]\n\n  def get_spawn_position(self, agent_id):\n    idx = self.team_helper.get_team_idx(agent_id)\n    return self.candidate_spawn_pos[idx].pop()\n\ndef spawn_team_together(config, num_teams):\n  '''Generates spawn positions for new teams\n  Agents in the same team spawn together in the same tile\n  Evenly spaces teams around the square map borders\n  Returns:\n      list of tuple(int, int):\n  position:\n      The position (row, col) to spawn the given teams\n  '''\n  teams_per_sides = (num_teams + 3) // 4 # 1-4 -> 1, 5-8 -> 2, etc.\n\n  tiles = spawn.get_edge_tiles(config)\n  each_side = len(tiles) // 4\n  assert each_side > 4*teams_per_sides, 'Map too small for teams'\n  sides = [tiles[i*each_side:(i+1)*each_side] for i in range(4)]\n\n  team_spawn_positions = []\n  for side in sides:\n    for i in range(teams_per_sides):\n      idx = int(len(side)*(i+1)/(teams_per_sides + 1))\n      team_spawn_positions.append([side[idx]])\n\n  return team_spawn_positions\n"
  },
  {
    "path": "nmmo/lib/utils.py",
    "content": "# pylint: disable=all\nimport inspect\nfrom collections import deque\nimport hashlib\nimport numpy as np\nfrom nmmo.entity.entity import Entity, EntityState\n\nEntityAttr = EntityState.State.attr_name_to_col\n\nclass staticproperty(property):\n  def __get__(self, cls, owner):\n    return self.fget.__get__(None, owner)()\n\nclass classproperty(object):\n  def __init__(self, f):\n    self.f = f\n  def __get__(self, obj, owner):\n    return self.f(owner)\n\nclass Iterable(type):\n  def __iter__(cls):\n    queue = deque(cls.__dict__.items())\n    while len(queue) > 0:\n      name, attr = queue.popleft()\n      if type(name) != tuple:\n        name = tuple([name])\n      if not inspect.isclass(attr):\n        continue\n      yield name, attr\n\n  def values(cls):\n    return [e[1] for e in cls]\n\nclass StaticIterable(type):\n  def __iter__(cls):\n    stack = list(cls.__dict__.items())\n    stack.reverse()\n    for name, attr in stack:\n      if name == '__module__':\n        continue\n      if name.startswith('__'):\n        break\n      yield name, attr\n\nclass NameComparable(type):\n  def __hash__(self):\n    return hash(self.__name__)\n\n  def __eq__(self, other):\n    return self.__name__ == other.__name__\n\n  def __ne__(self, other):\n    return self.__name__ != other.__name__\n\n  def __lt__(self, other):\n    return self.__name__ < other.__name__\n\n  def __le__(self, other):\n    return self.__name__ <= other.__name__\n\n  def __gt__(self, other):\n    return self.__name__ > other.__name__\n\n  def __ge__(self, other):\n    return self.__name__ >= other.__name__\n\nclass IterableNameComparable(Iterable, NameComparable):\n  pass\n\ndef linf(pos1, pos2):\n  # pos could be a single (r,c) or a vector of (r,c)s\n  diff = np.abs(np.array(pos1) - np.array(pos2))\n  return np.max(diff, axis=-1)\n\ndef linf_single(pos1, pos2):\n  # pos is a single (r,c) to avoid uneccessary function calls\n  return max(abs(pos1[0]-pos2[0]), abs(pos1[1]-pos2[1]))\n\n#Bounds checker\ndef in_bounds(r, c, shape, border=0):\n  R, C = shape\n  return (\n    r > border and\n    c > border and\n    r < R - border and\n    c < C - border\n  )\n\ndef l1_map(size):\n  # l1 distance from the center tile (size//2, size//2)\n  x      = np.abs(np.arange(size) - size//2)\n  X, Y   = np.meshgrid(x, x)\n  data   = np.stack((X, Y), -1)\n  return np.max(abs(data), -1)\n\ndef get_hash_embedding(func, embed_dim):\n  # NOTE: This is a hacky way to get a hash embedding for a function\n  # TODO: Can we get more meaningful embedding? coding LLMs are good but huge\n  func_src = inspect.getsource(func)\n  hash_object = hashlib.sha256(func_src.encode())\n  hex_digest = hash_object.hexdigest()\n\n  # Convert the hexadecimal hash to a numpy array with float16 data type\n  hash_bytes = bytes.fromhex(hex_digest)\n  hash_array = np.frombuffer(hash_bytes, dtype=np.float16)\n  hash_array = np.nan_to_num(hash_array, nan=1, posinf=1, neginf=1)\n  hash_array = np.log(abs(hash_array.astype(float)))\n  hash_array -= hash_array.mean()\n  hash_array /= hash_array.std()\n  embedding = np.zeros(embed_dim, dtype=np.float16)\n  embedding[:len(hash_array)] = hash_array\n  return embedding\n\ndef identify_closest_target(entity):\n  realm = entity.realm\n  radius = realm.config.PLAYER_VISION_RADIUS\n  visible_entities = Entity.Query.window(\n    realm.datastore, entity.pos[0], entity.pos[1], radius)\n  dist = linf(visible_entities[:,EntityAttr[\"row\"]:EntityAttr[\"col\"]+1], entity.pos)\n  entity_ids = visible_entities[:,EntityAttr[\"id\"]]\n\n  # Filter out the entities that are not attackable\n  flt_idx = visible_entities[:,EntityAttr[\"npc_type\"]] >= 0  # no immortal (-1)\n  if entity.config.NPC_SYSTEM_ENABLED and not entity.config.NPC_ALLOW_ATTACK_OTHER_NPCS:\n    flt_idx &= entity_ids > 0\n  dist = dist[flt_idx]\n  entity_ids = entity_ids[flt_idx]\n\n  # TODO: this could be made smarter/faster, or perhaps consider health\n  if len(dist) > 1:\n    closest_idx = np.argmin(dist)\n    return realm.entity(entity_ids[closest_idx])\n  if len(dist) == 1:\n    return realm.entity(entity_ids[0])\n  return None\n"
  },
  {
    "path": "nmmo/lib/vec_noise.py",
    "content": "import numpy as np\n\n# The noise2() was ported from https://github.com/zbenjamin/vec_noise\n\n# https://github.com/zbenjamin/vec_noise/blob/master/_noise.h#L13\nGRAD3 = np.array([\n    [1,1,0], [-1,1,0], [1,-1,0], [-1,-1,0],\n    [1,0,1], [-1,0,1], [1,0,-1], [-1,0,-1],\n    [0,1,1], [0,-1,1], [0,1,-1], [0,-1,-1],\n    [1,0,-1], [-1,0,-1], [0,-1,1], [0,1,1]\n])\n\n# https://github.com/zbenjamin/vec_noise/blob/master/_noise.h#L31\nPERM = np.array([\n    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,\n    36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120,\n    234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,\n    88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,\n    134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133,\n    230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161,\n    1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130,\n    116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250,\n    124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227,\n    47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44,\n    154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98,\n    108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34,\n    242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14,\n    239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121,\n    50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243,\n    141, 128, 195, 78, 66, 215, 61, 156, 180\n], dtype=np.int32)\nPERM = np.concatenate((PERM, PERM))\n\n# 2D simplex skew factors\nF2 = 0.5 * (np.sqrt(3.0) - 1.0)\nG2 = (3.0 - np.sqrt(3.0)) / 6.0\n\n# https://github.com/zbenjamin/vec_noise/blob/master/_simplex.c#L46\ndef snoise2(x, y):\n  \"\"\"Generate 2D simplex noise for given coordinates.\"\"\"\n  s = (x + y) * F2\n  i = np.floor(x + s).astype(int)\n  j = np.floor(y + s).astype(int)\n  t = (i + j) * G2\n\n  x0 = x - (i - t)\n  y0 = y - (j - t)\n\n  # Determine which simplex we're in\n  i1 = (x0 > y0).astype(int)\n  j1 = 1 - i1\n\n  x1 = x0 - i1 + G2\n  y1 = y0 - j1 + G2\n  x2 = x0 - 1 + 2 * G2\n  y2 = y0 - 1 + 2 * G2\n\n  # Hash coordinates of the three simplex corners\n  ii = i & 255\n  jj = j & 255\n  gi0 = PERM[ii + PERM[jj]] % 12\n  gi1 = PERM[ii + i1 + PERM[jj + j1]] % 12\n  gi2 = PERM[ii + 1 + PERM[jj + 1]] % 12\n\n  # Calculate contribution from three corners\n  t0 = 0.5 - x0**2 - y0**2\n  t1 = 0.5 - x1**2 - y1**2\n  t2 = 0.5 - x2**2 - y2**2\n\n  mask0 = (t0 >= 0).astype(float)\n  mask1 = (t1 >= 0).astype(float)\n  mask2 = (t2 >= 0).astype(float)\n\n  n0 = mask0 * t0**4 * (GRAD3[gi0, 0] * x0 + GRAD3[gi0, 1] * y0)\n  n1 = mask1 * t1**4 * (GRAD3[gi1, 0] * x1 + GRAD3[gi1, 1] * y1)\n  n2 = mask2 * t2**4 * (GRAD3[gi2, 0] * x2 + GRAD3[gi2, 1] * y2)\n\n  # Sum up and scale the result\n  return 70 * (n0 + n1 + n2)\n"
  },
  {
    "path": "nmmo/minigames/__init__.py",
    "content": "from .center_race import RacetoCenter, ProgressTowardCenter\nfrom .king_hill import KingoftheHill\nfrom .sandwich import Sandwich\nfrom .comm_together import CommTogether\nfrom .radio_raid import RadioRaid\n\nAVAILABLE_GAMES = [RacetoCenter, KingoftheHill, Sandwich, CommTogether, RadioRaid]\n"
  },
  {
    "path": "nmmo/minigames/center_race.py",
    "content": "# pylint: disable=invalid-name, duplicate-code, unused-argument\nimport time\nfrom nmmo.core.game_api import Game\nfrom nmmo.task import task_api\nfrom nmmo.task.base_predicates import ProgressTowardCenter\nfrom nmmo.lib import utils\n\n\nclass RacetoCenter(Game):\n  required_systems = [\"TERRAIN\", \"RESOURCE\"]\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n\n    self._map_size = 40  # determines the difficulty\n    self.adaptive_difficulty = True\n    self.num_game_won = 1  # at the same map size, threshold to increase the difficulty\n    self.step_size = 8\n    self.num_player_resurrect = 0\n\n    # NOTE: This is a hacky way to get a hash embedding for a function\n    # TODO: Can we get more meaningful embedding? coding LLMs are good but huge\n    self.task_embedding = utils.get_hash_embedding(ProgressTowardCenter,\n                                                   self.config.TASK_EMBED_DIM)\n\n  @property\n  def map_size(self):\n    return self._map_size\n\n  def set_map_size(self, map_size):\n    self._map_size = map_size\n\n  def is_compatible(self):\n    return self.config.are_systems_enabled(self.required_systems)\n\n  def reset(self, np_random, map_dict, tasks=None):\n    assert self.map_size >= self.config.PLAYER_N//4,\\\n      f\"self.map_size({self.map_size}) must be >= {self.config.PLAYER_N//4}\"\n    map_dict[\"mark_center\"] = True  # mark the center tile\n    super().reset(np_random, map_dict)\n    self.history[-1][\"map_size\"] = self.map_size\n    self.num_player_resurrect = 0\n\n  def _set_config(self):\n    self.config.reset()\n    self.config.toggle_systems(self.required_systems)\n    self.config.set_for_episode(\"ALLOW_MOVE_INTO_OCCUPIED_TILE\", False)\n\n    # Regenerate the map from fractal to have less obstacles\n    self.config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    self.config.set_for_episode(\"TERRAIN_WATER\", 0.05)\n    self.config.set_for_episode(\"TERRAIN_FOILAGE\", 0.95)  # prop of stone tiles: 0.05\n    self.config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", True)\n\n    # Activate death fog\n    self.config.set_for_episode(\"DEATH_FOG_ONSET\", None)  # 32\n    # self.config.set_for_episode(\"DEATH_FOG_SPEED\", 1/6)\n    # # Only the center tile is safe\n    # self.config.set_for_episode(\"DEATH_FOG_FINAL_SIZE\", 0)\n\n    self._determine_difficulty()  # sets the map_size\n    self.config.set_for_episode(\"MAP_CENTER\", self.map_size)\n\n  def _determine_difficulty(self):\n    # Determine the difficulty (the map size) based on the previous results\n    if self.adaptive_difficulty and self.history \\\n       and self.history[-1][\"result\"]:  # the last game was won\n      last_results = [r[\"result\"] for r in self.history if r[\"map_size\"] == self.map_size]\n      if sum(last_results) >= self.num_game_won \\\n        and self.map_size <= self.config.original[\"MAP_CENTER\"] - self.step_size:\n        self._map_size += self.step_size\n\n  def _set_realm(self, map_dict):\n    # NOTE: this game respawns dead players at the edge, so setting delete_dead_entity=False\n    self.realm.reset(self._np_random, map_dict, delete_dead_player=False)\n\n  def _define_tasks(self):\n    return task_api.make_same_task(ProgressTowardCenter, self.config.POSSIBLE_AGENTS,\n                                   task_kwargs={\"embedding\": self.task_embedding})\n\n  def _process_dead_players(self, terminated, dead_players):\n    # Respawn dead players at the edge\n    for player in dead_players.values():\n      player.resurrect(freeze_duration=10, health_prop=1, edge_spawn=True)\n      self.num_player_resurrect += 1\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      time_limit = self.config.HORIZON\n      return (time_limit - self.realm.tick) / time_limit  # speed bonus\n    # No one reached the center\n    return 0.0\n\n  def _check_winners(self, terminated):\n    return self._who_completed_task()\n\n  @staticmethod\n  def test(env, horizon=30, seed=0):\n    game = RacetoCenter(env)\n    env.reset(game=game, seed=seed)\n\n    # Check configs\n    config = env.config\n    assert config.are_systems_enabled(game.required_systems)\n    assert config.COMBAT_SYSTEM_ENABLED is False\n    assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False\n\n    start_time = time.time()\n    for _ in range(horizon):\n      _, r, terminated, _, _ = env.step({})\n    print(f\"Time taken: {time.time() - start_time:.3f} s\")  # pylint: disable=bad-builtin\n\n    # Test if the difficulty increases\n    org_map_size = game.map_size\n    for result in [False]*7 + [True]*game.num_game_won:\n      game.history.append({\"result\": result, \"map_size\": game.map_size})\n      game._determine_difficulty()  # pylint: disable=protected-access\n    assert game.map_size == (org_map_size + game.step_size)\n\n    # Check if returns of resurrect/frozen players are correct\n    for agent_id, player in env._dead_this_tick.items():  # pylint: disable=protected-access\n      assert player.alive, \"Resurrected players should be alive\"\n      assert player.status.frozen, \"Resurrected players should be frozen\"\n      assert player.my_task.progress == 0, \"Resurrected players should have 0 progress\"\n      assert terminated[agent_id], \"Resurrected players should be done = True\"\n      assert r[agent_id] == -1, \"Resurrected players should have -1 reward\"\n\nif __name__ == \"__main__\":\n  import nmmo\n  test_config = nmmo.config.Default()  # Medium, AllGameSystems\n  test_env = nmmo.Env(test_config)\n  RacetoCenter.test(test_env)  # 0.85 s\n\n  # performance test\n  from tests.testhelpers import profile_env_step\n  test_tasks = task_api.make_same_task(ProgressTowardCenter, test_env.possible_agents)\n  profile_env_step(tasks=test_tasks)\n  # env._compute_rewards(): 1.9577480710031523\n"
  },
  {
    "path": "nmmo/minigames/comm_together.py",
    "content": "# pylint: disable=duplicate-code, invalid-name, unused-argument\nimport time\nfrom nmmo.core.game_api import TeamBattle\nfrom nmmo.task import task_spec\nfrom nmmo.task.base_predicates import AllMembersWithinRange\nfrom nmmo.lib import utils, team_helper\n\n\ndef seek_task(within_dist):\n  return task_spec.TaskSpec(\n    eval_fn=AllMembersWithinRange,\n    eval_fn_kwargs={\"dist\": within_dist},\n    reward_to=\"team\")\n\nclass CommTogether(TeamBattle):\n  _required_systems = [\"TERRAIN\", \"COMMUNICATION\", \"COMBAT\"]\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n\n    # NOTE: all should fit in a 8x8 square, in which all can see each other\n    self.team_within_dist = 7  # gather all team members within this distance\n\n    self._map_size = 128  # determines the difficulty\n    self._spawn_immunity = 128  # so that agents can attack each other later\n    self.adaptive_difficulty = False\n    self.num_game_won = 1  # at the same map size, threshold to increase the difficulty\n    self.step_size = 8\n    self._grass_map = False\n    self.num_player_resurrect = 0\n\n    # NOTE: This is a hacky way to get a hash embedding for a function\n    # TODO: Can we get more meaningful embedding? coding LLMs are good but heavy\n    self.task_embedding = utils.get_hash_embedding(seek_task, self.config.TASK_EMBED_DIM)\n\n  @property\n  def required_systems(self):\n    return self._required_systems\n\n  @property\n  def map_size(self):\n    return self._map_size\n\n  def set_map_size(self, map_size):\n    self._map_size = map_size\n\n  def set_spawn_immunity(self, spawn_immunity):\n    self._spawn_immunity = spawn_immunity\n\n  def set_grass_map(self, grass_map):\n    self._grass_map = grass_map\n\n  def is_compatible(self):\n    return self.config.are_systems_enabled(self.required_systems)\n\n  def reset(self, np_random, map_dict, tasks=None):\n    super().reset(np_random, map_dict)\n    self.history[-1][\"map_size\"] = self.map_size\n    self._grass_map = False  # reset to default\n    self.num_player_resurrect = 0\n\n  def _set_config(self):\n    self.config.reset()\n    self.config.toggle_systems(self.required_systems)\n    self.config.set_for_episode(\"ALLOW_MOVE_INTO_OCCUPIED_TILE\", False)\n    # Regenerate the map from fractal to have less obstacles\n    self.config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    self.config.set_for_episode(\"TERRAIN_WATER\", 0.1)\n    self.config.set_for_episode(\"TERRAIN_FOILAGE\", 0.9)\n    self.config.set_for_episode(\"TERRAIN_RESET_TO_GRASS\", self._grass_map)\n    # NO death fog\n    self.config.set_for_episode(\"DEATH_FOG_ONSET\", None)\n    # Enable +10 hp per tick, so that getting hit once doesn't damage the agent\n    self.config.set_for_episode(\"PLAYER_HEALTH_INCREMENT\", 10)\n\n    self._determine_difficulty()  # sets the map size\n    self.config.set_for_episode(\"MAP_CENTER\", self.map_size)\n    self.config.set_for_episode(\"COMBAT_SPAWN_IMMUNITY\", self._spawn_immunity)\n\n  def _determine_difficulty(self):\n    # Determine the difficulty (the map size) based on the previous results\n    if self.adaptive_difficulty and self.history \\\n       and self.history[-1][\"result\"]:  # the last game was won\n      last_results = [r[\"result\"] for r in self.history if r[\"map_size\"] == self.map_size]\n      if sum(last_results) >= self.num_game_won:\n        self._map_size = min(self.map_size + self.step_size,\n                             self.config.original[\"MAP_CENTER\"])\n        # # Decrease the spawn immunity, to increase attack window\n        # if self._spawn_immunity > self.history[-1][\"winning_tick\"]:\n        #   next_immunity = (self._spawn_immunity + self.history[-1][\"winning_tick\"]) / 2\n        #   self._spawn_immunity = max(next_immunity, 64)  # 64 is the minimum\n\n  def _set_realm(self, map_dict):\n    # NOTE: this game respawns dead players at the edge, so setting delete_dead_entity=False\n    self.realm.reset(self._np_random, map_dict, delete_dead_player=False)\n\n  def _define_tasks(self):\n    spec_list = [seek_task(self.team_within_dist)] * len(self.teams)\n    return task_spec.make_task_from_spec(self.teams, spec_list)\n\n  def _process_dead_players(self, terminated, dead_players):\n    # Respawn dead players at a random location\n    for player in dead_players.values():\n      player.resurrect(freeze_duration=30, health_prop=1, edge_spawn=False)\n      self.num_player_resurrect += 1\n\n  def _check_winners(self, terminated):\n    # No winner game is possible\n    return self._who_completed_task()\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      time_limit = self.config.HORIZON\n      speed_bonus = (time_limit - self.realm.tick) / time_limit\n      return speed_bonus\n    return 0.0\n\n  @staticmethod\n  def test(env, horizon=30, seed=0):\n    # pylint: disable=protected-access\n    game = CommTogether(env)\n    env.reset(game=game, seed=seed)\n\n    # Check configs\n    config = env.config\n    assert config.are_systems_enabled(game.required_systems)\n    assert config.DEATH_FOG_ONSET is None\n    assert config.ITEM_SYSTEM_ENABLED is False\n    assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False\n\n    start_time = time.time()\n    for _ in range(horizon):\n      env.step({})\n    print(f\"Time taken: {time.time() - start_time:.3f} s\")  # pylint: disable=bad-builtin\n\n    # These should run without errors\n    game.history.append({\"result\": False, \"map_size\": 0, \"winning_tick\": 512})\n    game._determine_difficulty()\n    game.history.append({\"result\": True, \"winners\": None, \"map_size\": 0, \"winning_tick\": 512})\n    game._determine_difficulty()\n\n    # Test if the difficulty changes\n    org_map_size = game.map_size\n    for result in [False]*7 + [True]*game.num_game_won:\n      game.history.append({\"result\": result, \"map_size\": game.map_size, \"winning_tick\": 128})\n      game._determine_difficulty()\n    if game.adaptive_difficulty:\n      assert game.map_size == (org_map_size + game.step_size)\n\nif __name__ == \"__main__\":\n  import nmmo\n  test_config = nmmo.config.Default()  # Medium, AllGameSystems\n  teams = team_helper.make_teams(test_config, num_teams=7)\n  test_config.set(\"TEAMS\", teams)\n  test_env = nmmo.Env(test_config)\n  CommTogether.test(test_env)  # 0.65 s\n\n  # performance test\n  from tests.testhelpers import profile_env_step\n  test_tasks = task_spec.make_task_from_spec(teams, [seek_task(5)]*len(teams))\n  profile_env_step(tasks=test_tasks)\n  # env._compute_rewards(): 0.27938533399719745\n"
  },
  {
    "path": "nmmo/minigames/king_hill.py",
    "content": "# pylint: disable=invalid-name, duplicate-code, unused-argument\nimport time\nfrom nmmo.core.game_api import TeamBattle\nfrom nmmo.task import task_spec, base_predicates\nfrom nmmo.lib import utils, team_helper\n\n\ndef seize_task(dur_to_win):\n  return task_spec.TaskSpec(\n    eval_fn=base_predicates.SeizeCenter,\n    eval_fn_kwargs={\"num_ticks\": dur_to_win},\n    reward_to=\"team\")\n\nclass KingoftheHill(TeamBattle):\n  required_systems = [\"TERRAIN\", \"COMBAT\", \"RESOURCE\", \"COMMUNICATION\"]\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n\n    self._seize_duration = 10  # determines the difficulty\n    self.dur_step_size = 10\n    self.max_seize_duration = 200\n    self.adaptive_difficulty = True\n    self.num_game_won = 2  # at the same duration, threshold to increase the difficulty\n    self.map_size = 40\n    self.score_scaler = .5\n\n    # NOTE: This is a hacky way to get a hash embedding for a function\n    # TODO: Can we get more meaningful embedding? coding LLMs are good but huge\n    self.task_embedding = utils.get_hash_embedding(seize_task,\n                                                   self.config.TASK_EMBED_DIM)\n\n  @property\n  def seize_duration(self):\n    return self._seize_duration\n\n  def set_seize_duration(self, seize_duration):\n    self._seize_duration = seize_duration\n\n  def is_compatible(self):\n    return self.config.are_systems_enabled(self.required_systems)\n\n  def reset(self, np_random, map_dict, tasks=None):\n    super().reset(np_random, map_dict)\n    self.history[-1][\"map_size\"] = self.map_size\n    self.history[-1][\"seize_duration\"] = self.seize_duration\n\n  def _set_config(self):\n    self.config.reset()\n    self.config.toggle_systems(self.required_systems)\n    self.config.set_for_episode(\"MAP_CENTER\", self.map_size)\n    self.config.set_for_episode(\"ALLOW_MOVE_INTO_OCCUPIED_TILE\", False)\n\n    # Regenerate the map from fractal to have less obstacles\n    self.config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    self.config.set_for_episode(\"TERRAIN_WATER\", 0.05)\n    self.config.set_for_episode(\"TERRAIN_FOILAGE\", 0.95)  # prop of stone tiles: 0.05\n    self.config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", True)\n\n    # Activate death fog\n    self.config.set_for_episode(\"DEATH_FOG_ONSET\", 32)\n    self.config.set_for_episode(\"DEATH_FOG_SPEED\", 1/16)\n    self.config.set_for_episode(\"DEATH_FOG_FINAL_SIZE\", 5)\n\n    self._determine_difficulty()  # sets the seize duration\n\n  def _determine_difficulty(self):\n    # Determine the difficulty (the seize duration) based on the previous results\n    if self.adaptive_difficulty and self.history \\\n       and self.history[-1][\"result\"]:  # the last game was won\n      last_results = [r[\"result\"] for r in self.history\n                      if r[\"seize_duration\"] == self.seize_duration]\n      if sum(last_results) >= self.num_game_won:\n        self._seize_duration = min(self.seize_duration + self.dur_step_size,\n                                   self.max_seize_duration)\n\n  def _set_realm(self, map_dict):\n    self.realm.reset(self._np_random, map_dict, custom_spawn=True, seize_targets=[\"center\"])\n    # team spawn requires custom spawning\n    team_loader = team_helper.TeamLoader(self.config, self._np_random)\n    self.realm.players.spawn(team_loader)\n\n  def _define_tasks(self):\n    spec_list = [seize_task(self.seize_duration)] * len(self.teams)\n    return task_spec.make_task_from_spec(self.teams, spec_list)\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      time_limit = self.config.HORIZON\n      speed_bonus = (time_limit - self.realm.tick) / time_limit\n      alive_bonus = sum(1.0 for agent_id in self._winners if agent_id in self.realm.players)\\\n                    / len(self._winners)\n      return (speed_bonus + alive_bonus) / 2  # set max to 1.0\n    # No one succeeded\n    return 0.0\n\n  def _check_winners(self, terminated):\n    assert self.config.TEAMS is not None, \"Team battle mode requires TEAMS to be defined\"\n    winners = self._who_completed_task()\n    if winners is not None:\n      return winners\n\n    if len(self.realm.seize_status) == 0:\n      return None\n\n    seize_results = list(self.realm.seize_status.values())\n\n    # Time's up, and a team has seized the center\n    if self.realm.tick == self.config.HORIZON:\n      winners = []\n      # Declare the latest seizing agent as the winner\n      for agent_id, _ in seize_results:\n        for task in self.tasks:\n          if agent_id in task.assignee:\n            winners += task.assignee\n      return winners or None\n\n    # Only one team remains and they have seized the center\n    current_teams = self._check_remaining_teams()\n    if len(current_teams) == 1:\n      winning_team = list(current_teams.keys())[0]\n      team_members = self.config.TEAMS[winning_team]\n      for agent_id, _ in seize_results:\n        # Check if the agent is in the winning team\n        if agent_id in team_members:\n          return team_members\n\n    # No team has seized the center\n    return None\n\n  @staticmethod\n  def test(env, horizon=30, seed=0):\n    game = KingoftheHill(env)\n    env.reset(game=game, seed=seed)\n\n    # Check configs\n    config = env.config\n    assert config.are_systems_enabled(game.required_systems)\n    assert config.TERRAIN_SYSTEM_ENABLED is True\n    assert config.RESOURCE_SYSTEM_ENABLED is True\n    assert config.COMBAT_SYSTEM_ENABLED is True\n    assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False\n    assert config.DEATH_FOG_ONSET == 32\n    assert env.realm.map.seize_targets == [(config.MAP_SIZE//2, config.MAP_SIZE//2)]\n\n    start_time = time.time()\n    for _ in range(horizon):\n      env.step({})\n    print(f\"Time taken: {time.time() - start_time:.3f} s\")  # pylint: disable=bad-builtin\n\n    # Test if the difficulty increases\n    org_seize_dur = game.seize_duration\n    for result in [False]*7 + [True]*game.num_game_won:\n      game.history.append({\"result\": result, \"seize_duration\": game.seize_duration})\n      game._determine_difficulty()  # pylint: disable=protected-access\n    assert game.seize_duration == (org_seize_dur + game.dur_step_size)\n\nif __name__ == \"__main__\":\n  import nmmo\n  test_config = nmmo.config.Default()  # Medium, AllGameSystems\n  test_config.set(\"TEAMS\", team_helper.make_teams(test_config, num_teams=7))\n  test_env = nmmo.Env(test_config)\n  KingoftheHill.test(test_env)  # 0.59 s\n\n  # performance test\n  from tests.testhelpers import profile_env_step\n  teams = test_config.TEAMS\n  test_tasks = task_spec.make_task_from_spec(teams, [seize_task(30)]*len(teams))\n  profile_env_step(tasks=test_tasks)\n  # env._compute_rewards(): 0.24291237899888074\n"
  },
  {
    "path": "nmmo/minigames/radio_raid.py",
    "content": "# pylint: disable=duplicate-code, invalid-name, unused-argument\nimport time\nfrom nmmo.core.game_api import TeamBattle\nfrom nmmo.task import task_spec\nfrom nmmo.task.base_predicates import DefeatEntity\nfrom nmmo.lib import utils, team_helper\n\n\ndef hunt_task(num_npc):\n  return task_spec.TaskSpec(\n    eval_fn=DefeatEntity,\n    eval_fn_kwargs={\"agent_type\": \"npc\", \"level\": 0, \"num_agent\": num_npc},\n    reward_to=\"team\")\n\nclass RadioRaid(TeamBattle):\n  required_systems = [\"TERRAIN\", \"COMBAT\", \"COMMUNICATION\", \"NPC\"]\n  num_teams = 8\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n\n    self._goal_num_npc = 5  # determines the difficulty\n    self.adaptive_difficulty = True\n    self.num_game_won = 2  # at the same map size, threshold to increase the difficulty\n    self.step_size = 5\n    self.quad_centers = None\n    self._grass_map = False\n\n    # npc danger: 0=all npc are passive, 1=all npc are aggressive\n    self._npc_danger = 0  # increase by .1 per wave\n    self._danger_step_size = .1\n    self._spawn_center_crit = 0.4  # if danger is less than crit, spawn at center\n    self.npc_wave_num = 10  # number of npc to spawn per wave\n    self._last_wave_tick = 0\n    self.npc_spawn_crit = 3\n    self.npc_spawn_radius = 5\n    self.max_wave_interval = 20\n\n    # These will probably affect the difficulty\n    self.map_size = 48\n    self.spawn_immunity = self.config.HORIZON\n\n    # NOTE: This is a hacky way to get a hash embedding for a function\n    # TODO: Can we get more meaningful embedding? coding LLMs are good but heavy\n    self.task_embedding = utils.get_hash_embedding(hunt_task, self.config.TASK_EMBED_DIM)\n\n  @property\n  def teams(self):\n    team_size = self.config.PLAYER_N // self.num_teams\n    teams = {i: list(range((i-1)*team_size+1, i*team_size+1))\n             for i in range(1, self.num_teams)}\n    teams[self.num_teams] = \\\n      list(range((self.num_teams-1)*team_size+1, self.config.PLAYER_N+1))\n    return teams\n\n  @property\n  def goal_num_npc(self):\n    return self._goal_num_npc\n\n  def set_goal_num_npc(self, goal_num_npc):\n    self._goal_num_npc = goal_num_npc\n\n  def set_grass_map(self, grass_map):\n    self._grass_map = grass_map\n\n  def is_compatible(self):\n    return self.config.are_systems_enabled(self.required_systems)\n\n  def reset(self, np_random, map_dict, tasks=None):\n    super().reset(np_random, map_dict)\n    self.history[-1][\"goal_num_npc\"] = self.goal_num_npc\n    self._npc_danger = 0\n    self._last_wave_tick = 0\n\n  def _set_config(self):\n    self.config.reset()\n    self.config.toggle_systems(self.required_systems)\n    self.config.set_for_episode(\"MAP_CENTER\", self.map_size)\n    self.config.set_for_episode(\"COMBAT_SPAWN_IMMUNITY\", self.spawn_immunity)\n    self.config.set_for_episode(\"ALLOW_MOVE_INTO_OCCUPIED_TILE\", False)\n    self.config.set_for_episode(\"TEAMS\", self.teams)\n    self.config.set_for_episode(\"NPC_DEFAULT_REFILL_DEAD_NPCS\", False)\n    # Regenerate the map from fractal to have less obstacles\n    self.config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    self.config.set_for_episode(\"TERRAIN_WATER\", 0.1)\n    self.config.set_for_episode(\"TERRAIN_FOILAGE\", 0.95)\n    self.config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", False)\n    self.config.set_for_episode(\"TERRAIN_RESET_TO_GRASS\", self._grass_map)\n    # NO death fog\n    self.config.set_for_episode(\"DEATH_FOG_ONSET\", None)\n    # Enable +1 hp per tick -- restore health by eat/drink\n    self.config.set_for_episode(\"PLAYER_HEALTH_INCREMENT\", 1)\n    # Make NPCs more aggressive\n    self.config.set_for_episode(\"NPC_SPAWN_NEUTRAL\", 0.3)\n    self.config.set_for_episode(\"NPC_SPAWN_AGGRESSIVE\", 0.8)\n\n    self._determine_difficulty()  # sets the goal_num_npc\n\n  def _determine_difficulty(self):\n    # Determine the difficulty (the map size) based on the previous results\n    if self.adaptive_difficulty and self.history \\\n       and self.history[-1][\"result\"]:  # the last game was won\n      last_results = [r[\"result\"] for r in self.history if r[\"goal_num_npc\"] == self.goal_num_npc]\n      if sum(last_results) >= self.num_game_won:\n        self._goal_num_npc = self._goal_num_npc + self.step_size\n\n  def _set_realm(self, map_dict):\n    self.realm.reset(self._np_random, map_dict, custom_spawn=True)\n    # team spawn requires custom spawning\n    team_loader = team_helper.TeamLoader(self.config, self._np_random)\n    self.realm.players.spawn(team_loader)\n\n    # from each team, pick 4 agents and place on each quad center as recons\n    self.quad_centers = list(self.realm.map.quad_centers.values())\n    for members in self.teams.values():\n      recons = self._np_random.choice(members, size=4, replace=False)\n      for idx, agent_id in enumerate(recons):\n        self.realm.players[agent_id].make_recon(new_pos=self.quad_centers[idx])\n\n  def _define_tasks(self):\n    spec_list = [hunt_task(self.goal_num_npc)] * len(self.teams)\n    return task_spec.make_task_from_spec(self.teams, spec_list)\n\n  def _process_dead_npcs(self, dead_npcs):\n    npc_manager = self.realm.npcs\n    diff_player_npc = (self.realm.num_players - self.num_teams*4) - len(npc_manager)\n    # Spawn more NPCs if there are more players than NPCs\n    # If the gap is large, spawn in waves\n    # If the gap is small, spawn in small batches\n    if diff_player_npc >= 0 and (len(npc_manager) <= self.npc_spawn_crit or \\\n       self.realm.tick - self._last_wave_tick > self.max_wave_interval):\n      if self._npc_danger < self._spawn_center_crit:\n        spawn_pos = self.realm.map.center_coord\n      else:\n        spawn_pos = self._np_random.choice(self.quad_centers)\n      r_min, r_max = spawn_pos[0] - self.npc_spawn_radius, spawn_pos[0] + self.npc_spawn_radius\n      c_min, c_max = spawn_pos[1] - self.npc_spawn_radius, spawn_pos[1] + self.npc_spawn_radius\n      npc_manager.area_spawn(r_min, r_max, c_min, c_max, self.npc_wave_num,\n                             lambda r, c: npc_manager.spawn_npc(r, c, danger=self._npc_danger))\n      self._npc_danger += min(self._danger_step_size, 1)  # max danger = 1\n      self._last_wave_tick = self.realm.tick\n\n  def _check_winners(self, terminated):\n    # No winner game is possible\n    return self._who_completed_task()\n\n  @property\n  def is_over(self):\n    return self.winners is not None or self.realm.tick >= self.config.HORIZON or \\\n           self.realm.num_players <= (self.num_teams*4)  # 4 immortal recons per team\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      time_limit = self.config.HORIZON\n      speed_bonus = (time_limit - self.realm.tick) / time_limit\n      alive_bonus = sum(1.0 for agent_id in self._winners if agent_id in self.realm.players)\\\n                    / len(self._winners)\n      return (speed_bonus + alive_bonus) / 2  # set max to 1.0\n    return 0.0\n\n  @staticmethod\n  def test(env, horizon=30, seed=0):\n    game = RadioRaid(env)\n    env.reset(game=game, seed=seed)\n\n    # Check configs\n    config = env.config\n    assert config.are_systems_enabled(game.required_systems)\n    assert config.COMBAT_SYSTEM_ENABLED is True\n    assert config.RESOURCE_SYSTEM_ENABLED is False\n    assert config.COMMUNICATION_SYSTEM_ENABLED is True\n    assert config.ITEM_SYSTEM_ENABLED is False\n    assert config.DEATH_FOG_ONSET is None\n    assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False\n    assert config.NPC_SYSTEM_ENABLED is True\n    assert config.NPC_DEFAULT_REFILL_DEAD_NPCS is False\n\n    start_time = time.time()\n    for _ in range(horizon):\n      env.step({})\n    print(f\"Time taken: {time.time() - start_time:.3f} s\")  # pylint: disable=bad-builtin\n\n    # pylint: disable=protected-access\n    # These should run without errors\n    game.history.append({\"result\": False, \"goal_num_npc\": game.goal_num_npc})\n    game._determine_difficulty()\n\n    # Test if the difficulty changes\n    org_goal_npc = game.goal_num_npc\n    for result in [False]*7 + [True]*game.num_game_won:\n      game.history.append({\"result\": result, \"goal_num_npc\": game.goal_num_npc})\n      game._determine_difficulty()  # pylint: disable=protected-access\n    assert game.goal_num_npc == (org_goal_npc + game.step_size)\n\nif __name__ == \"__main__\":\n  import nmmo\n  test_config = nmmo.config.Default()  # Medium, AllGameSystems\n  test_env = nmmo.Env(test_config)\n  RadioRaid.test(test_env)  # 0.60 s\n\n  # performance test\n  from tests.testhelpers import profile_env_step\n  test_tasks = task_spec.make_task_from_spec(test_config.TEAMS,\n                                             [hunt_task(30)]*len(test_config.TEAMS))\n  profile_env_step(tasks=test_tasks)\n  # env._compute_rewards(): 0.17201571099940338\n"
  },
  {
    "path": "nmmo/minigames/sandwich.py",
    "content": "import time\nimport numpy as np\n\nfrom nmmo.core.game_api import TeamBattle, team_survival_task\nfrom nmmo.task import task_spec\nfrom nmmo.lib import team_helper\n\n\ndef secure_order(pos, radius=5):\n  return {\"secure\": {\"position\": pos, \"radius\": radius}}\n\nclass Sandwich(TeamBattle):\n  required_systems = [\"TERRAIN\", \"COMBAT\", \"NPC\", \"COMMUNICATION\"]\n  num_teams = 8\n\n  def __init__(self, env, sampling_weight=None):\n    super().__init__(env, sampling_weight)\n\n    self.map_size = 80\n    self._inner_npc_num = 2  # determines the difficulty\n    self._outer_npc_num = None  # these npcs rally to the center\n    self.npc_step_size = 2\n    self.adaptive_difficulty = True\n    self.num_game_won = 2  # at the same duration, threshold to increase the difficulty\n    self.max_npc_num = self.config.PLAYER_N // self.num_teams\n    self.survival_crit = 500  # to win, agents must survive this long\n    self._grass_map = False\n\n  @property\n  def teams(self):\n    team_size = self.config.PLAYER_N // self.num_teams\n    teams = {i: list(range((i-1)*team_size+1, i*team_size+1))\n             for i in range(1, self.num_teams)}\n    teams[self.num_teams] = \\\n      list(range((self.num_teams-1)*team_size+1, self.config.PLAYER_N+1))\n    return teams\n\n  @property\n  def inner_npc_num(self):\n    return self._inner_npc_num\n\n  def set_inner_npc_num(self, inner_npc_num):\n    self._inner_npc_num = inner_npc_num\n\n  @property\n  def outer_npc_num(self):\n    return self._outer_npc_num or min(self._inner_npc_num*self.num_teams, self.map_size*2)\n\n  def set_outer_npc_num(self, outer_npc_num):\n    self._outer_npc_num = outer_npc_num\n\n  def set_grass_map(self, grass_map):\n    self._grass_map = grass_map\n\n  def is_compatible(self):\n    return self.config.are_systems_enabled(self.required_systems)\n\n  def reset(self, np_random, map_dict, tasks=None):\n    super().reset(np_random, map_dict)\n    self.history[-1][\"inner_npc_num\"] = self.inner_npc_num\n    self.history[-1][\"outer_npc_num\"] = self.outer_npc_num\n    self._grass_map = False  # reset to default\n\n  def _set_config(self):\n    self.config.reset()\n    self.config.toggle_systems(self.required_systems)\n    self.config.set_for_episode(\"TEAMS\", self.teams)\n    self.config.set_for_episode(\"ALLOW_MOVE_INTO_OCCUPIED_TILE\", False)\n    self.config.set_for_episode(\"NPC_DEFAULT_REFILL_DEAD_NPCS\", False)\n    # Make the map small\n    self.config.set_for_episode(\"MAP_CENTER\", self.map_size)\n    # Regenerate the map from fractal to have less obstacles\n    self.config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    self.config.set_for_episode(\"TERRAIN_WATER\", 0.1)\n    self.config.set_for_episode(\"TERRAIN_FOILAGE\", 0.9)\n    self.config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", False)\n    self.config.set_for_episode(\"TERRAIN_RESET_TO_GRASS\", self._grass_map)\n    # Activate death fog from the onset\n    self.config.set_for_episode(\"DEATH_FOG_ONSET\", 1)\n    self.config.set_for_episode(\"DEATH_FOG_SPEED\", 1/10)\n    self.config.set_for_episode(\"DEATH_FOG_FINAL_SIZE\", 5)\n    # Enable +1 hp per tick\n    self.config.set_for_episode(\"PLAYER_HEALTH_INCREMENT\", 1)\n    self._determine_difficulty()  # sets the seize duration\n\n  def _determine_difficulty(self):\n    # Determine the difficulty based on the previous results\n    if self.adaptive_difficulty and self.history \\\n       and self.history[-1][\"result\"]:  # the last game was won\n      last_results = [r[\"result\"] for r in self.history\n                      if r[\"inner_npc_num\"] == self.inner_npc_num]\n      if sum(last_results) >= self.num_game_won:\n        # Increase the npc num, when there were only few npcs left at the end\n        self._inner_npc_num += self.npc_step_size\n        self._inner_npc_num = min(self._inner_npc_num, self.max_npc_num)\n\n  def _generate_spawn_locs(self):\n    center = self.config.MAP_SIZE // 2\n    radius = self.map_size // 4\n    angles = np.linspace(0, 2*np.pi, self.num_teams, endpoint=False)\n    return [(center + int(radius*np.cos(a)), center + int(radius*np.sin(a))) for a in angles]\n\n  def _set_realm(self, map_dict):\n    self.realm.reset(self._np_random, map_dict, custom_spawn=True)\n    # team spawn requires custom spawning\n    spawn_locs = self._generate_spawn_locs()\n    team_loader = team_helper.TeamLoader(self.config, self._np_random, spawn_locs)\n    self.realm.players.spawn(team_loader)\n\n    # spawn NPCs\n    npc_manager = self.realm.npcs\n    center = self.config.MAP_SIZE // 2\n    offset = self.config.MAP_CENTER // 8\n    for i in range(self.num_teams):\n      r, c = spawn_locs[i]\n      if r < center:\n        r_min, r_max = center - offset, center - 1\n      else:\n        r_min, r_max = center + 1, center + offset\n      if c < center:\n        c_min, c_max = center - offset, center - 1\n      else:\n        c_min, c_max = center + 1, center + offset\n      # pylint: disable=cell-var-from-loop\n      npc_manager.area_spawn(r_min, r_max, c_min, c_max, self.inner_npc_num,\n                             lambda r, c: npc_manager.spawn_npc(\n                               r, c, name=f\"NPC{i+1}\", order={\"rally\": spawn_locs[i]}))\n    npc_manager.edge_spawn(self.outer_npc_num,\n                           lambda r, c: npc_manager.spawn_npc(\n                              r, c, name=\"NPC5\", order={\"rally\": (center,center)}))\n\n  def _process_dead_npcs(self, dead_npcs):\n    npc_manager = self.realm.npcs\n    target_num = min(self.realm.num_players, self.inner_npc_num) // 2\n    if len(npc_manager) < target_num:\n      center = self.config.MAP_SIZE // 2\n      offset = self.config.MAP_CENTER // 6\n      r_min = c_min = center - offset\n      r_max = c_max = center + offset\n      num_spawn = target_num - len(npc_manager)\n      npc_manager.area_spawn(r_min, r_max, c_min, c_max, num_spawn,\n                             lambda r, c: npc_manager.spawn_npc(\n                               r, c, name=\"NPC5\", order={\"rally\": (center,center)}))\n\n  @property\n  def winning_score(self):\n    if self._winners:\n      time_limit = self.config.HORIZON\n      speed_bonus = (time_limit - self.realm.tick) / time_limit\n      return speed_bonus  # set max to 1.0\n    # No one succeeded\n    return 0.0\n\n  def _check_winners(self, terminated):\n    # Basic survival criteria\n    if self.realm.tick < self.survival_crit:\n      return None\n    return super()._check_winners(terminated)\n\n  @staticmethod\n  def test(env, horizon=30, seed=0):\n    game = Sandwich(env)\n    env.reset(game=game, seed=seed)\n\n    # Check configs\n    config = env.config\n    assert config.are_systems_enabled(game.required_systems)\n    assert config.TERRAIN_SYSTEM_ENABLED is True\n    assert config.RESOURCE_SYSTEM_ENABLED is False\n    assert config.COMBAT_SYSTEM_ENABLED is True\n    assert config.NPC_SYSTEM_ENABLED is True\n    assert config.NPC_DEFAULT_REFILL_DEAD_NPCS is False\n    assert config.EQUIPMENT_SYSTEM_ENABLED is False  # equipment is used to set npc stats\n    assert config.ALLOW_MOVE_INTO_OCCUPIED_TILE is False\n\n    start_time = time.time()\n    for _ in range(horizon):\n      env.step({})\n    print(f\"Time taken: {time.time() - start_time:.3f} s\")  # pylint: disable=bad-builtin\n\n    # Test if the difficulty increases\n    org_inner_npc_num = game.inner_npc_num\n    for result in [False]*7 + [True]*game.num_game_won:\n      game.history.append(\n        {\"result\": result, \"inner_npc_num\": game.inner_npc_num})\n      game._determine_difficulty()  # pylint: disable=protected-access\n    assert game.inner_npc_num == (org_inner_npc_num + game.npc_step_size)\n\nif __name__ == \"__main__\":\n  import nmmo\n  test_config = nmmo.config.Default()  # Medium, AllGameSystems\n  test_env = nmmo.Env(test_config)\n  Sandwich.test(test_env)  # 0.74 s\n\n  # performance test\n  from tests.testhelpers import profile_env_step\n  test_tasks = task_spec.make_task_from_spec(test_config.TEAMS,\n                                        [team_survival_task(30)]*len(test_config.TEAMS))\n  profile_env_step(tasks=test_tasks)\n  # env._compute_rewards(): 0.1768564050034911\n"
  },
  {
    "path": "nmmo/render/__init__.py",
    "content": ""
  },
  {
    "path": "nmmo/render/overlay.py",
    "content": "import numpy as np\n\nfrom nmmo.lib.colors import Neon\nfrom nmmo.systems import combat\n\nfrom .render_utils import normalize\n\n# pylint: disable=unused-argument\nclass OverlayRegistry:\n  def __init__(self, realm, renderer):\n    '''Manager class for overlays\n\n    Args:\n        config: A Config object\n        realm: An environment\n    '''\n    self.initialized = False\n\n    self.realm  = realm\n    self.config = realm.config\n    self.renderer = renderer\n\n    self.overlays = {\n       #'counts':     Counts, # TODO: change population to team\n       'skills':     Skills}\n\n  def init(self, *args):\n    self.initialized = True\n    for cmd, overlay in self.overlays.items():\n      self.overlays[cmd] = overlay(self.config, self.realm, self.renderer, *args)\n    return self\n\n  def step(self, cmd):\n    '''Per-tick overlay updates\n\n    Args:\n        cmd: User command returned by the client\n    '''\n    if not self.initialized:\n      self.init()\n\n    for overlay in self.overlays.values():\n      overlay.update()\n\n    if cmd in self.overlays:\n      self.overlays[cmd].register()\n\n\nclass Overlay:\n  '''Define a overlay for visualization in the client\n\n  Overlays are color images of the same size as the game map.\n  They are rendered over the environment with transparency and\n  can be used to gain insight about agent behaviors.'''\n  def __init__(self, config, realm, renderer, *args):\n    '''\n    Args:\n        config: A Config object\n        realm: An environment\n    '''\n    self.config     = config\n    self.realm      = realm\n    self.renderer   = renderer\n\n    self.size       = config.MAP_SIZE\n    self.values     = np.zeros((self.size, self.size))\n\n  def update(self):\n    '''Compute per-tick updates to this overlay. Override per overlay.\n\n    Args:\n        obs: Observation returned by the environment\n    '''\n\n  def register(self):\n    '''Compute the overlay and register it within realm. Override per overlay.'''\n\n\nclass Skills(Overlay):\n  def __init__(self, config, realm, renderer, *args):\n    '''Indicates whether agents specialize in foraging or combat'''\n    super().__init__(config, realm, renderer)\n    self.num_skill = 2\n\n    self.values  = np.zeros((self.size, self.size, self.num_skill))\n\n  def update(self):\n    '''Computes a count-based exploration map by painting\n    tiles as agents walk over them'''\n    for agent in self.realm.players.values():\n      r, c = agent.pos\n\n      skill_lvl  = (agent.skills.food.level.val + agent.skills.water.level.val)/2.0\n      combat_lvl = combat.level(agent.skills)\n\n      if skill_lvl == 10 and combat_lvl == 3:\n        continue\n\n      self.values[r, c, 0] = skill_lvl\n      self.values[r, c, 1] = combat_lvl\n\n  def register(self):\n    values = np.zeros((self.size, self.size, self.num_skill))\n    for idx in range(self.num_skill):\n      ary  = self.values[:, :, idx]\n      vals = ary[ary != 0]\n      mean = np.mean(vals)\n      std  = np.std(vals)\n      if std == 0:\n        std = 1\n\n      values[:, :, idx] = (ary - mean) / std\n      values[ary == 0] = 0\n\n    colors    = np.array([Neon.BLUE.rgb, Neon.BLOOD.rgb])\n    colorized = np.zeros((self.size, self.size, 3))\n    amax      = np.argmax(values, -1)\n\n    for idx in range(self.num_skill):\n      colorized[amax == idx] = colors[idx] / 255\n      colorized[values[:, :, idx] == 0] = 0\n\n    self.renderer.register(colorized)\n\n\n# CHECK ME: this was based on population, so disabling it for now\n#   We may want this back for the team-level analysis\nclass Counts(Overlay):\n  def __init__(self, config, realm, renderer, *args):\n    super().__init__(config, realm, renderer)\n    self.values = np.zeros((self.size, self.size, config.PLAYER_POLICIES))\n\n  def update(self):\n    '''Computes a count-based exploration map by painting\n    tiles as agents walk over them'''\n    for ent_id, agent in self.realm.players.items():\n      r, c = agent.pos\n      self.values[r, c][ent_id] += 1\n\n  def register(self):\n    colors    = self.realm.players.palette.colors\n    colors    = np.array([colors[pop].rgb\n                          for pop in range(self.config.PLAYER_POLICIES)])\n\n    colorized = self.values[:, :, :, None] * colors / 255\n    colorized = np.sum(colorized, -2)\n    count_sum  = np.sum(self.values[:, :], -1)\n    data      = normalize(count_sum)[..., None]\n\n    count_sum[count_sum==0] = 1\n    colorized = colorized * data / count_sum[..., None]\n\n    self.renderer.register(colorized)\n"
  },
  {
    "path": "nmmo/render/render_client.py",
    "content": "from __future__ import annotations\nimport numpy as np\n\nfrom nmmo.render.overlay import OverlayRegistry\nfrom nmmo.render.render_utils import patch_packet\n\n\n# Render is external to the game\n# NOTE: WebsocketRenderer has been renamed to DummyRenderer\nclass DummyRenderer:\n  def __init__(self, realm=None) -> None:\n    self._client = None  # websocket.Application(realm)\n    self.overlay_pos = [256, 256]\n\n    self._realm = realm\n\n    self.overlay = None\n    self.registry = OverlayRegistry(realm, renderer=self) if realm else None\n\n    self.packet = None\n\n  def set_realm(self, realm) -> None:\n    self._realm = realm\n    self.registry = OverlayRegistry(realm, renderer=self) if realm else None\n\n  def render_packet(self, packet) -> None:\n    packet = {\n      'pos': self.overlay_pos,\n      'wilderness': 0, # obsolete, but maintained for compatibility\n      **packet }\n\n    self.overlay_pos, _ = self._client.update(packet)\n\n  def render_realm(self) -> None:\n    assert self._realm is not None, 'This function requires a realm'\n    assert self._realm.tick is not None, 'render before reset'\n\n    packet = {\n      'config': self._realm.config,\n      'pos': self.overlay_pos,\n      'wilderness': 0,\n      **self._realm.packet()\n    }\n\n    # TODO: a hack to make the client work\n    packet = patch_packet(packet, self._realm)\n\n    if self.overlay is not None:\n      packet['overlay'] = self.overlay\n      self.overlay = None\n\n    # save the packet for investigation\n    self.packet = packet\n\n    # pass the packet to renderer\n    pos, cmd = None, None  # self._client.update(self.packet)\n\n    # NOTE: copy pasted from nmmo/render/websocket.py\n    #   def update(self, packet):\n    #     self.tick += 1\n    #     uptime = np.round(self.tickRate*self.tick, 1)\n    #     delta = time.time() - self.time\n    #     print('Wall Clock: ', str(delta)[:5], 'Uptime: ', uptime, ', Tick: ', self.tick)\n    #     delta = self.tickRate - delta\n    #     if delta > 0:\n    #       time.sleep(delta)\n    #     self.time = time.time()\n    #     for client in self.clients:\n    #       client.sendUpdate(packet)\n    #       if client.pos is not None:\n    #         self.pos = client.pos\n    #         self.cmd = client.cmd\n    #     return self.pos, self.cmd\n\n    self.overlay_pos = pos\n    self.registry.step(cmd)\n\n  def register(self, overlay: np.ndarray) -> None:\n    '''Register an overlay to be sent to the client\n\n    The intended use of this function is: User types overlay ->\n    client sends cmd to server -> server computes overlay update ->\n    register(overlay) -> overlay is sent to client -> overlay rendered\n\n    Args:\n        overlay: A map-sized (self.size) array of floating point values\n        overlay must be a numpy array of dimension (*(env.size), 3)\n    '''\n    self.overlay = overlay.tolist()\n"
  },
  {
    "path": "nmmo/render/render_utils.py",
    "content": "import numpy as np\nfrom scipy import signal\n\nfrom nmmo.lib.colors import Neon\n\n# NOTE: added to fix json.dumps() cannot serialize numpy objects\n# pylint: disable=inconsistent-return-statements\ndef np_encoder(obj):\n  if isinstance(obj, np.generic):\n    return obj.item()\n\ndef normalize(ary: np.ndarray, norm_std=2):\n  R, C         = ary.shape\n  preprocessed = np.zeros_like(ary)\n  nonzero      = ary[ary!= 0]\n  mean         = np.mean(nonzero)\n  std          = np.std(nonzero)\n  if std == 0:\n    std = 1\n  for r in range(R):\n    for c in range(C):\n      val = ary[r, c]\n      if val != 0:\n        val = (val - mean) / (norm_std * std)\n        val = np.clip(val+1, 0, 2)/2\n        preprocessed[r, c] = val\n  return preprocessed\n\ndef clip(ary: np.ndarray):\n  R, C         = ary.shape\n  preprocessed = np.zeros_like(ary)\n  nonzero      = ary[ary!= 0]\n  mmin         = np.min(nonzero)\n  mmag         = np.max(nonzero) - mmin\n  for r in range(R):\n    for c in range(C):\n      val = ary[r, c]\n      val = (val - mmin) / mmag\n      preprocessed[r, c] = val\n  return preprocessed\n\ndef make_two_tone(ary, norm_std=2, preprocess='norm', invert=False, periods=1):\n  if preprocess == 'norm':\n    ary   = normalize(ary, norm_std)\n  elif preprocess == 'clip':\n    ary   = clip(ary)\n\n  # if preprocess not in ['norm', 'clip'], assume no preprocessing\n  R, C      = ary.shape\n\n  colorized = np.zeros((R, C, 3))\n  if periods != 1:\n    ary = np.abs(signal.sawtooth(periods*3.14159*ary))\n  if invert:\n    colorized[:, :, 0] = ary\n    colorized[:, :, 1] = 1-ary\n  else:\n    colorized[:, :, 0] = 1-ary\n    colorized[:, :, 1] = ary\n\n  colorized *= (ary != 0)[:, :, None]\n\n  return colorized\n\n# TODO: this is a hack to make the client work\n#   by adding color, population, self to the packet\n#   integrating with team helper could make this neat\ndef patch_packet(packet, realm):\n  for ent_id in packet['player']:\n    packet['player'][ent_id]['base']['color'] = Neon.GREEN.packet()\n    # EntityAttr: population was changed to npc_type\n    packet['player'][ent_id]['base']['population'] = 0\n    # old code: nmmo.Serialized.Entity.Self, no longer being used\n    packet['player'][ent_id]['base']['self'] = 1\n\n  npc_colors = {\n    1: Neon.YELLOW.packet(), # passive npcs\n    2: Neon.MAGENTA.packet(), # neutral npcs\n    3: Neon.BLOOD.packet() } # aggressive npcs\n  for ent_id in packet['npc']:\n    npc = realm.npcs.corporeal[ent_id]\n    packet['npc'][ent_id]['base']['color'] = npc_colors[int(npc.npc_type.val)]\n    packet['npc'][ent_id]['base']['population'] = -int(npc.npc_type.val) # note negative\n    packet['npc'][ent_id]['base']['self'] = 1\n\n  return packet\n"
  },
  {
    "path": "nmmo/systems/__init__.py",
    "content": "from .skill import Skill\n"
  },
  {
    "path": "nmmo/systems/combat.py",
    "content": "#Various utilities for managing combat, including hit/damage\n\nimport numpy as np\n\nfrom nmmo.systems import skill as Skill\nfrom nmmo.lib.event_code import EventCode\n\ndef level(skills):\n  return max(e.level.val for e in skills.skills)\n\ndef damage_multiplier(config, skill, targ):\n  skills = [targ.skills.melee, targ.skills.range, targ.skills.mage]\n  exp    = [s.exp for s in skills]\n\n  if max(exp) == min(exp):\n    return 1.0\n\n  idx    = np.argmax([exp])\n  targ   = skills[idx]\n\n  if isinstance(skill, targ.weakness):\n    return config.COMBAT_WEAKNESS_MULTIPLIER\n\n  return 1.0\n\n# pylint: disable=unnecessary-lambda-assignment\ndef attack(realm, attacker, target, skill_fn):\n  config       = attacker.config\n  skill        = skill_fn(attacker)\n  skill_type   = type(skill)\n  skill_name   = skill_type.__name__\n\n  # Per-style offense/defense\n  level_damage = 0\n  if skill_type == Skill.Melee:\n    base_damage  = config.COMBAT_MELEE_DAMAGE\n\n    if config.PROGRESSION_SYSTEM_ENABLED:\n      base_damage  = config.PROGRESSION_MELEE_BASE_DAMAGE\n      level_damage = config.PROGRESSION_MELEE_LEVEL_DAMAGE\n\n    offense_fn   = lambda e: e.melee_attack\n    defense_fn   = lambda e: e.melee_defense\n\n  elif skill_type == Skill.Range:\n    base_damage  = config.COMBAT_RANGE_DAMAGE\n\n    if config.PROGRESSION_SYSTEM_ENABLED:\n      base_damage  = config.PROGRESSION_RANGE_BASE_DAMAGE\n      level_damage = config.PROGRESSION_RANGE_LEVEL_DAMAGE\n\n    offense_fn   = lambda e: e.range_attack\n    defense_fn   = lambda e: e.range_defense\n\n  elif skill_type == Skill.Mage:\n    base_damage  = config.COMBAT_MAGE_DAMAGE\n\n    if config.PROGRESSION_SYSTEM_ENABLED:\n      base_damage  = config.PROGRESSION_MAGE_BASE_DAMAGE\n      level_damage = config.PROGRESSION_MAGE_LEVEL_DAMAGE\n\n    offense_fn   = lambda e: e.mage_attack\n    defense_fn   = lambda e: e.mage_defense\n\n  elif __debug__:\n    assert False, 'Attack skill must be Melee, Range, or Mage'\n\n  # Compute modifiers\n  multiplier        = damage_multiplier(config, skill, target)\n\n  # NOTE: skill offense and defense are only for agents, NOT npcs\n  skill_offense = base_damage\n  if attacker.is_player:\n    skill_offense += level_damage * skill.level.val\n  if attacker.is_npc and config.EQUIPMENT_SYSTEM_ENABLED:\n    # NOTE: In this case, npc off/def is set only with equipment. Revisit this.\n    skill_offense = 0\n\n  if config.PROGRESSION_SYSTEM_ENABLED and target.is_player:\n    skill_defense = config.PROGRESSION_BASE_DEFENSE  + \\\n      config.PROGRESSION_LEVEL_DEFENSE*level(target.skills)\n  else:\n    skill_defense = 0\n\n  if config.EQUIPMENT_SYSTEM_ENABLED:\n    equipment_offense = attacker.equipment.total(offense_fn)\n    equipment_defense = target.equipment.total(defense_fn)\n\n    # after tallying ammo damage, consume ammo (i.e., fire) when the skill type matches\n    ammunition = attacker.equipment.ammunition.item\n    if ammunition is not None and getattr(ammunition, skill_name.lower() + '_attack').val > 0:\n      ammunition.fire(attacker)\n\n  else:\n    equipment_offense = 0\n    equipment_defense = 0\n\n  # Total damage calculation\n  offense = skill_offense + equipment_offense\n  defense = skill_defense + equipment_defense\n  min_damage_prop = config.COMBAT_MINIMUM_DAMAGE_PROPORTION\n  damage  = config.COMBAT_DAMAGE_FORMULA(offense, defense, multiplier, min_damage_prop)\n  damage  = max(int(damage), 0)\n\n  if attacker.is_player:\n    realm.event_log.record(EventCode.SCORE_HIT, attacker, target=target,\n                           combat_style=skill_type, damage=damage)\n\n  attacker.apply_damage(damage, skill.__class__.__name__.lower())\n  target.receive_damage(attacker, damage)\n\n  return damage\n\n\ndef danger(config, pos):\n  border = config.MAP_BORDER\n  center = config.MAP_CENTER\n  r, c   = pos\n\n  #Distance from border\n  r_dist  = min(r - border, center + border - r - 1)\n  c_dist  = min(c - border, center + border - c - 1)\n  dist   = min(r_dist, c_dist)\n  norm   = 2 * dist / center\n\n  return norm\n\ndef spawn(config, dnger, np_random):\n  border = config.MAP_BORDER\n  center = config.MAP_CENTER\n  mid    = center // 2\n\n  dist       = dnger * center / 2\n  max_offset = mid - dist\n  offset     = mid + border + np_random.integers(-max_offset, max_offset)\n\n  rng = np_random.random()\n  if rng < 0.25:\n    r = border + dist\n    c = offset\n  elif rng < 0.5:\n    r = border + center - dist - 1\n    c = offset\n  elif rng < 0.75:\n    c = border + dist\n    r = offset\n  else:\n    c = border + center - dist - 1\n    r = offset\n\n  if __debug__:\n    assert dnger == danger(config, (r,c)), 'Agent spawned at incorrect radius'\n\n  r = int(r)\n  c = int(c)\n\n  return r, c\n"
  },
  {
    "path": "nmmo/systems/droptable.py",
    "content": "class Fixed():\n  def __init__(self, item):\n    self.item = item\n\n  def roll(self, realm, level):\n    return [self.item(realm, level)]\n\nclass Drop:\n  def __init__(self, item, prob):\n    self.item = item\n    self.prob = prob\n\n  def roll(self, realm, level):\n    # TODO: do not access realm._np_random directly\n    #   related to skill.py, all harvest skills\n    # pylint: disable=protected-access\n    if realm._np_random.random() < self.prob:\n      return self.item(realm, level)\n\n    return None\n\nclass Standard:\n  def __init__(self):\n    self.drops = []\n\n  def add(self, item, prob=1.0):\n    self.drops += [Drop(item, prob)]\n\n  def roll(self, realm, level):\n    ret = []\n    for e in self.drops:\n      drop = e.roll(realm, level)\n      if drop is not None:\n        ret += [drop]\n    return ret\n\nclass Empty(Standard):\n  def roll(self, realm, level):\n    return []\n\nclass Ammunition(Standard):\n  def __init__(self, item):\n    super().__init__()\n    self.item = item\n\n  def roll(self, realm, level):\n    return [self.item(realm, level)]\n\nclass Consumable(Standard):\n  def __init__(self, item):\n    super().__init__()\n    self.item = item\n\n  def roll(self, realm, level):\n    return [self.item(realm, level)]\n"
  },
  {
    "path": "nmmo/systems/exchange.py",
    "content": "from __future__ import annotations\nfrom collections import deque\nimport math\n\nfrom typing import Dict\n\nfrom nmmo.systems.item import Item, Stack\nfrom nmmo.lib.event_code import EventCode\n\n\"\"\"\nThe Exchange class is a simulation of an in-game item exchange.\nIt has several methods that allow players to list items for sale,\nbuy items, and remove expired listings.\n\nThe _list_item() method is used to add a new item to the\nexchange, and the unlist_item() method is used to remove\nan item from the exchange. The step() method is used to\nregularly check and remove expired listings.\n\nThe sell() method allows a player to sell an item, and the buy() method\nallows a player to purchase an item. The packet property returns a\ndictionary that contains information about the items currently being\nsold on the exchange, such as the maximum and minimum price,\nthe average price, and the total supply of the items.\n\n\"\"\"\nclass ItemListing:\n  def __init__(self, item: Item, seller, price: int, tick: int):\n    self.item = item\n    self.seller = seller\n    self.price = price\n    self.tick = tick\n\nclass Exchange:\n  def __init__(self, realm):\n    self._listings_queue: deque[(int, int)] = deque() # (item_id, tick)\n    self._item_listings: Dict[int, ItemListing] = {}\n    self._realm = realm\n    self._config = realm.config\n\n  def reset(self):\n    self._listings_queue.clear()\n    self._item_listings.clear()\n\n  def _list_item(self, item: Item, seller, price: int, tick: int):\n    item.listed_price.update(price)\n    self._item_listings[item.id.val] = ItemListing(item, seller, price, tick)\n    self._listings_queue.append((item.id.val, tick))\n\n  def unlist_item(self, item: Item):\n    if item.id.val in self._item_listings:\n      self._unlist_item(item.id.val)\n\n  def _unlist_item(self, item_id: int):\n    item = self._item_listings.pop(item_id).item\n    item.listed_price.update(0)\n\n  def step(self):\n    \"\"\"\n    Remove expired listings from the exchange's listings queue\n    and item listings dictionary.\n\n    The method starts by checking the oldest listing in the listings\n    queue using a while loop. If the current tick minus the\n    listing tick is less than or equal to the EXCHANGE_LISTING_DURATION\n    in the realm's configuration, the method breaks out of\n    the loop as the oldest listing has not expired.\n    If the oldest listing has expired, the method removes it from the\n    listings queue and the item listings dictionary.\n\n    It then checks if the actual listing still exists and that\n    it is indeed expired. If it does exist and is expired,\n    it calls the _unlist_item method to remove the listing and update\n    the item's listed price. The process repeats until all expired listings\n    are removed from the queue and dictionary.\n    \"\"\"\n    if self._config.EXCHANGE_SYSTEM_ENABLED is False:\n      return\n\n    current_tick = self._realm.tick\n\n    # Remove expired listings\n    while self._listings_queue:\n      (item_id, listing_tick) = self._listings_queue[0]\n      if current_tick - listing_tick <= self._config.EXCHANGE_LISTING_DURATION:\n        # Oldest listing has not expired\n        break\n\n      # Remove expired listing from queue\n      self._listings_queue.popleft()\n\n      # The actual listing might have been refreshed and is newer than the queue record.\n      # Or it might have already been removed.\n      listing = self._item_listings.get(item_id)\n      if listing is not None and \\\n        current_tick - listing.tick > self._config.EXCHANGE_LISTING_DURATION:\n        self._unlist_item(item_id)\n\n  def sell(self, seller, item: Item, price: int, tick: int):\n    assert isinstance(\n        item, object), f'{item} for sale is not an Item instance'\n    assert item in seller.inventory, f'{item} for sale is not in {seller} inventory'\n    assert item.quantity.val > 0, f'{item} for sale has quantity {item.quantity.val}'\n    assert item.listed_price.val == 0, 'Item is already listed'\n    assert item.equipped.val == 0, 'Item has been equiped so cannot be listed'\n    assert price > 0, 'Price must be larger than 0'\n    self._list_item(item, seller, price, tick)\n    self._realm.event_log.record(EventCode.LIST_ITEM, seller, item=item, price=price)\n\n  def buy(self, buyer, item: Item):\n    assert item.quantity.val > 0, f'{item} purchase has quantity {item.quantity.val}'\n    assert item.equipped.val == 0, 'Listed item must not be equipped'\n    assert buyer.gold.val >= item.listed_price.val, 'Buyer does not have enough gold'\n    assert buyer.ent_id != item.owner_id.val, 'One cannot buy their own items'\n\n    if not buyer.inventory.space:\n      if isinstance(item, Stack):\n        if not buyer.inventory.has_stack(item.signature):\n          # no ammo stack with the same signature, so cannot buy\n          return\n      else: # no space, and item is not ammo stack, so cannot buy\n        return\n\n    # item is not in the listing (perhaps bought by other)\n    if item.id.val not in self._item_listings:\n      return\n\n    listing = self._item_listings[item.id.val]\n    price = item.listed_price.val\n\n    self.unlist_item(item)\n    listing.seller.inventory.remove(item)\n    buyer.inventory.receive(item)\n    buyer.gold.decrement(price)\n    listing.seller.gold.increment(price)\n\n    self._realm.event_log.record(EventCode.BUY_ITEM, buyer, item=item, price=price)\n    self._realm.event_log.record(EventCode.EARN_GOLD, listing.seller, amount=price)\n\n  @property\n  def packet(self):\n    packet = {}\n    for listing in self._item_listings.values():\n      item = listing.item\n      key = f'{item.__class__.__name__}_{item.level.val}'\n      max_price = max(packet.get(key, {}).get('max_price', -math.inf), listing.price)\n      min_price = min(packet.get(key, {}).get('min_price', math.inf), listing.price)\n      supply = packet.get(key, {}).get('supply', 0) + item.quantity.val\n\n      packet[key] = {\n        'max_price': max_price,\n        'min_price': min_price,\n        'price': (max_price + min_price) / 2,\n        'supply': supply\n      }\n\n    return packet\n"
  },
  {
    "path": "nmmo/systems/inventory.py",
    "content": "from typing import Dict, Tuple\n\nfrom ordered_set import OrderedSet\n\nfrom nmmo.systems import item as Item\nclass EquipmentSlot:\n  def __init__(self) -> None:\n    self.item = None\n\n  def equip(self, item: Item.Item) -> None:\n    self.item = item\n\n  def unequip(self) -> None:\n    if self.item:\n      self.item.equipped.update(0)\n    self.item = None\n\nclass Equipment:\n  def __init__(self):\n    self.hat = EquipmentSlot()\n    self.top = EquipmentSlot()\n    self.bottom = EquipmentSlot()\n    self.held = EquipmentSlot()\n    self.ammunition = EquipmentSlot()\n\n  def total(self, lambda_getter):\n    items = [lambda_getter(e).val for e in self]\n    if not items:\n      return 0\n    return sum(items)\n\n  def __iter__(self):\n    for slot in [self.hat, self.top, self.bottom, self.held, self.ammunition]:\n      if slot.item is not None:\n        yield slot.item\n\n  def conditional_packet(self, packet, slot_name: str, slot: EquipmentSlot):\n    if slot.item:\n      packet[slot_name] = slot.item.packet\n\n  @property\n  def item_level(self):\n    return self.total(lambda e: e.level)\n\n  @property\n  def melee_attack(self):\n    return self.total(lambda e: e.melee_attack)\n\n  @property\n  def range_attack(self):\n    return self.total(lambda e: e.range_attack)\n\n  @property\n  def mage_attack(self):\n    return self.total(lambda e: e.mage_attack)\n\n  @property\n  def melee_defense(self):\n    return self.total(lambda e: e.melee_defense)\n\n  @property\n  def range_defense(self):\n    return self.total(lambda e: e.range_defense)\n\n  @property\n  def mage_defense(self):\n    return self.total(lambda e: e.mage_defense)\n\n  @property\n  def packet(self):\n    packet = {}\n\n    self.conditional_packet(packet, 'hat',        self.hat)\n    self.conditional_packet(packet, 'top',        self.top)\n    self.conditional_packet(packet, 'bottom',     self.bottom)\n    self.conditional_packet(packet, 'held',       self.held)\n    self.conditional_packet(packet, 'ammunition', self.ammunition)\n\n    # pylint: disable=R0801\n    # Similar lines here and in npc.py\n    packet['item_level']    = self.item_level\n    packet['melee_attack']  = self.melee_attack\n    packet['range_attack']  = self.range_attack\n    packet['mage_attack']   = self.mage_attack\n    packet['melee_defense'] = self.melee_defense\n    packet['range_defense'] = self.range_defense\n    packet['mage_defense']  = self.mage_defense\n\n    return packet\n\n\nclass Inventory:\n  def __init__(self, realm, entity):\n    config           = realm.config\n    self.realm       = realm\n    self.entity      = entity\n    self.config      = config\n\n    self.equipment   = Equipment()\n    self.capacity = 0\n\n    if config.ITEM_SYSTEM_ENABLED and entity.is_player:\n      self.capacity         = config.ITEM_INVENTORY_CAPACITY\n\n    self._item_stacks: Dict[Tuple, Item.Stack] = {}\n    self.items: OrderedSet[Item.Item] = OrderedSet([]) # critical for correct functioning\n\n  @property\n  def space(self):\n    return self.capacity - len(self.items)\n\n  def has_stack(self, signature: Tuple) -> bool:\n    return signature in self._item_stacks\n\n  def packet(self):\n    item_packet = []\n    if self.config.ITEM_SYSTEM_ENABLED:\n      item_packet = [e.packet for e in self.items]\n\n    return {\n          'items':     item_packet,\n          'equipment': self.equipment.packet}\n\n  def __iter__(self):\n    for item in self.items:\n      yield item\n\n  def receive(self, item: Item.Item) -> bool:\n    # Return True if the item is received\n    assert isinstance(item, Item.Item), f'{item} received is not an Item instance'\n    assert item not in self.items, f'{item} object received already in inventory'\n    assert not item.equipped.val, f'Received equipped item {item}'\n    assert not item.listed_price.val, f'Received listed item {item}'\n    assert item.quantity.val, f'Received empty item {item}'\n\n    if isinstance(item, Item.Stack):\n      signature = item.signature\n      if self.has_stack(signature):\n        stack = self._item_stacks[signature]\n        assert item.level.val == stack.level.val, f'{item} stack level mismatch'\n        stack.quantity.increment(item.quantity.val)\n        # destroy the original item instance after the transfer is complete\n        item.destroy()\n        return False\n\n      if not self.space:\n        # if no space thus cannot receive, just destroy the item\n        item.destroy()\n        return False\n\n      self._item_stacks[signature] = item\n\n    if not self.space:\n      # if no space thus cannot receive, just destroy the item\n      item.destroy()\n      return False\n\n    item.owner_id.update(self.entity.id.val)\n    self.items.add(item)\n    return True\n\n  # pylint: disable=protected-access\n  def remove(self, item, quantity=None):\n    assert isinstance(item, Item.Item), f'{item} removing item is not an Item instance'\n    assert item in self.items, f'No item {item} to remove'\n\n    if isinstance(item, Item.Equipment) and item.equipped.val:\n      item.unequip(item._slot(self.entity))\n\n    if isinstance(item, Item.Stack):\n      signature = item.signature\n\n      assert self.has_stack(item.signature), f'{item} stack to remove not in inventory'\n      stack = self._item_stacks[signature]\n\n      if quantity is None or stack.quantity.val == quantity:\n        self._remove(stack)\n        del self._item_stacks[signature]\n        return\n\n      assert 0 < quantity <= stack.quantity.val, \\\n        f'Invalid remove {quantity} x {item} ({stack.quantity.val} available)'\n      stack.quantity.val -= quantity\n      return\n\n    self._remove(item)\n\n  def _remove(self, item):\n    self.realm.exchange.unlist_item(item)\n    item.owner_id.update(0)\n    self.items.remove(item)\n"
  },
  {
    "path": "nmmo/systems/item.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom abc import ABC\nfrom types import SimpleNamespace\nfrom typing import Dict\n\nfrom nmmo.datastore.serialized import SerializedState\nfrom nmmo.lib.colors import Tier\nfrom nmmo.lib.event_code import EventCode\n\n# pylint: disable=no-member\nItemState = SerializedState.subclass(\"Item\", [\n  \"id\",\n  \"type_id\",\n  \"owner_id\",\n\n  \"level\",\n  \"capacity\",\n  \"quantity\",\n  \"melee_attack\",\n  \"range_attack\",\n  \"mage_attack\",\n  \"melee_defense\",\n  \"range_defense\",\n  \"mage_defense\",\n  \"health_restore\",\n  \"resource_restore\",\n  \"equipped\",\n\n  # Market\n  \"listed_price\",\n])\n\n# TODO: These limits should be defined in the config.\nItemState.Limits = lambda config: {\n  \"id\": (0, math.inf),\n  \"type_id\": (0, 99),\n  \"owner_id\": (-math.inf, math.inf),\n  \"level\": (0, 99),\n  \"capacity\": (0, 99),\n  \"quantity\": (0, math.inf), # NOTE: Ammunitions can be stacked infinitely\n  \"melee_attack\": (0, 100),\n  \"range_attack\": (0, 100),\n  \"mage_attack\": (0, 100),\n  \"melee_defense\": (0, 100),\n  \"range_defense\": (0, 100),\n  \"mage_defense\": (0, 100),\n  \"health_restore\": (0, 100),\n  \"resource_restore\": (0, 100),\n  \"equipped\": (0, 1),\n  \"listed_price\": (0, math.inf),\n}\n\nItemState.Query = SimpleNamespace(\n  table=lambda ds: ds.table(\"Item\").where_neq(\n    ItemState.State.attr_name_to_col[\"id\"], 0),\n\n  by_id=lambda ds, id: ds.table(\"Item\").where_eq(\n    ItemState.State.attr_name_to_col[\"id\"], id),\n\n  owned_by = lambda ds, id: ds.table(\"Item\").where_eq(\n    ItemState.State.attr_name_to_col[\"owner_id\"], id),\n\n  for_sale = lambda ds: ds.table(\"Item\").where_neq(\n    ItemState.State.attr_name_to_col[\"listed_price\"], 0),\n)\n\nclass Item(ItemState):\n  ITEM_TYPE_ID = None\n  _item_type_id_to_class: Dict[int, type] = {}\n\n  @staticmethod\n  def register(item_type):\n    assert item_type.ITEM_TYPE_ID is not None\n    if item_type.ITEM_TYPE_ID not in Item._item_type_id_to_class:\n      Item._item_type_id_to_class[item_type.ITEM_TYPE_ID] = item_type\n\n  @staticmethod\n  def item_class(type_id: int):\n    return Item._item_type_id_to_class[type_id]\n\n  def __init__(self, realm, level,\n              capacity=0,\n              melee_attack=0, range_attack=0, mage_attack=0,\n              melee_defense=0, range_defense=0, mage_defense=0,\n              health_restore=0, resource_restore=0):\n\n    super().__init__(realm.datastore, ItemState.Limits(realm.config))\n    self.realm = realm\n    self.config = realm.config\n\n    Item.register(self.__class__)\n\n    self.id.update(self.datastore_record.id)\n    self.type_id.update(self.ITEM_TYPE_ID)\n    self.level.update(level)\n    self.capacity.update(capacity)\n    # every item instance is created individually, i.e., quantity=1\n    self.quantity.update(1)\n    self.melee_attack.update(melee_attack)\n    self.range_attack.update(range_attack)\n    self.mage_attack.update(mage_attack)\n    self.melee_defense.update(melee_defense)\n    self.range_defense.update(range_defense)\n    self.mage_defense.update(mage_defense)\n    self.health_restore.update(health_restore)\n    self.resource_restore.update(resource_restore)\n    realm.items[self.id.val] = self\n\n  def destroy(self):\n    # NOTE: we may want to track the item lifecycle and\n    #   and see how many high-level items are wasted\n    if self.config.EXCHANGE_SYSTEM_ENABLED:\n      self.realm.exchange.unlist_item(self)\n    if self.owner_id.val in self.realm.players:\n      self.realm.players[self.owner_id.val].inventory.remove(self)\n    self.realm.items.pop(self.id.val, None)\n    self.datastore_record.delete()\n\n  @property\n  def packet(self):\n    return {'item':             self.__class__.__name__,\n            'level':            self.level.val,\n            'capacity':         self.capacity.val,\n            'quantity':         self.quantity.val,\n            'melee_attack':     self.melee_attack.val,\n            'range_attack':     self.range_attack.val,\n            'mage_attack':      self.mage_attack.val,\n            'melee_defense':    self.melee_defense.val,\n            'range_defense':    self.range_defense.val,\n            'mage_defense':     self.mage_defense.val,\n            'health_restore':   self.health_restore.val,\n            'resource_restore': self.resource_restore.val,\n            }\n\n  def _level(self, entity):\n    # this is for armors, ration, and potion\n    # weapons and tools must override this with specific skills\n    return entity.level\n\n  def level_gt(self, entity):\n    return self.level.val > self._level(entity)\n\n  def use(self, entity) -> bool:\n    raise NotImplementedError\n\nclass Stack:\n  @property\n  def signature(self):\n    return (self.type_id.val, self.level.val)\n\nclass Equipment(Item):\n  @property\n  def packet(self):\n    packet = {'color': self.color.packet()}\n    return {**packet, **super().packet}\n\n  @property\n  def color(self):\n    if self.level == 0:\n      return Tier.BLACK\n    if self.level < 10:\n      return Tier.WOOD\n    if self.level < 20:\n      return Tier.BRONZE\n    if self.level < 40:\n      return Tier.SILVER\n    if self.level < 60:\n      return Tier.GOLD\n    if self.level < 80:\n      return Tier.PLATINUM\n    return Tier.DIAMOND\n\n  def unequip(self, equip_slot):\n    assert self.equipped.val == 1\n    self.equipped.update(0)\n    equip_slot.unequip()\n\n  def equip(self, entity, equip_slot):\n    assert self.equipped.val == 0\n    if self._level(entity) < self.level.val:\n      return\n\n    self.equipped.update(1)\n    equip_slot.equip(self)\n\n  def _slot(self, entity):\n    raise NotImplementedError\n\n  def use(self, entity):\n    assert self in entity.inventory, \"Item is not in entity's inventory\"\n    assert self.listed_price == 0, \"Listed item cannot be used\"\n    assert self._level(entity) >= self.level.val, \"Entity's level is not sufficient to use the item\"\n\n    if self.equipped.val:\n      self.unequip(self._slot(entity))\n    else:\n      # always empty the slot first\n      self._slot(entity).unequip()\n      self.equip(entity, self._slot(entity))\n      self.realm.event_log.record(EventCode.EQUIP_ITEM, entity, item=self)\n\nclass Armor(Equipment, ABC):\n  def __init__(self, realm, level, **kwargs):\n    defense = realm.config.EQUIPMENT_ARMOR_BASE_DEFENSE + \\\n              level*realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE\n    super().__init__(realm, level,\n                     melee_defense=defense,\n                     range_defense=defense,\n                     mage_defense=defense,\n                     **kwargs)\nclass Hat(Armor):\n  ITEM_TYPE_ID = 2\n  def _slot(self, entity):\n    return entity.inventory.equipment.hat\nclass Top(Armor):\n  ITEM_TYPE_ID = 3\n  def _slot(self, entity):\n    return entity.inventory.equipment.top\nclass Bottom(Armor):\n  ITEM_TYPE_ID = 4\n  def _slot(self, entity):\n    return entity.inventory.equipment.bottom\n\n\nclass Weapon(Equipment):\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.attack = (\n      realm.config.EQUIPMENT_WEAPON_BASE_DAMAGE +\n      level*realm.config.EQUIPMENT_WEAPON_LEVEL_DAMAGE)\n\n  def _slot(self, entity):\n    return entity.inventory.equipment.held\n\nclass Spear(Weapon):\n  ITEM_TYPE_ID = 5\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.melee_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.melee.level.val\nclass Bow(Weapon):\n  ITEM_TYPE_ID = 6\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.range_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.range.level.val\nclass Wand(Weapon):\n  ITEM_TYPE_ID = 7\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.mage_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.mage.level.val\n\n\nclass Tool(Equipment):\n  def __init__(self, realm, level, **kwargs):\n    defense = realm.config.EQUIPMENT_TOOL_BASE_DEFENSE + \\\n        level*realm.config.EQUIPMENT_TOOL_LEVEL_DEFENSE\n    super().__init__(realm, level,\n                      melee_defense=defense,\n                      range_defense=defense,\n                      mage_defense=defense,\n                      **kwargs)\n\n  def _slot(self, entity):\n    return entity.inventory.equipment.held\nclass Rod(Tool):\n  ITEM_TYPE_ID = 8\n  def _level(self, entity):\n    return entity.skills.fishing.level.val\nclass Gloves(Tool):\n  ITEM_TYPE_ID = 9\n  def _level(self, entity):\n    return entity.skills.herbalism.level.val\nclass Pickaxe(Tool):\n  ITEM_TYPE_ID = 10\n  def _level(self, entity):\n    return entity.skills.prospecting.level.val\nclass Axe(Tool):\n  ITEM_TYPE_ID = 11\n  def _level(self, entity):\n    return entity.skills.carving.level.val\nclass Chisel(Tool):\n  ITEM_TYPE_ID = 12\n  def _level(self, entity):\n    return entity.skills.alchemy.level.val\n\n\nclass Ammunition(Equipment, Stack):\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.attack = (\n      realm.config.EQUIPMENT_AMMUNITION_BASE_DAMAGE +\n      level*realm.config.EQUIPMENT_AMMUNITION_LEVEL_DAMAGE)\n\n  def _slot(self, entity):\n    return entity.inventory.equipment.ammunition\n\n  def fire(self, entity) -> int:\n    assert self.equipped.val > 0, 'Ammunition not equipped'\n    assert self.quantity.val > 0, 'Used ammunition with 0 quantity'\n\n    self.quantity.decrement()\n\n    if self.quantity.val == 0:\n      entity.inventory.remove(self)\n      # delete this empty item instance from the datastore\n      self.destroy()\n\n    self.realm.event_log.record(EventCode.FIRE_AMMO, entity, item=self)\n    return self.damage\n\nclass Whetstone(Ammunition):\n  ITEM_TYPE_ID = 13\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.melee_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.melee.level.val\n\n  @property\n  def damage(self):\n    return self.melee_attack.val\n\nclass Arrow(Ammunition):\n  ITEM_TYPE_ID = 14\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.range_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.range.level.val\n\n  @property\n  def damage(self):\n    return self.range_attack.val\n\nclass Runes(Ammunition):\n  ITEM_TYPE_ID = 15\n\n  def __init__(self, realm, level, **kwargs):\n    super().__init__(realm, level, **kwargs)\n    self.mage_attack.update(self.attack)\n\n  def _level(self, entity):\n    return entity.skills.mage.level.val\n\n  @property\n  def damage(self):\n    return self.mage_attack.val\n\n\n# NOTE: Each consumable item (ration, potion) cannot be stacked,\n#   so each item takes 1 inventory space\nclass Consumable(Item):\n  def use(self, entity) -> bool:\n    assert self in entity.inventory, \"Item is not in entity's inventory\"\n    assert self.listed_price == 0, \"Listed item cannot be used\"\n    assert self._level(entity) >= self.level.val, \"Entity's level is not sufficient to use the item\"\n\n    self.realm.event_log.record(EventCode.CONSUME_ITEM, entity, item=self)\n    self._apply_effects(entity)\n    entity.inventory.remove(self)\n    self.destroy()\n    return True\n\nclass Ration(Consumable):\n  ITEM_TYPE_ID = 16\n\n  def __init__(self, realm, level, **kwargs):\n    restore = 0\n    if realm.config.PROFESSION_SYSTEM_ENABLED:\n      restore = realm.config.PROFESSION_CONSUMABLE_RESTORE(level)\n    super().__init__(realm, level, resource_restore=restore, **kwargs)\n\n  def _apply_effects(self, entity):\n    entity.resources.food.increment(self.resource_restore.val)\n    entity.resources.water.increment(self.resource_restore.val)\n\nclass Potion(Consumable):\n  ITEM_TYPE_ID = 17\n\n  def __init__(self, realm, level, **kwargs):\n    restore = 0\n    if realm.config.PROFESSION_SYSTEM_ENABLED:\n      restore = realm.config.PROFESSION_CONSUMABLE_RESTORE(level)\n    super().__init__(realm, level, health_restore=restore, **kwargs)\n\n  def _apply_effects(self, entity):\n    entity.resources.health.increment(self.health_restore.val)\n    entity.poultice_consumed += 1\n    entity.poultice_level_consumed = max(\n      entity.poultice_level_consumed, self.level.val)\n\n# Item groupings\nARMOR = [Hat, Top, Bottom]\nWEAPON = [Spear, Bow, Wand]\nTOOL = [Rod, Gloves, Pickaxe, Axe, Chisel]\nAMMUNITION = [Whetstone, Arrow, Runes]\nCONSUMABLE = [Ration, Potion]\nALL_ITEM = ARMOR + WEAPON + TOOL + AMMUNITION + CONSUMABLE\n"
  },
  {
    "path": "nmmo/systems/skill.py",
    "content": "from __future__ import annotations\n\nimport abc\n\nimport numpy as np\nfrom ordered_set import OrderedSet\n\nfrom nmmo.lib import material\nfrom nmmo.systems import combat\nfrom nmmo.lib.event_code import EventCode\n\n### Infrastructure ###\nclass ExperienceCalculator:\n  def __init__(self, config):\n    if not config.PROGRESSION_SYSTEM_ENABLED:\n      return\n    self.config = config\n    self.exp_threshold = np.array(config.PROGRESSION_EXP_THRESHOLD)\n    assert len(self.exp_threshold) >= config.PROGRESSION_LEVEL_MAX,\\\n      \"PROGRESSION_LEVEL_BY_EXP must have at least PROGRESSION_LEVEL_MAX entries\"\n    self.max_exp = self.exp_threshold[self.config.PROGRESSION_LEVEL_MAX - 1]\n\n  def exp_at_level(self, level):\n    level = min(max(level, self.config.PROGRESSION_BASE_LEVEL),\n                self.config.PROGRESSION_LEVEL_MAX)\n    return int(self.exp_threshold[level - 1])\n\n  def level_at_exp(self, exp):\n    if exp >= self.max_exp:\n      return self.config.PROGRESSION_LEVEL_MAX\n    return np.argmin(exp >= self.exp_threshold)\n\nclass SkillGroup:\n  def __init__(self, realm, entity):\n    self.config  = realm.config\n    self.realm   = realm\n    self.entity = entity\n\n    self.experience_calculator = ExperienceCalculator(self.config)\n    self.skills  = OrderedSet() # critical for determinism\n\n  def update(self):\n    for skill in self.skills:\n      skill.update()\n\n  def packet(self):\n    data = {}\n    for skill in self.skills:\n      data[skill.__class__.__name__.lower()] = skill.packet()\n\n    return data\n\nclass Skill(abc.ABC):\n  def __init__(self, skill_group: SkillGroup):\n    self.realm = skill_group.realm\n    self.config = skill_group.config\n    self.entity = skill_group.entity\n\n    self.experience_calculator = skill_group.experience_calculator\n    self.skill_group = skill_group\n    skill_group.skills.add(self)\n\n  def packet(self):\n    data = {}\n    data['exp']   = self.exp.val\n    data['level'] = self.level.val\n    return data\n\n  def add_xp(self, xp):\n    self.exp.increment(xp)\n    new_level = int(self.experience_calculator.level_at_exp(self.exp.val))\n\n    if new_level > self.level.val:\n      self.level.update(new_level)\n      self.realm.event_log.record(EventCode.LEVEL_UP, self.entity,\n                                  skill=self, level=new_level)\n\n  def set_experience_by_level(self, level):\n    self.exp.update(self.experience_calculator.level_at_exp(level))\n    self.level.update(int(level))\n\n  @property\n  def level(self):\n    raise NotImplementedError(f\"Skill {self.__class__.__name__} \"\\\n      \"does not implement 'level' property\")\n\n  @property\n  def exp(self):\n    raise NotImplementedError(f\"Skill {self.__class__.__name__} \"\\\n      \"does not implement 'exp' property\")\n\n### Skill Bases ###\nclass CombatSkill(Skill):\n  def update(self):\n    pass\n\nclass NonCombatSkill(Skill):\n  def __init__(self, skill_group: SkillGroup):\n    super().__init__(skill_group)\n    self._dummy_value = DummyValue()  # for water and food\n\n  @property\n  def level(self):\n    return self._dummy_value\n\n  @property\n  def exp(self):\n    return self._dummy_value\n\nclass HarvestSkill(NonCombatSkill):\n  def process_drops(self, matl, drop_table):\n    if not self.config.ITEM_SYSTEM_ENABLED:\n      return\n\n    entity = self.entity\n\n    # harvest without tool will only yield level-1 item even with high skill level\n    # for example, fishing level=5 without rod will only yield level-1 ration\n    level = 1\n    tool  = entity.equipment.held\n    if matl.tool is not None and isinstance(tool.item, matl.tool):\n      level = min(1+tool.item.level.val, self.config.PROGRESSION_LEVEL_MAX)\n\n    #TODO: double-check drop table quantity\n    for drop in drop_table.roll(self.realm, level):\n      assert drop.level.val == level, 'Drop level does not match roll specification'\n      if entity.inventory.space:\n        entity.inventory.receive(drop)\n        self.realm.event_log.record(EventCode.HARVEST_ITEM, entity, item=drop)\n      else:\n        drop.destroy()  # this was the source of the item leak\n\n  def harvest(self, matl, deplete=True):\n    entity = self.entity\n    realm  = self.realm\n\n    r, c = entity.pos\n    if realm.map.tiles[r, c].state != matl:\n      return False\n\n    drop_table = realm.map.harvest(r, c, deplete)\n    if drop_table:\n      self.process_drops(matl, drop_table)\n\n    return drop_table\n\n  def harvest_adjacent(self, matl, deplete=True):\n    entity = self.entity\n    realm  = self.realm\n\n    r, c      = entity.pos\n    drop_table = None\n\n    if realm.map.tiles[r-1, c].state == matl:\n      drop_table = realm.map.harvest(r-1, c, deplete)\n    if realm.map.tiles[r+1, c].state == matl:\n      drop_table = realm.map.harvest(r+1, c, deplete)\n    if realm.map.tiles[r, c-1].state == matl:\n      drop_table = realm.map.harvest(r, c-1, deplete)\n    if realm.map.tiles[r, c+1].state == matl:\n      drop_table = realm.map.harvest(r, c+1, deplete)\n\n    if drop_table:\n      self.process_drops(matl, drop_table)\n\n    return drop_table\n\nclass AmmunitionSkill(HarvestSkill):\n  def process_drops(self, matl, drop_table):\n    super().process_drops(matl, drop_table)\n    if self.config.PROGRESSION_SYSTEM_ENABLED:\n      self.add_xp(self.config.PROGRESSION_AMMUNITION_XP_SCALE)\n\n\nclass ConsumableSkill(HarvestSkill):\n  def process_drops(self, matl, drop_table):\n    super().process_drops(matl, drop_table)\n    if self.config.PROGRESSION_SYSTEM_ENABLED:\n      self.add_xp(self.config.PROGRESSION_CONSUMABLE_XP_SCALE)\n\n\n### Skill groups ###\nclass Basic(SkillGroup):\n  def __init__(self, realm, entity):\n    super().__init__(realm, entity)\n\n    self.water = Water(self)\n    self.food  = Food(self)\n\n  @property\n  def basic_level(self):\n    return 0.5 * (self.water.level\n            + self.food.level)\n\nclass Harvest(SkillGroup):\n  def __init__(self, realm, entity):\n    super().__init__(realm, entity)\n\n    self.fishing      = Fishing(self)\n    self.herbalism    = Herbalism(self)\n    self.prospecting  = Prospecting(self)\n    self.carving      = Carving(self)\n    self.alchemy      = Alchemy(self)\n\n  @property\n  def harvest_level(self):\n    return max(self.fishing.level,\n                self.herbalism.level,\n                self.prospecting.level,\n                self.carving.level,\n                self.alchemy.level)\n\nclass Combat(SkillGroup):\n  def __init__(self, realm, entity):\n    super().__init__(realm, entity)\n\n    self.melee = Melee(self)\n    self.range = Range(self)\n    self.mage  = Mage(self)\n\n  def packet(self):\n    data          = super().packet()\n    data['level'] = combat.level(self)\n\n    return data\n\n  @property\n  def combat_level(self):\n    return max(self.melee.level,\n                self.range.level,\n                self.mage.level)\n\n  def apply_damage(self, style):\n    if self.config.PROGRESSION_SYSTEM_ENABLED:\n      skill  = self.__dict__[style]\n      skill.add_xp(self.config.PROGRESSION_COMBAT_XP_SCALE)\n\n  def receive_damage(self, dmg):\n    pass\n\nclass Skills(Basic, Harvest, Combat):\n  pass\n\n### Combat Skills ###\nclass Melee(CombatSkill):\n  SKILL_ID = 1\n\n  @property\n  def level(self):\n    return self.entity.melee_level\n\n  @property\n  def exp(self):\n    return self.entity.melee_exp\n\nclass Range(CombatSkill):\n  SKILL_ID = 2\n\n  @property\n  def level(self):\n    return self.entity.range_level\n\n  @property\n  def exp(self):\n    return self.entity.range_exp\n\nclass Mage(CombatSkill):\n  SKILL_ID = 3\n\n  @property\n  def level(self):\n    return self.entity.mage_level\n\n  @property\n  def exp(self):\n    return self.entity.mage_exp\n\nMelee.weakness = Mage\nRange.weakness = Melee\nMage.weakness  = Range\n\n\n### Basic/Harvest Skills ###\n\nclass DummyValue:\n  def __init__(self, val=0):\n    self.val = val\n\n  def update(self, val):\n    self.val = val\n\nclass Water(HarvestSkill):\n  def update(self):\n    config = self.config\n    if not config.RESOURCE_SYSTEM_ENABLED:\n      return\n\n    if config.IMMORTAL or self.entity.immortal:\n      return\n\n    depletion = config.RESOURCE_DEPLETION_RATE\n    water = self.entity.resources.water\n    water.decrement(depletion)\n\n    if not self.harvest_adjacent(material.Water, deplete=False):\n      return\n\n    restore = np.floor(config.RESOURCE_BASE\n                      * config.RESOURCE_HARVEST_RESTORE_FRACTION)\n    water.increment(restore)\n\n    self.realm.event_log.record(EventCode.DRINK_WATER, self.entity)\n\nclass Food(HarvestSkill):\n  def update(self):\n    config = self.config\n    if not config.RESOURCE_SYSTEM_ENABLED:\n      return\n\n    if config.IMMORTAL or self.entity.immortal:\n      return\n\n    depletion = config.RESOURCE_DEPLETION_RATE\n    food = self.entity.resources.food\n    food.decrement(depletion)\n\n    if not self.harvest(material.Foilage):\n      return\n\n    restore = np.floor(config.RESOURCE_BASE\n                      * config.RESOURCE_HARVEST_RESTORE_FRACTION)\n    food.increment(restore)\n\n    self.realm.event_log.record(EventCode.EAT_FOOD, self.entity)\n\nclass Fishing(ConsumableSkill):\n  SKILL_ID = 4\n\n  @property\n  def level(self):\n    return self.entity.fishing_level\n\n  @property\n  def exp(self):\n    return self.entity.fishing_exp\n\n  def update(self):\n    self.harvest_adjacent(material.Fish)\n\nclass Herbalism(ConsumableSkill):\n  SKILL_ID = 5\n\n  @property\n  def level(self):\n    return self.entity.herbalism_level\n\n  @property\n  def exp(self):\n    return self.entity.herbalism_exp\n\n  def update(self):\n    self.harvest(material.Herb)\n\nclass Prospecting(AmmunitionSkill):\n  SKILL_ID = 6\n\n  @property\n  def level(self):\n    return self.entity.prospecting_level\n\n  @property\n  def exp(self):\n    return self.entity.prospecting_exp\n\n  def update(self):\n    self.harvest(material.Ore)\n\nclass Carving(AmmunitionSkill):\n  SKILL_ID = 7\n\n  @property\n  def level(self):\n    return self.entity.carving_level\n\n  @property\n  def exp(self):\n    return self.entity.carving_exp\n\n  def update(self,):\n    self.harvest(material.Tree)\n\nclass Alchemy(AmmunitionSkill):\n  SKILL_ID = 8\n\n  @property\n  def level(self):\n    return self.entity.alchemy_level\n\n  @property\n  def exp(self):\n    return self.entity.alchemy_exp\n\n  def update(self):\n    self.harvest(material.Crystal)\n\n# Skill groupings\nCOMBAT_SKILL = [Melee, Range, Mage]\nHARVEST_SKILL = [Fishing, Herbalism, Prospecting, Carving, Alchemy]\n"
  },
  {
    "path": "nmmo/task/__init__.py",
    "content": "from .game_state import *\nfrom .predicate_api import *\nfrom .task_api import *\n"
  },
  {
    "path": "nmmo/task/base_predicates.py",
    "content": "#pylint: disable=invalid-name, unused-argument, no-value-for-parameter\nfrom __future__ import annotations\nfrom typing import Iterable\nimport numpy as np\nfrom numpy import count_nonzero as count\n\nfrom nmmo.task.group import Group\nfrom nmmo.task.game_state import GameState\nfrom nmmo.systems import skill as nmmo_skill\nfrom nmmo.systems.skill import Skill\nfrom nmmo.systems.item import Item\nfrom nmmo.lib.material import Material\nfrom nmmo.lib import utils\n\ndef norm(progress):\n  return max(min(progress, 1.0), 0.0)\n\ndef Success(gs: GameState, subject: Group):\n  ''' Returns True. For debugging.\n  '''\n  return True\n\ndef TickGE(gs: GameState, subject: Group, num_tick: int = None):\n  \"\"\"True if the current tick is greater than or equal to the specified num_tick.\n  Is progress counter.\n  \"\"\"\n  if num_tick is None:\n    num_tick = gs.config.HORIZON\n  return norm(gs.current_tick / num_tick)\n\ndef CanSeeTile(gs: GameState, subject: Group, tile_type: type[Material]):\n  \"\"\" True if any agent in subject can see a tile of tile_type\n  \"\"\"\n  return any(tile_type.index in t for t in subject.obs.tile.material_id)\n\ndef StayAlive(gs: GameState, subject: Group):\n  \"\"\"True if all subjects are alive.\n  \"\"\"\n  return count(subject.health > 0) == len(subject)\n\ndef AllDead(gs: GameState, subject: Group):\n  \"\"\"True if all subjects are dead.\n  \"\"\"\n  return norm(1.0 - count(subject.health) / len(subject))\n\ndef CheckAgentStatus(gs: GameState, subject: Group, target: Iterable[int], status: str):\n  \"\"\"Check if target agents are alive or dead using the game status\"\"\"\n  if isinstance(target, int):\n    target = [target]\n  num_agents = len(target)\n  num_alive = sum(1 for agent in target if agent in gs.alive_agents)\n  if status == 'alive':\n    return num_alive / num_agents\n  if status == 'dead':\n    return (num_agents - num_alive) / num_agents\n  # invalid status\n  return 0.0\n\ndef OccupyTile(gs: GameState, subject: Group, row: int, col: int):\n  \"\"\"True if any subject agent is on the desginated tile.\n  \"\"\"\n  return np.any((subject.row == row) & (subject.col == col))\n\ndef CanSeeAgent(gs: GameState, subject: Group, target: int):\n  \"\"\"True if obj_agent is present in the subjects' entities obs.\n  \"\"\"\n  return any(target in e.ids for e in subject.obs.entities)\n\ndef CanSeeGroup(gs: GameState, subject: Group, target: Iterable[int]):\n  \"\"\" Returns True if subject can see any of target\n  \"\"\"\n  if target is None:\n    return False\n  return any(CanSeeAgent(gs, subject, agent) for agent in target)\n\ndef DistanceTraveled(gs: GameState, subject: Group, dist: int):\n  \"\"\"True if the summed l-inf distance between each agent's current pos and spawn pos\n        is greater than or equal to the specified _dist.\n  \"\"\"\n  if not any(subject.health > 0):\n    return False\n  r = subject.row\n  c = subject.col\n  dists = utils.linf(list(zip(r,c)),[gs.spawn_pos[id_] for id_ in subject.entity.id])\n  return norm(dists.sum() / dist)\n\ndef AttainSkill(gs: GameState, subject: Group,\n                skill: type[Skill], level: int, num_agent: int):\n  \"\"\"True if the number of agents having skill level GE level\n        is greather than or equal to num_agent\n  \"\"\"\n  if level <= 1:\n    return 1.0\n  skill_level = getattr(subject,skill.__name__.lower() + '_level') - 1  # base level is 1\n  return norm(sum(skill_level) / (num_agent * (level-1)))\n\ndef GainExperience(gs: GameState, subject: Group,\n                   skill: type[Skill], experience: int, num_agent: int):\n  \"\"\"True if the experience gained for the skill is greater than or equal to experience.\"\"\"\n  skill_exp = getattr(subject,skill.__name__.lower() + '_exp')\n  return norm(sum(skill_exp) / (experience*num_agent))\n\ndef CountEvent(gs: GameState, subject: Group, event: str, N: int):\n  \"\"\"True if the number of events occured in subject corresponding\n      to event >= N\n  \"\"\"\n  return norm(len(getattr(subject.event, event)) / N)\n\ndef ScoreHit(gs: GameState, subject: Group, combat_style: type[Skill], N: int):\n  \"\"\"True if the number of hits scored in style\n  combat_style >= count\n  \"\"\"\n  hits = subject.event.SCORE_HIT.combat_style == combat_style.SKILL_ID\n  return norm(count(hits) / N)\n\ndef DefeatEntity(gs: GameState, subject: Group, agent_type: str, level: int, num_agent: int):\n  \"\"\"True if the number of agents (agent_type, >= level) defeated\n        is greater than or equal to num_agent\n  \"\"\"\n  # NOTE: there is no way to tell if an agent is a teammate or an enemy\n  #   so agents can get rewarded for killing their own teammates\n  defeated_type = subject.event.PLAYER_KILL.target_ent > 0 if agent_type == 'player' \\\n                    else subject.event.PLAYER_KILL.target_ent < 0\n  defeated = defeated_type & (subject.event.PLAYER_KILL.level >= level)\n  if num_agent > 0:\n    return norm(count(defeated) / num_agent)\n  return 1.0\n\ndef HoardGold(gs: GameState, subject: Group, amount: int):\n  \"\"\"True iff the summed gold of all teammate is greater than or equal to amount.\n  \"\"\"\n  return norm(subject.gold.sum() / amount)\n\ndef EarnGold(gs: GameState, subject: Group, amount: int):\n  \"\"\" True if the total amount of gold earned is greater than or equal to amount.\n  \"\"\"\n  gold = subject.event.EARN_GOLD.gold.sum() + subject.event.LOOT_GOLD.gold.sum()\n  return norm(gold / amount)\n\ndef SpendGold(gs: GameState, subject: Group, amount: int):\n  \"\"\" True if the total amount of gold spent is greater than or equal to amount.\n  \"\"\"\n  return norm(subject.event.BUY_ITEM.gold.sum() / amount)\n\ndef MakeProfit(gs: GameState, subject: Group, amount: int):\n  \"\"\" True if the total amount of gold earned-spent is greater than or equal to amount.\n  \"\"\"\n  profits = subject.event.EARN_GOLD.gold.sum() + subject.event.LOOT_GOLD.gold.sum()\n  costs = subject.event.BUY_ITEM.gold.sum()\n  return  norm((profits-costs) / amount)\n\ndef InventorySpaceGE(gs: GameState, subject: Group, space: int):\n  \"\"\"True if the inventory space of every subjects is greater than or equal to\n       the space. Otherwise false.\n  \"\"\"\n  max_space = gs.config.ITEM_INVENTORY_CAPACITY\n  return all(max_space - inv.len >= space for inv in subject.obs.inventory)\n\ndef OwnItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if the number of items owned (_item_type, >= level)\n     is greater than or equal to quantity.\n  \"\"\"\n  owned = (subject.item.type_id == item.ITEM_TYPE_ID) & \\\n          (subject.item.level >= level)\n  return norm(sum(subject.item.quantity[owned]) / quantity)\n\ndef EquipItem(gs: GameState, subject: Group, item: type[Item], level: int, num_agent: int):\n  \"\"\"True if the number of agents that equip the item (_item_type, >=_level)\n     is greater than or equal to _num_agent.\n  \"\"\"\n  equipped = (subject.item.type_id == item.ITEM_TYPE_ID) & \\\n             (subject.item.level >= level) & \\\n             (subject.item.equipped > 0)\n  if num_agent > 0:\n    return norm(count(equipped) / num_agent)\n  return 1.0\n\ndef FullyArmed(gs: GameState, subject: Group,\n               combat_style: type[Skill], level: int, num_agent: int):\n  \"\"\"True if the number of fully equipped agents is greater than or equal to _num_agent\n       Otherwise false.\n       To determine fully equipped, we look at hat, top, bottom, weapon, ammo, respectively,\n       and see whether these are equipped and has level greater than or equal to _level.\n  \"\"\"\n  WEAPON_IDS = {\n    nmmo_skill.Melee: {'weapon':5, 'ammo':13}, # Spear, Whetstone\n    nmmo_skill.Range: {'weapon':6, 'ammo':14}, # Bow, Arrow\n    nmmo_skill.Mage: {'weapon':7, 'ammo':15} # Wand, Runes\n  }\n  item_ids = { 'hat':2, 'top':3, 'bottom':4 }\n  item_ids.update(WEAPON_IDS[combat_style])\n\n  lvl_flt = (subject.item.level >= level) & \\\n            (subject.item.equipped > 0)\n  type_flt = np.isin(subject.item.type_id,list(item_ids.values()))\n  _, equipment_numbers = np.unique(subject.item.owner_id[lvl_flt & type_flt],\n                                   return_counts=True)\n  if num_agent > 0:\n    return norm((equipment_numbers >= len(item_ids.items())).sum() / num_agent)\n  return 1.0\n\ndef ConsumeItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if total quantity consumed of item type above level is >= quantity\n  \"\"\"\n  type_flt = subject.event.CONSUME_ITEM.type == item.ITEM_TYPE_ID\n  lvl_flt = subject.event.CONSUME_ITEM.level >= level\n  return norm(subject.event.CONSUME_ITEM.number[type_flt & lvl_flt].sum() / quantity)\n\ndef HarvestItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if total quantity harvested of item type above level is >= quantity\n  \"\"\"\n  type_flt = subject.event.HARVEST_ITEM.type == item.ITEM_TYPE_ID\n  lvl_flt = subject.event.HARVEST_ITEM.level >= level\n  return norm(subject.event.HARVEST_ITEM.number[type_flt & lvl_flt].sum() / quantity)\n\ndef FireAmmo(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if total quantity consumed of item type above level is >= quantity\n  \"\"\"\n  type_flt = subject.event.FIRE_AMMO.type == item.ITEM_TYPE_ID\n  lvl_flt = subject.event.FIRE_AMMO.level >= level\n  return norm(subject.event.FIRE_AMMO.number[type_flt & lvl_flt].sum() / quantity)\n\ndef ListItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if total quantity listed of item type above level is >= quantity\n  \"\"\"\n  type_flt = subject.event.LIST_ITEM.type == item.ITEM_TYPE_ID\n  lvl_flt = subject.event.LIST_ITEM.level >= level\n  return norm(subject.event.LIST_ITEM.number[type_flt & lvl_flt].sum() / quantity)\n\ndef BuyItem(gs: GameState, subject: Group, item: type[Item], level: int, quantity: int):\n  \"\"\"True if total quantity purchased of item type above level is >= quantity\n  \"\"\"\n  type_flt = subject.event.BUY_ITEM.type == item.ITEM_TYPE_ID\n  lvl_flt = subject.event.BUY_ITEM.level >= level\n  return norm(subject.event.BUY_ITEM.number[type_flt & lvl_flt].sum() / quantity)\n\n\n############################################################################################\n# Below are used for the mini games, so these need to be fast\n\ndef ProgressTowardCenter(gs, subject):\n  if not any(a in gs.alive_agents for a in subject.agents):  # subject should be alive\n    return 0.0\n  center = gs.config.MAP_SIZE // 2\n  max_dist = center - gs.config.MAP_BORDER\n\n  r = subject.row\n  c = subject.col\n  # distance to the center tile, so dist = 0 when subject is on the center tile\n  if len(r) == 1:\n    dists = utils.linf_single((r[0], c[0]), (center, center))\n  else:\n    coords = np.hstack([r, c])\n    # NOTE: subject can be multiple agents (e.g., team), so taking the minimum\n    dists = np.min(utils.linf(coords, (center, center)))\n  return 1.0 - dists/max_dist\n\ndef AllMembersWithinRange(gs: GameState, subject: Group, dist: int):\n  \"\"\"True if the max l-inf distance of teammates is\n         less than or equal to dist\n  \"\"\"\n  if dist < 0 or \\\n     not any(a in gs.alive_agents for a in subject.agents):  # subject should be alive\n    return 0.0\n\n  max_dist = gs.config.MAP_CENTER\n  r = subject.row\n  c = subject.col\n  current_dist = max(r.max()-r.min(), c.max()-c.min())\n  if current_dist <= dist:\n    return 1.0\n\n  # progress bonus, which takes account of the overall distribution\n  max_dist_score = (max_dist - current_dist) / (max_dist - dist)\n  r_sd_score = dist / max(3*np.std(r), dist)  # becomes 1 if 3*std(r) < dist\n  c_sd_score = dist / max(3*np.std(c), dist)  # becomes 1 if 3*std(c) < dist\n  return (max_dist_score + r_sd_score + c_sd_score) / 3.0\n\ndef SeizeTile(gs: GameState, subject: Group, row: int, col: int, num_ticks: int,\n              progress_bonus = 0.4, seize_bonus = 0.3):\n  if not any(subject.health > 0):  # subject should be alive\n    return 0.0\n  target_tile = (row, col)\n\n  # When the subject seizes the target tile\n  if target_tile in gs.seize_status and gs.seize_status[target_tile][0] in subject.agents:\n    seize_duration = gs.current_tick - gs.seize_status[target_tile][1]\n    hold_bonus = (1.0 - progress_bonus - seize_bonus) * seize_duration/num_ticks\n    return norm(progress_bonus + seize_bonus + hold_bonus)\n\n  # motivate agents to seize the target tile\n  #max_dist = utils.linf_single(target_tile, gs.spawn_pos[subject.agents[0]])\n  max_dist = gs.config.MAP_CENTER // 2  # does not have to be precise\n  r = subject.row\n  c = subject.col\n  # distance to the center tile, so dist = 0 when subject is on the center tile\n  if len(r) == 1:\n    dists = utils.linf_single((r[0], c[0]), target_tile)\n  else:\n    coords = np.hstack([r.reshape(-1,1), c.reshape(-1,1)])\n    # NOTE: subject can be multiple agents (e.g., team), so taking the minimum\n    dists = np.min(utils.linf(coords, target_tile))\n\n  return norm(progress_bonus * (1.0 - dists/max_dist))\n\ndef SeizeCenter(gs: GameState, subject: Group, num_ticks: int,\n                progress_bonus = 0.3):\n  row = col = gs.config.MAP_SIZE // 2  # center tile\n  return SeizeTile(gs, subject, row, col, num_ticks, progress_bonus)\n\ndef SeizeQuadCenter(gs: GameState, subject: Group, num_ticks: int, quadrant: str,\n                    progress_bonus = 0.3):\n  center = gs.config.MAP_SIZE // 2\n  half_dist = gs.config.MAP_CENTER // 4\n  if quadrant == \"first\":\n    row = col = center + half_dist\n  elif quadrant == \"second\":\n    row, col = center - half_dist, center + half_dist\n  elif quadrant == \"third\":\n    row = col = center - half_dist\n  elif quadrant == \"fourth\":\n    row, col = center + half_dist, center - half_dist\n  else:\n    raise ValueError(f\"Invalid quadrant {quadrant}\")\n  return SeizeTile(gs, subject, row, col, num_ticks, progress_bonus)\n\ndef ProtectLeader(gs, subject, target_protect: int, target_destroy: Iterable[int]):\n  \"\"\"target_destory is not used for reward, but used as info for the reward wrapper\"\"\"\n  # Failed to protect the leader\n  if target_protect not in gs.alive_agents:\n    return 0\n\n  # Reward each tick the target is alive\n  return gs.current_tick / gs.config.HORIZON\n"
  },
  {
    "path": "nmmo/task/game_state.py",
    "content": "from __future__ import annotations\nfrom typing import Dict, Iterable, Tuple, MutableMapping, Set, List\nfrom dataclasses import dataclass, field\nfrom copy import deepcopy\nfrom collections import defaultdict\nimport weakref\n\nfrom abc import ABC, abstractmethod\nimport functools\nimport numpy as np\n\nfrom nmmo.core.config import Config\nfrom nmmo.core.realm import Realm\nfrom nmmo.core.observation import Observation\nfrom nmmo.task.group import Group\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.lib.event_log import EventState, ATTACK_COL_MAP, ITEM_COL_MAP, LEVEL_COL_MAP\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.systems.item import ItemState\nfrom nmmo.core.tile import TileState\n\nEntityAttr = EntityState.State.attr_name_to_col\nEntityAttrKeys = EntityAttr.keys()\nEventAttr = EventState.State.attr_name_to_col\nItemAttr = ItemState.State.attr_name_to_col\nTileAttr = TileState.State.attr_name_to_col\nEventAttr.update(ITEM_COL_MAP)\nEventAttr.update(ATTACK_COL_MAP)\nEventAttr.update(LEVEL_COL_MAP)\n\n@dataclass(frozen=True) # make gs read-only, except cache_result\nclass GameState:\n  current_tick: int\n  config: Config\n  spawn_pos: Dict[int, Tuple[int, int]] # ent_id: (row, col) of all spawned agents\n\n  alive_agents: Set[int] # of alive agents' ent_id (for convenience)\n  env_obs: Dict[int, Observation] # env passes the obs of only alive agents\n\n  entity_data: np.ndarray # a copied, whole Entity ds table\n  entity_index: Dict[int, Iterable] # precomputed index for where_in_1d\n  item_data: np.ndarray # a copied, whole Item ds table\n  item_index: Dict[int, Iterable]\n  event_data: np.ndarray # a copied, whole Event log table\n  event_index: Dict[int, Iterable]\n\n  # status of the seize target tiles (row, col) -> (ent_id, tick)\n  seize_status: Dict[Tuple[int, int], Tuple[int, int]]\n\n  cache_result: MutableMapping # cache for general memoization\n  _group_view: List[GroupView] = field(default_factory=list) # cache for GroupView\n\n  # add helper functions below\n  @functools.lru_cache\n  def entity_or_none(self, ent_id):\n    if ent_id not in self.entity_index:\n      return None\n    return EntityState.parse_array(self.entity_data[self.entity_index[ent_id]][0])\n\n  def where_in_id(self, data_type, subject: Iterable[int]):\n    k = (data_type, subject)\n    if k in self.cache_result:\n      return self.cache_result[k]\n\n    if data_type == 'entity':\n      flt_idx = [row for sbj in subject for row in self.entity_index.get(sbj,[])]\n      self.cache_result[k] = self.entity_data[flt_idx]\n    if data_type == 'item':\n      flt_idx = [row for sbj in subject for row in self.item_index.get(sbj,[])]\n      self.cache_result[k] = self.item_data[flt_idx]\n    if data_type == 'event':\n      flt_idx = [row for sbj in subject for row in self.event_index.get(sbj,[])]\n      self.cache_result[k] = self.event_data[flt_idx]\n    if data_type in ['entity', 'item', 'event']:\n      return self.cache_result[k]\n\n    raise ValueError(\"data_type must be in entity, item, event\")\n\n  def get_subject_view(self, subject: Group):\n    new_group_view = GroupView(self, subject)\n    self._group_view.append(new_group_view)\n    return new_group_view\n\n  def clear_cache(self):\n    # clear the cache, so that this object can be garbage collected\n    self.entity_or_none.cache_clear()  # pylint: disable=no-member\n    self.cache_result.clear()\n    self.alive_agents.clear()\n    while self._group_view:\n      weakref.ref(self._group_view.pop())  # clear the cache\n\n# Wrapper around an iterable datastore\nclass CachedProperty:\n  def __init__(self, func):\n    self.func = func\n    # Allows the instance keys to be garbage collected\n    #   when they are no longer referenced elsewhere\n    self.cache = weakref.WeakKeyDictionary()\n\n  def __get__(self, instance, owner):\n    if instance is None:\n      return self\n    if instance not in self.cache:\n      self.cache[instance] = self.func(instance)\n    return self.cache[instance]\n\nclass ArrayView(ABC):\n  def __init__(self,\n               mapping,\n               name: str,\n               gs: GameState,\n               subject: Group,\n               arr: np.ndarray):\n    self._mapping = mapping\n    self._name = name\n    self._gs = gs\n    self._subject = subject\n    self._hash = hash(subject) ^ hash(name)\n    self._arr = arr\n    self._cache = self._gs.cache_result\n\n  def __len__(self):\n    return len(self._arr)\n\n  @abstractmethod\n  def get_attribute(self, attr) -> np.ndarray:\n    raise NotImplementedError\n\n  def __getattr__(self, attr) -> np.ndarray:\n    k = (self._hash, attr)\n    if k in self._cache:\n      return self._cache[k]\n    v = object.__getattribute__(self, 'get_attribute')(attr)\n    self._cache[k] = v\n    return v\n\nclass ItemView(ArrayView):\n  def __init__(self, gs: GameState, subject: Group, arr: np.ndarray):\n    super().__init__(ItemAttr, 'item', gs, subject, arr)\n    self._mapping = ItemAttr\n\n  def get_attribute(self, attr) -> np.ndarray:\n    return self._arr[:, self._mapping[attr]]\n\nclass EntityView(ArrayView):\n  def __init__(self, gs: GameState, subject: Group, arr: np.ndarray):\n    super().__init__(EntityAttr, 'entity', gs, subject, arr)\n\n  def get_attribute(self, attr) -> np.ndarray:\n    return self._arr[:, self._mapping[attr]]\n\nclass EventView(ArrayView):\n  def __init__(self, gs: GameState, subject: Group, arr: np.ndarray):\n    super().__init__(EventAttr, 'event', gs, subject, arr)\n\n  def get_attribute(self, attr) -> np.ndarray:\n    assert hasattr(EventCode, attr), 'Invalid event code'\n    arr = self._arr[np.in1d(self._arr[:, EventAttr['event']],\n                                          getattr(EventCode, attr))]\n    return EventCodeView(attr, self._gs, self._subject, arr)\n\nclass TileView(ArrayView):\n  def __init__(self, gs: GameState, subject: Group, arr: np.ndarray):\n    super().__init__(TileAttr, 'tile', gs, subject, arr)\n\n  def get_attribute(self, attr) -> np.ndarray:\n    return [o[:, self._mapping[attr]] for o in self._arr]\n\nclass EventCodeView(ArrayView):\n  def __init__(self,\n               name: str,\n               gs: GameState,\n               subject: Group,\n               arr: np.ndarray):\n    super().__init__(EventAttr, name, gs, subject, arr)\n\n  def get_attribute(self, attr) -> np.ndarray:\n    return self._arr[:, self._mapping[attr]]\n\n# Group\nclass GroupObsView:\n  def __init__(self, gs: GameState, subject: Group):\n    self._gs = gs\n\n    valid_agents = filter(lambda eid: eid in gs.env_obs,subject.agents)\n    self._obs = [gs.env_obs[ent_id] for ent_id in valid_agents]\n    self._subject = subject\n\n  @CachedProperty\n  def tile(self):\n    return TileView(self._gs, self._subject, [o.tiles for o in self._obs])\n\n  def __getattr__(self, attr):\n    return [getattr(o, attr) for o in self._obs]\n\nclass GroupView:\n  def __init__(self, gs: GameState, subject: Group):\n    self._gs = gs\n    self._subject = subject\n    self._subject_hash = hash(subject)\n\n  @CachedProperty\n  def obs(self):\n    return GroupObsView(self._gs, self._subject)\n\n  @CachedProperty\n  def _sbj_ent(self):\n    return self._gs.where_in_id('entity', self._subject.agents)\n\n  @CachedProperty\n  def entity(self):\n    return EntityView(self._gs, self._subject, self._sbj_ent)\n\n  @CachedProperty\n  def _sbj_item(self):\n    return self._gs.where_in_id('item', self._subject.agents)\n\n  @CachedProperty\n  def item(self):\n    return ItemView(self._gs, self._subject, self._sbj_item)\n\n  @CachedProperty\n  def _sbj_event(self):\n    return self._gs.where_in_id('event', self._subject.agents)\n\n  @CachedProperty\n  def event(self):\n    return EventView(self._gs, self._subject, self._sbj_event)\n\n  def __getattribute__(self, attr):\n    if attr in {'_gs','_subject','_sbj_ent','_sbj_item',\n    'entity','item','event','obs', '_subject_hash'}:\n      return object.__getattribute__(self, attr)\n\n    # Cached optimization\n    k = (self._subject_hash, attr)\n    cache = self._gs.cache_result\n    if k in cache:\n      return cache[k]\n\n    try:\n      # Get property\n      if attr in EntityAttrKeys:\n        v = getattr(self.entity, attr)\n      else:\n        v = object.__getattribute__(self, attr)\n      cache[k] = v\n      return v\n    except AttributeError:\n      # View behavior\n      return object.__getattribute__(self._gs, attr)\n\nclass GameStateGenerator:\n  def __init__(self, realm: Realm, config: Config):\n    self.config = deepcopy(config)\n    self.spawn_pos: Dict[int, Tuple[int, int]] = {}\n\n    for ent_id, ent in realm.players.items():\n      self.spawn_pos.update( {ent_id: ent.pos} )\n\n  def generate(self, realm: Realm, env_obs: Dict[int, Observation]) -> GameState:\n    # copy the datastore, by running astype\n    entity_all = EntityState.Query.table(realm.datastore).copy()\n    alive_agents = entity_all[:, EntityAttr[\"id\"]]\n    alive_agents = set(alive_agents[alive_agents > 0])\n    item_data = ItemState.Query.table(realm.datastore).copy()\n    event_data = EventState.Query.table(realm.datastore).copy()\n    return GameState(\n      current_tick = realm.tick,\n      config = self.config,\n      spawn_pos = self.spawn_pos,\n      alive_agents = alive_agents,\n      env_obs = env_obs,\n      entity_data = entity_all,\n      entity_index = precompute_index(entity_all, EntityAttr[\"id\"]),\n      item_data = item_data,\n      item_index = precompute_index(item_data, ItemAttr[\"owner_id\"]),\n      event_data = event_data,\n      event_index = precompute_index(event_data, EventAttr['ent_id']),\n      seize_status = realm.seize_status,\n      cache_result = {}\n    )\n\ndef precompute_index(table, id_col):\n  index = defaultdict()\n  for row, id_ in enumerate(table[:,id_col]):\n    if id_ in index:\n      index[id_].append(row)\n    else:\n      index[id_] = [row]\n  return index\n"
  },
  {
    "path": "nmmo/task/group.py",
    "content": "from __future__ import annotations\nfrom typing import Dict, Union, Iterable, TYPE_CHECKING\nfrom collections import OrderedDict\nfrom collections.abc import Set, Sequence\nimport weakref\n\nif TYPE_CHECKING:\n  from nmmo.task.game_state import GameState, GroupView\n\nclass Group(Sequence, Set):\n  ''' An immutable, ordered, unique group of agents involved in a task\n  '''\n  def __init__(self,\n               agents: Union(Iterable[int], int),\n               name: str=None):\n\n    if isinstance(agents, int):\n      agents = (agents,)\n    assert len(agents) > 0, \"Team must have at least one agent\"\n    self.name = name if name else f\"Agent({','.join([str(e) for e in agents])})\"\n    # Remove duplicates\n    self._agents = tuple(OrderedDict.fromkeys(sorted(agents)).keys())\n    if not isinstance(self._agents,tuple):\n      self._agents = (self._agents,)\n\n    self._sd: GroupView = None\n    self._gs: GameState = None\n\n    self._hash = hash(self._agents)\n\n  @property\n  def agents(self):\n    return self._agents\n\n  def union(self, o: Group):\n    return Group(self._agents + o.agents)\n\n  def intersection(self, o: Group):\n    return Group(set(self._agents).intersection(set(o.agents)))\n\n  def __eq__(self, o):\n    return self._agents == o\n\n  def __len__(self):\n    return len(self._agents)\n\n  def __hash__(self):\n    return self._hash\n\n  def __getitem__(self, key):\n    if len(self) == 1 and key == 0:\n      return self\n    return Group((self._agents[key],), f\"{self.name}.{key}\")\n\n  def __contains__(self, key):\n    if isinstance(key, int):\n      return key in self.agents\n    return Sequence.__contains__(self, key)\n\n  def __str__(self) -> str:\n    return str(self._agents)\n\n  def __int__(self) -> int:\n    assert len(self._agents) == 1, \"Group is not a singleton\"\n    return int(self._agents[0])\n\n  def __copy__(self):\n    return self\n  def __deepcopy__(self, memo):\n    return Group(self.agents, self.name)\n\n  def description(self) -> Dict:\n    return {\n      \"type\": \"Group\",\n      \"name\": self.name,\n      \"agents\": self._agents\n    }\n\n  def clear_prev_state(self) -> None:\n    if self._gs is not None:\n      self._gs.clear_cache()  # prevent memory leak\n      self._gs = None\n    if self._sd is not None:\n      weakref.ref(self._sd)  # prevent memory leak\n      self._sd = None\n\n  def update(self, gs: GameState) -> None:\n    self.clear_prev_state()\n    self._gs = gs\n    self._sd = gs.get_subject_view(self)\n\n  def __getattr__(self, attr):\n    return self._sd.__getattribute__(attr)\n\ndef union(*groups: Group) -> Group:\n  \"\"\" Performs a big union over groups\n  \"\"\"\n  agents = []\n  for group in groups:\n    for agent in group.agents:\n      agents.append(agent)\n  return Group(agents)\n\ndef complement(group: Group, universe: Group) -> Group:\n  \"\"\" Returns the complement of group in universe\n  \"\"\"\n  agents = []\n  for agent in universe.agents:\n    if not agent in group:\n      agents.append(agent)\n  return Group(agents)\n"
  },
  {
    "path": "nmmo/task/predicate_api.py",
    "content": "from __future__ import annotations\nfrom typing import Callable, List, Optional, Union, Iterable, Type, TYPE_CHECKING\nfrom types import FunctionType\nfrom abc import ABC, abstractmethod\nimport inspect\nfrom numbers import Real\n\nfrom nmmo.core.config import Config\nfrom nmmo.task.group import Group, union\nfrom nmmo.task.game_state import GameState\n\nif TYPE_CHECKING:\n  from nmmo.task.task_api import Task\n\nclass InvalidPredicateDefinition(Exception):\n  pass\n\nclass Predicate(ABC):\n  \"\"\" A mapping from a game state to bounded [0, 1] float\n  \"\"\"\n  def __init__(self,\n               subject: Group,\n               *args,\n               **kwargs):\n    self.name = self._make_name(self.__class__.__name__, args, kwargs)\n\n    self._groups: List[Group] = [x for x in list(args) + list(kwargs.values())\n                                 if isinstance(x, Group)]\n\n    self._groups.append(subject)\n\n    self._args = args\n    self._kwargs = kwargs\n    self._config = None\n    self._subject = subject\n\n  def __call__(self, gs: GameState) -> float:\n    \"\"\" Calculates score\n\n    Params:\n      gs: GameState\n\n    Returns:\n      progress: float bounded between [0, 1], 1 is considered to be true\n    \"\"\"\n    # Update views\n    for group in self._groups:\n      group.update(gs)\n    # Calculate score\n    cache = gs.cache_result\n    if self.name in cache:\n      progress = cache[self.name]\n    else:\n      progress = max(min(float(self._evaluate(gs)),1.0),0.0)\n      cache[self.name] = progress\n    return progress\n\n  def close(self):\n    # To prevent memory leak, clear all refs to old game state\n    for group in self._groups:\n      group.clear_prev_state()\n\n  @abstractmethod\n  def _evaluate(self, gs: GameState) -> float:\n    \"\"\" A mapping from a game state to the desirability/progress of that state.\n        __call__() will cap its value to [0, 1]\n    \"\"\"\n    raise NotImplementedError\n\n  def _make_name(self, class_name, args, kwargs) -> str:\n    name = [class_name] + \\\n      list(map(arg_to_string, args)) + \\\n      [f\"{arg_to_string(key)}:{arg_to_string(arg)}\" for key, arg in kwargs.items()]\n    name = \"(\"+'_'.join(name).replace(' ', '')+\")\"\n    return name\n\n  def __str__(self):\n    return self.name\n\n  @abstractmethod\n  def get_source_code(self) -> str:\n    \"\"\" Returns the actual source code how the game state/progress evaluation is done.\n    \"\"\"\n    raise NotImplementedError\n\n  @abstractmethod\n  def get_signature(self) -> List:\n    \"\"\" Returns the signature of the game state/progress evaluation function.\n    \"\"\"\n    raise NotImplementedError\n\n  @property\n  def args(self):\n    return self._args\n\n  @property\n  def kwargs(self):\n    return self._kwargs\n\n  @property\n  def subject(self):\n    return self._subject\n\n  def create_task(self,\n                  task_cls: Optional[Type[Task]]=None,\n                  assignee: Union[Iterable[int], int]=None,\n                  **kwargs) -> Task:\n    \"\"\" Creates a task from this predicate\"\"\"\n    if task_cls is None:\n      from nmmo.task.task_api import Task\n      task_cls = Task\n\n    if assignee is None:\n      # the new task is assigned to this predicate's subject\n      assignee = self._subject.agents\n\n    return task_cls(eval_fn=self, assignee=assignee, **kwargs)\n\n  def __and__(self, other):\n    return AND(self, other)\n  def __or__(self, other):\n    return OR(self, other)\n  def __invert__(self):\n    return NOT(self)\n  def __add__(self, other):\n    return ADD(self, other)\n  def __radd__(self, other):\n    return ADD(self, other)\n  def __sub__(self, other):\n    return SUB(self, other)\n  def __rsub__(self, other):\n    return SUB(self, other)\n  def __mul__(self, other):\n    return MUL(self, other)\n  def __rmul__(self, other):\n    return MUL(self, other)\n\n# _make_name helper functions\ndef arg_to_string(arg):\n  if isinstance(arg, (type, FunctionType)): # class or function\n    return arg.__name__\n  if arg is None:\n    return 'Any'\n  return str(arg)\n\n################################################\n\ndef make_predicate(fn: Callable) -> Type[Predicate]:\n  \"\"\" Syntactic sugar API for defining predicates from function\n  \"\"\"\n  signature = inspect.signature(fn)\n  for i, param in enumerate(signature.parameters.values()):\n    if i == 0 and param.name != 'gs':\n      raise InvalidPredicateDefinition('First parameter must be gs: GameState')\n    if i == 1 and (param.name != 'subject'):\n      raise InvalidPredicateDefinition(\"Second parameter must be subject: Group\")\n\n  class FunctionPredicate(Predicate):\n    def __init__(self, *args, **kwargs) -> None:\n      self._signature = signature\n      super().__init__(*args, **kwargs)\n      self._args = args\n      self._kwargs = kwargs\n      self.name = self._make_name(fn.__name__, args, kwargs)\n    def _evaluate(self, gs: GameState) -> float:\n      return float(fn(gs, *self._args, **self._kwargs))\n    def get_source_code(self):\n      return inspect.getsource(fn).strip()\n    def get_signature(self) -> List:\n      return list(self._signature.parameters)\n\n  return FunctionPredicate\n\n\n################################################\nclass PredicateOperator(Predicate):\n  def __init__(self, n, *predicates: Union[Predicate, Real], subject: Group=None):\n    if not n(len(predicates)):\n      raise InvalidPredicateDefinition(f\"Need {n} arguments\")\n    predicates = list(predicates)\n    self._subject_argument = subject\n    if subject is None:\n      subject = union(*[p.subject\n                        for p in filter(lambda p: isinstance(p, Predicate), predicates)])\n    super().__init__(subject, *predicates)\n\n    for i, p in enumerate(predicates):\n      if isinstance(p, Real):\n        predicates[i] = lambda _,v=predicates[i] : v\n    self._predicates = predicates\n\n  def check(self, config: Config) -> bool:\n    return all((p.check(config) if isinstance(p, Predicate)\n                else True for p in self._predicates))\n\n  def sample(self, config: Config, cls: Type[PredicateOperator], **kwargs):\n    subject = self._subject_argument if 'subject' not in kwargs else kwargs['subject']\n    predicates = [p.sample(config, **kwargs) if isinstance(p, Predicate)\n                  else p(None) for p in self._predicates]\n    return cls(*predicates, subject=subject)\n\n  def get_source_code(self) -> str:\n    # NOTE: get_source_code() of the combined predicates returns the joined str\n    #   of each predicate's source code, which may NOT represent what the actual\n    #   predicate is doing\n    # TODO: try to generate \"the source code\" that matches\n    #   what the actual instantiated predicate returns,\n    #   which perhaps should reflect the actual agent ids, etc...\n    src_list = []\n    for pred in self._predicates:\n      if isinstance(pred, Predicate):\n        src_list.append(pred.get_source_code())\n    return '\\n\\n'.join(src_list).strip()\n\n  def get_signature(self):\n    # TODO: try to generate the correct signature\n    return []\n\n  @property\n  def args(self):\n    # TODO: try to generate the correct args\n    return []\n\n  @property\n  def kwargs(self):\n    # NOTE: This is incorrect implementation. kwargs of the combined predicates returns\n    #   all summed kwargs dict, which can OVERWRITE the values of duplicated keys\n    # TODO: try to match the eval function and kwargs, which can be correctly used downstream\n    # for pred in self._predicates:\n    #   if isinstance(pred, Predicate):\n    #     kwargs.update(pred.kwargs)\n    return {}\n\nclass OR(PredicateOperator, Predicate):\n  def __init__(self, *predicates: Predicate, subject: Group=None):\n    super().__init__(lambda n: n>0, *predicates, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    # using max as OR for the [0,1] float\n    return max(p(gs) for p in self._predicates)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, OR, **kwargs)\n\nclass AND(PredicateOperator, Predicate):\n  def __init__(self, *predicates: Predicate, subject: Group=None):\n    super().__init__(lambda n: n>0, *predicates, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    # using min as AND for the [0,1] float\n    return min(p(gs) for p in self._predicates)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, AND, **kwargs)\n\nclass NOT(PredicateOperator, Predicate):\n  def __init__(self, predicate: Predicate, subject: Group=None):\n    super().__init__(lambda n: n==1, predicate, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    return 1.0 - self._predicates[0](gs)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, NOT, **kwargs)\n\nclass ADD(PredicateOperator, Predicate):\n  def __init__(self, *predicate: Union[Predicate, Real], subject: Group=None):\n    super().__init__(lambda n: n>0, *predicate, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    return max(min(sum(p(gs) for p in self._predicates),1.0),0.0)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, ADD, **kwargs)\n\nclass SUB(PredicateOperator, Predicate):\n  def __init__(self, p: Predicate, q: Union[Predicate, Real], subject: Group=None):\n    super().__init__(lambda n: n==2, p,q, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    return max(min(self._predicates[0](gs)-self._predicates[1](gs),1.0),0.0)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, SUB, **kwargs)\n\nclass MUL(PredicateOperator, Predicate):\n  def __init__(self, *predicate: Union[Predicate, Real], subject: Group=None):\n    super().__init__(lambda n: n>0, *predicate, subject=subject)\n  def _evaluate(self, gs: GameState) -> float:\n    result = 1.0\n    for p in self._predicates:\n      result = result * p(gs)\n    return max(min(result,1.0),0.0)\n  def sample(self, config: Config, **kwargs):\n    return super().sample(config, MUL, **kwargs)\n"
  },
  {
    "path": "nmmo/task/task_api.py",
    "content": "# pylint: disable=unused-import,attribute-defined-outside-init\nfrom typing import Callable, Iterable, Dict, List, Union, Tuple, Type\nfrom types import FunctionType\nfrom abc import ABC\nimport inspect\nimport numpy as np\n\nfrom nmmo.task.group import Group\nfrom nmmo.task.game_state import GameState\nfrom nmmo.task.predicate_api import Predicate, make_predicate, arg_to_string\nfrom nmmo.task import base_predicates as bp\n\nclass Task(ABC):\n  \"\"\" A task is used to calculate rewards for agents in assignee\n      based on the predicate and game state\n  \"\"\"\n  def __init__(self,\n               eval_fn: Callable,\n               assignee: Union[Iterable[int], int],\n               reward_multiplier = 1.0,\n               embedding = None,\n               spec_name: str = None,\n               reward_to = None,\n               tags: List[str] = None):\n    if isinstance(assignee, int):\n      self._assignee = (assignee,)\n    else:\n      assert len(assignee) > 0, \"Assignee cannot be empty\"\n      self._assignee = tuple(set(assignee)) # dedup\n    self._eval_fn = eval_fn\n    self._reward_multiplier = reward_multiplier\n    self._embedding = None if embedding is None else np.array(embedding, dtype=np.float16)\n    # These are None if not created using TaskSpec\n    self.spec_name, self.reward_to, self.tags = spec_name, reward_to, tags\n    self.name = self._make_name(self.__class__.__name__,\n                                eval_fn=eval_fn, assignee=self._assignee)\n    self.reset()\n\n  def reset(self):\n    self._stop_eval = False\n    self._last_eval_tick = None\n    self._progress = 0.0\n    self._completed_tick = None\n    self._max_progress = 0.0\n    self._positive_reward_count = 0\n    self._negative_reward_count = 0\n\n  def close(self):\n    if self._stop_eval is False:\n      if isinstance(self._eval_fn, Predicate):\n        self._eval_fn.close()\n      self._stop_eval = True\n\n  @property\n  def assignee(self) -> Tuple[int]:\n    return self._assignee\n\n  @property\n  def completed(self) -> bool:\n    return self._completed_tick is not None\n\n  @property\n  def progress(self) -> float:\n    return self._progress\n\n  @property\n  def reward_multiplier(self) -> float:\n    return self._reward_multiplier\n\n  @property\n  def reward_signal_count(self) -> int:\n    return self._positive_reward_count + self._negative_reward_count\n\n  @property\n  def embedding(self):\n    return self._embedding\n\n  def set_embedding(self, embedding):\n    self._embedding = embedding\n\n  def _map_progress_to_reward(self, gs: GameState) -> float:\n    \"\"\" The default reward is the diff between the old and new progress.\n        Once the task is completed, no more reward is provided.\n\n        Override this function to create a custom reward function\n    \"\"\"\n    if self.completed:\n      return 0.0\n\n    new_progress = max(min(float(self._eval_fn(gs)),1.0),0.0)\n    diff = new_progress - self._progress\n    self._progress = new_progress\n    if self._progress >= 1:\n      self._completed_tick = gs.current_tick\n\n    return diff\n\n  def compute_rewards(self, gs: GameState) -> Tuple[Dict[int, float], Dict[int, Dict]]:\n    \"\"\" Environment facing API\n\n    Returns rewards and infos for all agents in subject\n    \"\"\"\n    reward = self._map_progress_to_reward(gs) * self._reward_multiplier\n    self._last_eval_tick = gs.current_tick\n    self._max_progress = max(self._max_progress, self._progress)\n    self._positive_reward_count += int(reward > 0)\n    self._negative_reward_count += int(reward < 0)\n    rewards = {int(ent_id): reward for ent_id in self._assignee}\n    infos = {int(ent_id): {\"task_spec\": self.spec_name,\n                           \"reward\": reward,\n                           \"progress\": self._progress,\n                           \"completed\": self.completed}\n             for ent_id in self._assignee}\n\n    # NOTE: tasks do not know whether assignee agents are alive or dead\n    #   so the Env must check it before filling in rewards and infos\n    return rewards, infos\n\n  def _make_name(self, class_name, **kwargs) -> str:\n    name = [class_name] + \\\n      [f\"{arg_to_string(key)}:{arg_to_string(arg)}\" for key, arg in kwargs.items()]\n    name = \"(\"+\"_\".join(name).replace(\" \", \"\")+\")\"\n    return name\n\n  def __str__(self):\n    return self.name\n\n  @property\n  def subject(self):\n    if isinstance(self._eval_fn, Predicate):\n      return self._eval_fn.subject.agents\n    return self.assignee\n\n  def get_source_code(self):\n    if isinstance(self._eval_fn, Predicate):\n      return self._eval_fn.get_source_code()\n    return inspect.getsource(self._eval_fn).strip()\n\n  def get_signature(self):\n    if isinstance(self._eval_fn, Predicate):\n      return self._eval_fn.get_signature()\n    signature = inspect.signature(self._eval_fn)\n    return list(signature.parameters)\n\n  @property\n  def args(self):\n    if isinstance(self._eval_fn, Predicate):\n      return self._eval_fn.args\n    # the function _eval_fn must only take gs\n    return []\n\n  @property\n  def kwargs(self):\n    if isinstance(self._eval_fn, Predicate):\n      return self._eval_fn.kwargs\n    # the function _eval_fn must only take gs\n    return {}\n\n  @property\n  def progress_info(self):\n    return {\n      \"task_spec_name\": self.spec_name,\n      \"last_eval_tick\": self._last_eval_tick,\n      \"completed\": self.completed,\n      \"completed_tick\": self._completed_tick,\n      \"max_progress\": self._max_progress,\n      \"positive_reward_count\": self._positive_reward_count,\n      \"negative_reward_count\": self._negative_reward_count,\n      \"reward_signal_count\": self.reward_signal_count,\n    }\n\nclass OngoingTask(Task):\n  def _map_progress_to_reward(self, gs: GameState) -> float:\n    \"\"\"Keep returning the progress reward after the task is completed.\n       However, this task tracks the completion status in the same manner.\n    \"\"\"\n    self._progress = max(min(self._eval_fn(gs)*1.0,1.0),0.0)\n    if self._progress >= 1 and self._completed_tick is None:\n      self._completed_tick = gs.current_tick\n    return self._progress\n\nclass HoldDurationTask(Task):\n  def __init__(self,\n               eval_fn: Callable,\n               assignee: Union[Iterable[int], int],\n               hold_duration: int,\n               **kwargs):\n    super().__init__(eval_fn, assignee, **kwargs)\n    self._hold_duration = hold_duration\n    self._reset_timer()\n\n  def _reset_timer(self):\n    self._timer = 0\n    self._last_success_tick = 0\n\n  def reset(self):\n    super().reset()\n    self._reset_timer()\n\n  def _map_progress_to_reward(self, gs: GameState) -> float:\n    # pylint: disable=attribute-defined-outside-init\n    if self.completed:\n      return 0.0\n\n    curr_eval = max(min(self._eval_fn(gs)*1.0,1.0),0.0)\n    if curr_eval < 1:\n      self._reset_timer()\n    else:\n      self._timer += 1\n      self._last_success_tick = gs.current_tick\n\n    new_progress = self._timer / self._hold_duration\n    diff = new_progress - self._progress\n    self._progress = new_progress\n    if self._progress >= 1 and self._completed_tick is None:\n      self._completed_tick = gs.current_tick\n      diff = 1.0  # give out the max reward when task is completed\n\n    return diff\n\n######################################################################\n\n# The same task is assigned each agent in agent_list individually\n#   with the agent as the predicate subject and task assignee\ndef make_same_task(pred_cls: Union[Type[Predicate], Callable],\n                   agent_list: Iterable[int],\n                   pred_kwargs=None,\n                   task_cls: Type[Task]=Task,\n                   task_kwargs=None) -> List[Task]:\n  # if a function is provided, make it a predicate class\n  if isinstance(pred_cls, FunctionType):\n    pred_cls = make_predicate(pred_cls)\n  if pred_kwargs is None:\n    pred_kwargs = {}\n  if task_kwargs is None:\n    task_kwargs = {}\n\n  task_list = []\n  for agent_id in agent_list:\n    predicate = pred_cls(Group(agent_id), **pred_kwargs)\n    task_list.append(predicate.create_task(task_cls=task_cls, **task_kwargs))\n  return task_list\n\ndef nmmo_default_task(agent_list: Iterable[int], test_mode=None) -> List[Task]:\n  # (almost) no overhead in env._compute_rewards()\n  if test_mode == \"no_task\":\n    return []\n\n  # eval function on Predicate class, but does not use Group during eval\n  if test_mode == \"dummy_eval_fn\":\n    # pylint: disable=unused-argument\n    return make_same_task(lambda gs, subject: True, agent_list, task_cls=OngoingTask)\n\n  return make_same_task(bp.TickGE, agent_list)\n"
  },
  {
    "path": "nmmo/task/task_spec.py",
    "content": "import functools\nfrom dataclasses import dataclass, field\nfrom typing import Iterable, Dict, List, Union, Type\nfrom types import FunctionType\nfrom copy import deepcopy\nfrom tqdm import tqdm\n\nimport numpy as np\n\nimport nmmo\nfrom nmmo.task.task_api import Task, make_same_task\nfrom nmmo.task.predicate_api import Predicate, make_predicate\nfrom nmmo.task.group import Group\nfrom nmmo.task import base_predicates as bp\nfrom nmmo.lib.team_helper import TeamHelper\n\n\"\"\" task_spec\n\n    eval_fn can come from the base_predicates.py or could be custom functions like above\n    eval_fn_kwargs are the additional args that go into predicate. There are also special keys\n      * \"target\" must be [\"left_team\", \"right_team\", \"left_team_leader\", \"right_team_leader\"]\n          these str will be translated into the actual agent ids\n\n    task_cls specifies the task class to be used. Default is Task.\n    task_kwargs are the optional, additional args that go into the task.\n \n    reward_to: must be in [\"team\", \"agent\"]\n      * \"team\" create a single team task, in which all team members get rewarded\n      * \"agent\" create a task for each agent, in which only the agent gets rewarded\n    \n    sampling_weight specifies the weight of the task in the curriculum sampling. Default is 1\n\"\"\"\n\nREWARD_TO = [\"agent\", \"team\"]\nVALID_TARGET = [\"left_team\", \"left_team_leader\", \"right_team\", \"right_team_leader\",\n                \"my_team_leader\", \"all_foes\", \"all_foe_leaders\"]\n\n@dataclass\nclass TaskSpec:\n  eval_fn: FunctionType\n  eval_fn_kwargs: Dict\n  task_cls: Type[Task] = Task\n  task_kwargs: Dict = field(default_factory=dict)\n  reward_to: str = \"agent\"\n  sampling_weight: float = 1.0\n  embedding: np.ndarray = None\n  predicate: Predicate = None\n  tags: List[str] = field(default_factory=list)\n\n  def __post_init__(self):\n    if self.predicate is None:\n      assert isinstance(self.eval_fn, FunctionType), \\\n        \"eval_fn must be a function\"\n    else:\n      assert self.eval_fn is None, \"Cannot specify both eval_fn and predicate\"\n    assert self.reward_to in REWARD_TO, \\\n      f\"reward_to must be in {REWARD_TO}\"\n    if \"target\" in self.eval_fn_kwargs:\n      assert self.eval_fn_kwargs[\"target\"] in VALID_TARGET, \\\n      f\"target must be in {VALID_TARGET}\"\n\n  @functools.cached_property\n  def name(self):\n    # pylint: disable=no-member\n    kwargs_str = []\n    for key, val in self.eval_fn_kwargs.items():\n      val_str = str(val)\n      if isinstance(val, type):\n        val_str = val.__name__\n      kwargs_str.append(f\"{key}:{val_str}_\")\n    kwargs_str = \"(\" + \"\".join(kwargs_str)[:-1] + \")\" # remove the last _\n    pred_name = self.eval_fn.__name__ if self.predicate is None else self.predicate.name\n    return \"_\".join([self.task_cls.__name__, pred_name,\n                     kwargs_str, \"reward_to:\" + self.reward_to])\n\ndef make_task_from_spec(assign_to: Union[Iterable[int], Dict],\n                        task_spec: List[TaskSpec]) -> List[Task]:\n  \"\"\"\n  Args:\n    assign_to: either a Dict with { team_id: [agent_id]} or a List of agent ids\n    task_spec: a list of tuples (reward_to, eval_fn, pred_fn_kwargs, task_kwargs)\n    \n    each tuple is assigned to the teams\n  \"\"\"\n  teams = assign_to\n  if not isinstance(teams, Dict): # convert agent id list to the team dict format\n    teams = {idx: [agent_id] for idx, agent_id in enumerate(assign_to)}\n  team_list = list(teams.keys())\n  team_helper = TeamHelper(teams)\n\n  # assign task spec to teams (assign_to)\n  tasks = []\n  for idx in range(min(len(team_list), len(task_spec))):\n    team_id = team_list[idx]\n\n    # map local vars to spec attributes\n    reward_to = task_spec[idx].reward_to\n    pred_fn = task_spec[idx].eval_fn\n    pred_fn_kwargs = deepcopy(task_spec[idx].eval_fn_kwargs)\n    task_cls = task_spec[idx].task_cls\n    task_kwargs = deepcopy(task_spec[idx].task_kwargs)\n    task_kwargs[\"embedding\"] = task_spec[idx].embedding # to pass to task_cls\n    task_kwargs[\"spec_name\"] = task_spec[idx].name\n    task_kwargs[\"reward_to\"] = task_spec[idx].reward_to\n    task_kwargs[\"tags\"] = task_spec[idx].tags\n    predicate = task_spec[idx].predicate\n\n    # reserve \"target\" for relative agent mapping\n    target_keys = [key for key in pred_fn_kwargs.keys() if key.startswith(\"target\")]\n    for key in target_keys:\n      target_keyword = pred_fn_kwargs.pop(key)\n      assert target_keyword in VALID_TARGET, \"Invalid target\"\n      # translate target to specific agent ids using team_helper\n      target_ent = team_helper.get_target_agent(team_id, target_keyword)\n      pred_fn_kwargs[key] = target_ent\n\n    # handle some special cases and instantiate the predicate first\n    if pred_fn is not None and isinstance(pred_fn, FunctionType):\n      # if a function is provided as a predicate\n      pred_cls = make_predicate(pred_fn)\n\n    # TODO: should create a test for these\n    if (pred_fn in [bp.AllDead]) or \\\n       (pred_fn in [bp.StayAlive] and \"target\" in pred_fn_kwargs):\n      # use the target as the predicate subject\n      target_ent = pred_fn_kwargs.pop(\"target\") # remove target\n      predicate = pred_cls(Group(target_ent), **pred_fn_kwargs)\n\n    # create the task\n    if reward_to == \"team\":\n      assignee = team_helper.teams[team_id]\n      if predicate is None:\n        predicate = pred_cls(Group(assignee), **pred_fn_kwargs)\n        tasks.append(predicate.create_task(task_cls=task_cls, **task_kwargs))\n      else:\n        # this branch is for the cases like AllDead, StayAlive\n        tasks.append(predicate.create_task(assignee=assignee, task_cls=task_cls,\n                                           **task_kwargs))\n\n    elif reward_to == \"agent\":\n      agent_list = team_helper.teams[team_id]\n      if predicate is None:\n        tasks += make_same_task(pred_cls, agent_list, pred_kwargs=pred_fn_kwargs,\n                                task_cls=task_cls, task_kwargs=task_kwargs)\n      else:\n        # this branch is for the cases like AllDead, StayAlive\n        tasks += [predicate.create_task(assignee=agent_id, task_cls=task_cls, **task_kwargs)\n                  for agent_id in agent_list]\n\n  return tasks\n\n# pylint: disable=bare-except,cell-var-from-loop\ndef check_task_spec(spec_list: List[TaskSpec], debug=False) -> List[Dict]:\n  teams = {0: [1, 2, 3], 3: [4, 5], 7: [6, 7], 11: [8, 9], 14: [10, 11]}\n  config = nmmo.config.Default()\n  config.set(\"PLAYER_N\", 11)\n  config.set(\"TEAMS\", teams)\n  env = nmmo.Env(config)\n  results = []\n  for single_spec in tqdm(spec_list):\n    result = {\"spec_name\": single_spec.name}\n    try:\n      env.reset(make_task_fn=lambda: make_task_from_spec(teams, [single_spec]))\n      for _ in range(3):\n        env.step({})\n      result[\"runnable\"] = True\n    except:\n      result[\"runnable\"] = False\n      if debug:\n        raise\n    results.append(result)\n  return results\n"
  },
  {
    "path": "nmmo/version.py",
    "content": "__version__ = '2.1.2'\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"wheel\", \"cython\", \"numpy==1.23.3\"]"
  },
  {
    "path": "scripted/__init__.py",
    "content": ""
  },
  {
    "path": "scripted/attack.py",
    "content": "# pylint: disable=invalid-name, unused-argument\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core.observation import Observation\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.lib import utils\n\n\ndef closestTarget(config, ob: Observation):\n  shortestDist = np.inf\n  closestAgent = None\n\n  agent  = ob.agent\n  start = (agent.row, agent.col)\n\n  for target_ent in ob.entities.values:\n    target_ent = EntityState.parse_array(target_ent)\n    if target_ent.id == agent.id:\n      continue\n\n    dist = utils.linf_single(start, (target_ent.row, target_ent.col))\n    if dist < shortestDist and dist != 0:\n      shortestDist = dist\n      closestAgent = target_ent\n\n  if closestAgent is None:\n    return None, None\n\n  return closestAgent, shortestDist\n\ndef attacker(config, ob: Observation):\n  agent = ob.agent\n\n  attacker_id = agent.attacker_id\n  if attacker_id == 0:\n    return None, None\n\n  target_ent = ob.entity(attacker_id)\n  if target_ent is None:\n    return None, None\n\n  return target_ent,\\\n         utils.linf_single((agent.row, agent.col), (target_ent.row, target_ent.col))\n\ndef target(config, actions, style, targetID):\n  actions[nmmo.action.Attack] = {\n        nmmo.action.Style: style,\n        nmmo.action.Target: targetID}\n"
  },
  {
    "path": "scripted/baselines.py",
    "content": "# pylint: disable=invalid-name, attribute-defined-outside-init, no-member\nfrom typing import Dict\nfrom collections import defaultdict\n\nimport nmmo\nfrom nmmo import material\nfrom nmmo.systems import skill\nimport nmmo.systems.item as item_system\nfrom nmmo.lib import colors\nfrom nmmo.core import action\nfrom nmmo.core.observation import Observation\n\nfrom scripted import attack, move\n\n\nclass Scripted(nmmo.Scripted):\n  '''Template class for baseline scripted models.\n\n  You may either subclass directly or mirror the __call__ function'''\n  scripted = True\n  color    = colors.Neon.SKY\n  def __init__(self, config, idx):\n    '''\n    Args:\n        config : A forge.blade.core.Config object or subclass object\n    '''\n    super().__init__(config, idx)\n    self.health_max = config.PLAYER_BASE_HEALTH\n\n    if config.RESOURCE_SYSTEM_ENABLED:\n      self.food_max   = config.RESOURCE_BASE\n      self.water_max  = config.RESOURCE_BASE\n\n    self.spawnR    = None\n    self.spawnC    = None\n\n  @property\n  def policy(self):\n    return self.__class__.__name__\n\n  @property\n  def forage_criterion(self) -> bool:\n    '''Return true if low on food or water'''\n    min_level = 7 * self.config.RESOURCE_DEPLETION_RATE\n    return self.me.food <= min_level or self.me.water <= min_level\n\n  def forage(self):\n    '''Min/max food and water using Dijkstra's algorithm'''\n    # TODO: do not access realm._np_random directly. ALSO see below for all other uses\n    move.forageDijkstra(self.config, self.ob, self.actions,\n                        self.food_max, self.water_max, self._np_random)\n\n  def gather(self, resource):\n    '''BFS search for a particular resource'''\n    return move.gatherBFS(self.config, self.ob, self.actions, resource, self._np_random)\n\n  def explore(self):\n    '''Route away from spawn'''\n    move.explore(self.config, self.ob, self.actions, self.me.row, self.me.col, self._np_random)\n\n  @property\n  def downtime(self):\n    '''Return true if agent is not occupied with a high-priority action'''\n    return not self.forage_criterion and self.attacker is None\n\n  def evade(self):\n    '''Target and path away from an attacker'''\n    move.evade(self.config, self.ob, self.actions, self.attacker, self._np_random)\n    self.target     = self.attacker\n    self.targetID   = self.attackerID\n    self.targetDist = self.attackerDist\n\n  def attack(self):\n    '''Attack the current target'''\n    if self.target is not None:\n      assert self.targetID is not None\n      style = self._np_random.choice(self.style)\n      attack.target(self.config, self.actions, style, self.targetID)\n\n  def target_weak(self): # pylint: disable=inconsistent-return-statements\n    '''Target the nearest agent if it is weak'''\n    if self.closest is None:\n      return False\n\n    selfLevel  = self.me.level\n    targLevel  = max(self.closest.melee_level, self.closest.range_level, self.closest.mage_level)\n\n    if self.closest.npc_type == 1 or \\\n       targLevel <= selfLevel <= 5 or \\\n       selfLevel >= targLevel + 3:\n      self.target     = self.closest\n      self.targetID   = self.closestID\n      self.targetDist = self.closestDist\n\n  def scan_agents(self):\n    '''Scan the nearby area for agents'''\n    self.closest, self.closestDist   = attack.closestTarget(self.config, self.ob)\n    self.attacker, self.attackerDist = attack.attacker(self.config, self.ob)\n\n    self.closestID = None\n    if self.closest is not None:\n      self.closestID = self.ob.entities.index(self.closest.id)\n\n    self.attackerID = None\n    if self.attacker is not None:\n      self.attackerID = self.ob.entities.index(self.attacker.id)\n\n    self.target     = None\n    self.targetID   = None\n    self.targetDist = None\n\n  def adaptive_control_and_targeting(self, explore=True):\n    '''Balanced foraging, evasion, and exploration'''\n    self.scan_agents()\n\n    if self.attacker is not None:\n      self.evade()\n      return\n\n    if self.fog_criterion:\n      self.explore()\n    elif self.forage_criterion or not explore:\n      self.forage()\n    else:\n      self.explore()\n\n    self.target_weak()\n\n  def process_inventory(self):\n    if not self.config.ITEM_SYSTEM_ENABLED:\n      return\n\n    self.inventory   = {}\n    self.best_items: Dict   = {}\n    self.item_counts = defaultdict(int)\n\n    self.item_levels = {\n      item_system.Hat.ITEM_TYPE_ID: self.level,\n      item_system.Top.ITEM_TYPE_ID: self.level,\n      item_system.Bottom.ITEM_TYPE_ID: self.level,\n      item_system.Spear.ITEM_TYPE_ID: self.me.melee_level,\n      item_system.Bow.ITEM_TYPE_ID: self.me.range_level,\n      item_system.Wand.ITEM_TYPE_ID: self.me.mage_level,\n      item_system.Rod.ITEM_TYPE_ID: self.me.fishing_level,\n      item_system.Gloves.ITEM_TYPE_ID: self.me.herbalism_level,\n      item_system.Pickaxe.ITEM_TYPE_ID: self.me.prospecting_level,\n      item_system.Axe.ITEM_TYPE_ID: self.me.carving_level,\n      item_system.Chisel.ITEM_TYPE_ID: self.me.alchemy_level,\n      item_system.Whetstone.ITEM_TYPE_ID: self.me.melee_level,\n      item_system.Arrow.ITEM_TYPE_ID: self.me.range_level,\n      item_system.Runes.ITEM_TYPE_ID: self.me.mage_level,\n      item_system.Ration.ITEM_TYPE_ID: self.level,\n      item_system.Potion.ITEM_TYPE_ID: self.level\n    }\n\n    for item_ary in self.ob.inventory.values:\n      itm = item_system.ItemState.parse_array(item_ary)\n      assert itm.quantity != 0\n\n      # Too high level to equip or use\n      if itm.type_id in self.item_levels and itm.level > self.item_levels[itm.type_id]:\n        continue\n\n      # cannot use listed item\n      if itm.listed_price:\n        continue\n\n      self.item_counts[itm.type_id] += itm.quantity\n      self.inventory[itm.id] = itm\n\n      # Best by default\n      if itm.type_id not in self.best_items:\n        self.best_items[itm.type_id] = itm\n\n      best_itm = self.best_items[itm.type_id]\n\n      if itm.level > best_itm.level:\n        self.best_items[itm.type_id] = itm\n\n  def upgrade_heuristic(self, current_level, upgrade_level, price):\n    return (upgrade_level - current_level) / max(price, 1)\n\n  def process_market(self):\n    if not self.config.EXCHANGE_SYSTEM_ENABLED:\n      return\n\n    self.market         = {}\n    self.best_heuristic = {}\n\n    for item_ary in self.ob.market.values:\n      itm = item_system.ItemState.parse_array(item_ary)\n\n      self.market[itm.id] = itm\n\n      # Prune Unaffordable\n      if itm.listed_price > self.me.gold:\n        continue\n\n      # Too high level to equip\n      if itm.type_id in self.item_levels and itm.level > self.item_levels[itm.type_id] :\n        continue\n\n      #Current best item level\n      current_level = 0\n      if itm.type_id in self.best_items:\n        current_level = self.best_items[itm.type_id].level\n\n      itm.heuristic = self.upgrade_heuristic(current_level, itm.level, itm.listed_price)\n\n      #Always count first item\n      if itm.type_id not in self.best_heuristic:\n        self.best_heuristic[itm.type_id] = itm\n        continue\n\n      #Better heuristic value\n      if itm.heuristic > self.best_heuristic[itm.type_id].heuristic:\n        self.best_heuristic[itm.type_id] = itm\n\n  def equip(self, items: set):\n    for type_id, itm in self.best_items.items():\n      if type_id not in items:\n        continue\n\n      if itm.equipped or itm.listed_price:\n        continue\n\n      # InventoryItem needs where the item is (index) in the inventory\n      self.actions[action.Use] = {\n        action.InventoryItem: self.ob.inventory.index(itm.id)}\n\n    return True\n\n  def consume(self):\n    if self.me.health <= self.health_max // 2 \\\n        and item_system.Potion.ITEM_TYPE_ID in self.best_items:\n      itm = self.best_items[item_system.Potion.ITEM_TYPE_ID]\n    elif (self.me.food == 0 or self.me.water == 0) \\\n        and item_system.Ration.ITEM_TYPE_ID in self.best_items:\n      itm = self.best_items[item_system.Ration.ITEM_TYPE_ID]\n    else:\n      return\n\n    if itm.listed_price:\n      return\n\n    # InventoryItem needs where the item is (index) in the inventory\n    self.actions[action.Use] = {\n      action.InventoryItem: self.ob.inventory.index(itm.id)}\n\n  def sell(self, keep_k: dict, keep_best: set):\n    for itm in self.inventory.values():\n      price = int(max(itm.level, 1))\n      assert itm.quantity > 0\n\n      if itm.equipped or itm.listed_price:\n        continue\n\n      if itm.type_id in keep_k:\n        owned = self.item_counts[itm.type_id]\n        k     = keep_k[itm.type_id]\n        if owned <= k:\n          continue\n\n      #Exists an equippable of the current class, best needs to be kept, and this is the best item\n      if itm.type_id in self.best_items and \\\n        itm.type_id in keep_best and \\\n        itm.id == self.best_items[itm.type_id].id:\n        continue\n\n      self.actions[action.Sell] = {\n        action.InventoryItem: self.ob.inventory.index(itm.id),\n        action.Price: action.Price.index(price) }\n\n      return itm\n\n  def buy(self, buy_k: dict, buy_upgrade: set):\n    if len(self.inventory) >= self.config.ITEM_INVENTORY_CAPACITY:\n      return\n\n    purchase = None\n    best = list(self.best_heuristic.items())\n    self._np_random.shuffle(best)\n    for type_id, itm in best:\n      # Buy top k\n      if type_id in buy_k:\n        owned = self.item_counts[type_id]\n        k = buy_k[type_id]\n        if owned < k:\n          purchase = itm\n\n      # Check if item desired and upgrade\n      elif type_id in buy_upgrade and itm.heuristic > 0:\n        purchase = itm\n\n      # Buy best heuristic upgrade\n      if purchase:\n        self.actions[action.Buy] = {\n          action.MarketItem: self.ob.market.index(purchase.id)}\n        return\n\n  def exchange(self):\n    if not self.config.EXCHANGE_SYSTEM_ENABLED:\n      return\n\n    self.process_market()\n    self.sell(keep_k=self.supplies, keep_best=self.wishlist)\n    self.buy(buy_k=self.supplies, buy_upgrade=self.wishlist)\n\n  def use(self):\n    self.process_inventory()\n    if self.config.EQUIPMENT_SYSTEM_ENABLED and not self.consume():\n      self.equip(items=self.wishlist)\n\n  def __call__(self, observation: Observation):\n    '''Process observations and return actions'''\n    assert self._np_random is not None, \"Agent's RNG must be set.\"\n    self.actions = {}\n\n    self.ob = observation\n    self.me = observation.agent\n\n    # combat level\n    self.me.level = max(self.me.melee_level, self.me.range_level, self.me.mage_level)\n\n    self.skills = {\n      skill.Melee: self.me.melee_level,\n      skill.Range: self.me.range_level,\n      skill.Mage: self.me.mage_level,\n      skill.Fishing: self.me.fishing_level,\n      skill.Herbalism: self.me.herbalism_level,\n      skill.Prospecting: self.me.prospecting_level,\n      skill.Carving: self.me.carving_level,\n      skill.Alchemy: self.me.alchemy_level\n    }\n\n    # TODO(kywch): need a consistent level variables\n    # level for using armor, rations, and potion\n    self.level = min(1, max(self.skills.values()))\n\n    if self.spawnR is None:\n      self.spawnR = self.me.row\n    if self.spawnC is None:\n      self.spawnC = self.me.col\n\n    # When to run from death fog in BR configs\n    self.fog_criterion = None\n    if self.config.DEATH_FOG_ONSET is not None:\n      time_alive = self.me.time_alive\n      start_running = time_alive > self.config.DEATH_FOG_ONSET - 64\n      run_now = time_alive % max(1, int(1 / self.config.DEATH_FOG_SPEED))\n      self.fog_criterion = start_running and run_now\n\n\nclass Sleeper(Scripted):\n  '''Do Nothing'''\n  def __call__(self, obs):\n    super().__call__(obs)\n    return {}\nclass Random(Scripted):\n  '''Moves randomly'''\n  def __call__(self, obs):\n    super().__call__(obs)\n\n    move.rand(self.config, self.ob, self.actions, self._np_random)\n    return self.actions\n\nclass Meander(Scripted):\n  '''Moves randomly on safe terrain'''\n  def __call__(self, obs):\n    super().__call__(obs)\n\n    move.meander(self.config, self.ob, self.actions, self._np_random)\n    return self.actions\n\nclass Explore(Scripted):\n  '''Actively explores towards the center'''\n  def __call__(self, obs):\n    super().__call__(obs)\n\n    self.explore()\n\n    return self.actions\n\nclass Forage(Scripted):\n  '''Forages using Dijkstra's algorithm and actively explores'''\n  def __call__(self, obs):\n    super().__call__(obs)\n\n    if self.forage_criterion:\n      self.forage()\n    else:\n      self.explore()\n\n    return self.actions\n\nclass Combat(Scripted):\n  '''Forages, fights, and explores'''\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    self.style  = [action.Melee, action.Range, action.Mage]\n\n  @property\n  def supplies(self):\n    return {\n      item_system.Ration.ITEM_TYPE_ID: 2,\n      item_system.Potion.ITEM_TYPE_ID: 2,\n      self.ammo.ITEM_TYPE_ID: 10\n    }\n\n  @property\n  def wishlist(self):\n    return {\n      item_system.Hat.ITEM_TYPE_ID,\n      item_system.Top.ITEM_TYPE_ID,\n      item_system.Bottom.ITEM_TYPE_ID,\n      self.weapon.ITEM_TYPE_ID,\n      self.ammo.ITEM_TYPE_ID\n    }\n\n  def __call__(self, obs):\n    super().__call__(obs)\n    self.use()\n    self.exchange()\n\n    self.adaptive_control_and_targeting()\n    self.attack()\n\n    return self.actions\n\nclass Gather(Scripted):\n  '''Forages, fights, and explores'''\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    self.resource = [material.Fish, material.Herb, material.Ore, material.Tree, material.Crystal]\n\n  @property\n  def supplies(self):\n    return {\n      item_system.Ration.ITEM_TYPE_ID: 1,\n      item_system.Potion.ITEM_TYPE_ID: 1\n    }\n\n  @property\n  def wishlist(self):\n    return {\n      item_system.Hat.ITEM_TYPE_ID,\n      item_system.Top.ITEM_TYPE_ID,\n      item_system.Bottom.ITEM_TYPE_ID,\n      self.tool.ITEM_TYPE_ID\n    }\n\n  def __call__(self, obs):\n    super().__call__(obs)\n    self.use()\n    self.exchange()\n\n    if self.forage_criterion:\n      self.forage()\n    elif self.fog_criterion or not self.gather(self.resource):\n      self.explore()\n\n    return self.actions\n\nclass Fisher(Gather):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.resource = [material.Fish]\n    self.tool     = item_system.Rod\n\nclass Herbalist(Gather):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.resource = [material.Herb]\n    self.tool     = item_system.Gloves\n\nclass Prospector(Gather):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.resource = [material.Ore]\n    self.tool     = item_system.Pickaxe\n\nclass Carver(Gather):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.resource = [material.Tree]\n    self.tool     = item_system.Axe\n\nclass Alchemist(Gather):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.PROFESSION_SYSTEM_ENABLED:\n      self.resource = [material.Crystal]\n    self.tool     = item_system.Chisel\n\nclass Melee(Combat):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.COMBAT_SYSTEM_ENABLED:\n      self.style  = [action.Melee]\n    self.weapon = item_system.Spear\n    self.ammo   = item_system.Whetstone\n\nclass Range(Combat):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.COMBAT_SYSTEM_ENABLED:\n      self.style  = [action.Range]\n    self.weapon = item_system.Bow\n    self.ammo   = item_system.Arrow\n\nclass Mage(Combat):\n  def __init__(self, config, idx):\n    super().__init__(config, idx)\n    if config.COMBAT_SYSTEM_ENABLED:\n      self.style  = [action.Mage]\n    self.weapon = item_system.Wand\n    self.ammo   = item_system.Runes\n"
  },
  {
    "path": "scripted/move.py",
    "content": "# pylint: disable=invalid-name, unused-argument\nimport heapq\nimport numpy as np\n\nfrom nmmo.core import action\nfrom nmmo.core.observation import Observation\nfrom nmmo.lib import material, astar\n\n\ndef inSight(dr, dc, vision):\n  return (-vision <= dr <= vision and\n          -vision <= dc <= vision)\n\ndef rand(config, ob, actions, np_random):\n  direction = np_random.choice(action.Direction.edges)\n  actions[action.Move] = {action.Direction: direction}\n\ndef towards(direction, np_random):\n  if direction == (-1, 0):\n    return action.North\n  if direction == (1, 0):\n    return action.South\n  if direction == (0, -1):\n    return action.West\n  if direction == (0, 1):\n    return action.East\n\n  return np_random.choice(action.Direction.edges)\n\ndef pathfind(config, ob, actions, rr, cc, np_random):\n  direction = aStar(config, ob, actions, rr, cc)\n  direction = towards(direction, np_random)\n  actions[action.Move] = {action.Direction: direction}\n\ndef meander(config, ob, actions, np_random):\n  cands = []\n  if ob.tile(-1, 0).material_id in material.Habitable.indices:\n    cands.append((-1, 0))\n  if ob.tile(1, 0).material_id in material.Habitable.indices:\n    cands.append((1, 0))\n  if ob.tile(0, -1).material_id in material.Habitable.indices:\n    cands.append((0, -1))\n  if ob.tile(0, 1).material_id in material.Habitable.indices:\n    cands.append((0, 1))\n\n  if len(cands) > 0:\n    direction = np_random.choices(cands)[0]\n    direction = towards(direction, np_random)\n    actions[action.Move] = {action.Direction: direction}\n\ndef explore(config, ob, actions, r, c, np_random):\n  vision = config.PLAYER_VISION_RADIUS\n  sz     = config.MAP_SIZE\n  centR, centC = sz//2, sz//2\n  vR, vC = centR-r, centC-c\n  mmag = max(1, abs(vR), abs(vC))\n  rr   = int(np.round(vision*vR/mmag))\n  cc   = int(np.round(vision*vC/mmag))\n  pathfind(config, ob, actions, rr, cc, np_random)\n\ndef evade(config, ob: Observation, actions, attacker, np_random):\n  agent = ob.agent\n  rr, cc = (2*agent.row - attacker.row, 2*agent.col - attacker.col)\n  pathfind(config, ob, actions, rr, cc, np_random)\n\ndef forageDijkstra(config, ob: Observation, actions,\n                   food_max, water_max, np_random, cutoff=100):\n  vision = config.PLAYER_VISION_RADIUS\n\n  agent  = ob.agent\n  food = agent.food\n  water = agent.water\n\n  best      = -1000\n  start     = (0, 0)\n  goal      = (0, 0)\n\n  reward    = {start: (food, water)}\n  backtrace = {start: None}\n\n  queue = [start]\n\n  while queue:\n    cutoff -= 1\n    if cutoff <= 0:\n      break\n\n    cur = queue.pop(0)\n    for nxt in astar.adjacentPos(cur):\n      if nxt in backtrace:\n        continue\n\n      if not inSight(*nxt, vision):\n        continue\n\n      tile     = ob.tile(*nxt)\n      matl     = tile.material_id\n\n      if not matl in material.Habitable.indices:\n        continue\n\n      food, water = reward[cur]\n      water = max(0, water - 1)\n      food  = max(0, food - 1)\n      if matl == material.Foilage.index:\n        food = min(food+food_max//2, food_max)\n\n      for pos in astar.adjacentPos(nxt):\n        if not inSight(*pos, vision):\n          continue\n\n        tile = ob.tile(*pos)\n        matl = tile.material_id\n        if matl == material.Water.index:\n          water = min(water+water_max//2, water_max)\n          break\n\n      reward[nxt] = (food, water)\n\n      total = min(food, water)\n      if total > best \\\n          or (total == best and max(food, water) > max(reward[goal])):\n        best = total\n        goal = nxt\n\n      queue.append(nxt)\n      backtrace[nxt] = cur\n\n  while goal in backtrace and backtrace[goal] != start:\n    goal = backtrace[goal]\n  direction = towards(goal, np_random)\n  actions[action.Move] = {action.Direction: direction}\n\ndef findResource(config, ob: Observation, resource):\n  vision = config.PLAYER_VISION_RADIUS\n  resource_index = resource.index\n  for r in range(-vision, vision+1):\n    for c in range(-vision, vision+1):\n      tile = ob.tile(r, c)\n      material_id = tile.material_id\n    if material_id == resource_index:\n      return (r, c)\n  return False\n\ndef gatherAStar(config, ob, actions, resource, np_random, cutoff=100):\n  resource_pos = findResource(config, ob, resource)\n  if not resource_pos:\n    return False\n\n  rr, cc = resource_pos\n  next_pos = aStar(config, ob, actions, rr, cc, cutoff=cutoff)\n  if not next_pos or next_pos == (0, 0):\n    return False\n\n  direction = towards(next_pos, np_random)\n  actions[action.Move] = {action.Direction: direction}\n  return True\n\ndef gatherBFS(config, ob: Observation, actions, resource, np_random, cutoff=100):\n  vision = config.PLAYER_VISION_RADIUS\n\n  start  = (0, 0)\n  backtrace = {start: None}\n  queue = [start]\n  found = False\n\n  while queue:\n    cutoff -= 1\n    if cutoff <= 0:\n      return False\n\n    cur = queue.pop(0)\n    for nxt in astar.adjacentPos(cur):\n      if found:\n        break\n\n      if nxt in backtrace:\n        continue\n\n      if not inSight(*nxt, vision):\n        continue\n\n      tile     = ob.tile(*nxt)\n      matl     = tile.material_id\n\n      if material.Fish in resource and material.Fish.index == matl:\n        found = nxt\n        backtrace[nxt] = cur\n        break\n\n      if not tile.material_id in material.Habitable.indices:\n        continue\n\n      if matl in (e.index for e in resource):\n        found = nxt\n        backtrace[nxt] = cur\n        break\n\n      for pos in astar.adjacentPos(nxt):\n        if not inSight(*pos, vision):\n          continue\n\n        tile = ob.tile(*pos)\n        matl = tile.material_id\n\n        if matl == material.Fish.index:\n          backtrace[nxt] = cur\n          break\n\n        queue.append(nxt)\n        backtrace[nxt] = cur\n\n  #Ran out of tiles\n  if not found:\n    return False\n\n  while found in backtrace and backtrace[found] != start:\n    found = backtrace[found]\n\n  direction = towards(found, np_random)\n  actions[action.Move] = {action.Direction: direction}\n\n  return True\n\n\ndef aStar(config, ob: Observation, actions, rr, cc, cutoff=100):\n  vision = config.PLAYER_VISION_RADIUS\n\n  start = (0, 0)\n  goal  = (rr, cc)\n  if start == goal:\n    return (0, 0)\n\n  pq = [(0, start)]\n\n  backtrace = {}\n  cost = {start: 0}\n\n  closestPos = start\n  closestHeuristic = astar.l1(start, goal)\n  closestCost = closestHeuristic\n\n  while pq:\n    # Use approximate solution if budget exhausted\n    cutoff -= 1\n    if cutoff <= 0:\n      if goal not in backtrace:\n        goal = closestPos\n      break\n\n    priority, cur = heapq.heappop(pq)\n\n    if cur == goal:\n      break\n\n    for nxt in astar.adjacentPos(cur):\n      if not inSight(*nxt, vision):\n        continue\n\n      tile     = ob.tile(*nxt)\n      matl     = tile.material_id\n\n      if not matl in material.Habitable.indices:\n        continue\n\n      #Omitted water from the original implementation. Seems key\n      if matl in material.Impassible.indices:\n        continue\n\n      newCost = cost[cur] + 1\n      if nxt not in cost or newCost < cost[nxt]:\n        cost[nxt] = newCost\n        heuristic = astar.l1(goal, nxt)\n        priority = newCost + heuristic\n\n        # Compute approximate solution\n        if heuristic < closestHeuristic \\\n            or (heuristic == closestHeuristic and priority < closestCost):\n          closestPos = nxt\n          closestHeuristic = heuristic\n          closestCost = priority\n\n        heapq.heappush(pq, (priority, nxt))\n        backtrace[nxt] = cur\n\n  goal = closestPos\n  while goal in backtrace and backtrace[goal] != start:\n    goal = backtrace[goal]\n\n  return goal\n"
  },
  {
    "path": "setup.py",
    "content": "from itertools import chain\n\nfrom setuptools import find_packages, setup\nfrom Cython.Build import cythonize\nimport numpy as np\n\nREPO_URL = \"https://github.com/neuralmmo/environment\"\n\nextra = {\n    'docs': [\n        'sphinx==5.0.0',\n        'sphinx-rtd-theme==0.5.1',\n        'sphinxcontrib-youtube==1.0.1',\n        'myst-parser==1.0.0',\n        'sphinx-rtd-theme==0.5.1',\n        'sphinx-design==0.4.1',\n        'furo==2023.3.27',\n    ],\n}\n\nextra['all'] = list(set(chain.from_iterable(extra.values())))\n\nwith open('nmmo/version.py', encoding=\"utf-8\") as vf:\n  ver = vf.read().split()[-1].strip(\"'\")\n\nsetup(\n  name=\"nmmo\",\n  description=\"Neural MMO is a platform for multiagent intelligence research \" + \\\n              \"inspired by Massively Multiplayer Online (MMO) role-playing games. \" + \\\n              \"Documentation hosted at neuralmmo.github.io.\",\n  long_description_content_type=\"text/markdown\",\n  version=ver,\n  packages=find_packages(),\n  include_package_data=True,\n  install_requires=[\n    'cython>=3.0.0',\n    'numpy==1.23.3',\n    'scipy==1.10.0',\n    'pytest==7.3.0',\n    'pytest-benchmark==3.4.1',\n    'imageio>=2.27',\n    'ordered-set==4.1.0',\n    'pettingzoo==1.24.1',\n    'gymnasium==0.29.1',\n    'pylint==2.16.0',\n    'psutil<6',\n    'tqdm<5',\n    'py==1.11.0',\n    'dill<0.4',\n  ],\n  ext_modules = cythonize([\"nmmo/lib/cython_helper.pyx\"]),\n  include_dirs=[np.get_include()],\n  extras_require=extra,\n  python_requires=\">=3.7,<3.11\",\n  license=\"MIT\",\n  author=\"Joseph Suarez\",\n  author_email=\"jsuarez@mit.edu\",\n  url=REPO_URL,\n  keywords=[\"Neural MMO\", \"MMO\"],\n  classifiers=[\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Science/Research\",\n    \"Intended Audience :: Developers\",\n    \"Environment :: Console\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3.7\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n  ],\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/action/test_ammo_use.py",
    "content": "import unittest\nimport logging\nimport numpy as np\n\nfrom tests.testhelpers import ScriptedTestTemplate, provide_item\n\nfrom nmmo.core import action\nfrom nmmo.systems import item as Item\nfrom nmmo.systems.item import ItemState\n\nRANDOM_SEED = 284\n\nLOGFILE = None  # 'tests/action/test_ammo_use.log'\n\nclass TestAmmoUse(ScriptedTestTemplate):\n  # pylint: disable=protected-access,multiple-statements,no-member\n\n  @classmethod\n  def setUpClass(cls):\n    super().setUpClass()\n\n    # config specific to the tests here\n    if LOGFILE:  # for debugging\n      logging.basicConfig(filename=LOGFILE, level=logging.INFO)\n\n  def _assert_action_targets_zero(self, gym_obs):\n    mask = np.sum(gym_obs[\"ActionTargets\"][\"GiveGold\"][\"Price\"]) \\\n          + np.sum(gym_obs[\"ActionTargets\"][\"Buy\"][\"MarketItem\"])\n    for atn in [action.Use, action.Give, action.Destroy, action.Sell]:\n      mask += np.sum(gym_obs[\"ActionTargets\"][atn.__name__][\"InventoryItem\"])\n    # If MarketItem and InventoryTarget have no-action flags, these sum up to 104\n    # To prevent entropy collapse, GiveGold/Price and Buy/MarketItem masks are tweaked\n    # The Price mask is all ones, so the sum is 104\n    self.assertEqual(mask, 99 + 5*int(self.config.PROVIDE_NOOP_ACTION_TARGET))\n\n  def test_spawn_immunity(self):\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # Check spawn immunity in the action targets\n    for ent_obs in env.obs.values():\n      gym_obs = ent_obs.to_gym()\n      target_mask = gym_obs[\"ActionTargets\"][\"Attack\"][\"Target\"][:len(ent_obs.entities.ids)]\n      # cannot target other agents\n      self.assertTrue(np.sum(target_mask[ent_obs.entities.ids > 0]) == 0)\n\n    # Test attack during spawn immunity, which should be ignored\n    env.step({ ent_id: { action.Attack:\n        { action.Style: env.realm.players[ent_id].agent.style[0],\n          action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } }\n        for ent_id in self.ammo })\n\n    for ent_id in [1, 2, 3]:\n      # in_combat status is set when attack is executed\n      self.assertFalse(env.realm.players[ent_id].in_combat)\n\n  def test_ammo_fire_all(self):\n    env = self._setup_env(random_seed=RANDOM_SEED, remove_immunity=True)\n\n    # First tick actions: USE (equip) level-0 ammo\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) }\n      } for ent_id, ent_ammo in self.ammo.items() })\n\n    # check if the agents have equipped the ammo\n    for ent_id, ent_ammo in self.ammo.items():\n      gym_obs = env.obs[ent_id].to_gym()\n      inventory = env.obs[ent_id].inventory\n      inv_idx = inventory.sig(ent_ammo, 0)\n      self.assertEqual(1, # True\n        ItemState.parse_array(inventory.values[inv_idx]).equipped)\n\n      # check SELL InventoryItem mask -- one cannot sell equipped item\n      mask = gym_obs[\"ActionTargets\"][\"Sell\"][\"InventoryItem\"][:inventory.len] > 0\n      self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask])\n\n      # the agents must not be in combat status\n      self.assertFalse(env.realm.players[ent_id].in_combat)\n\n    # Second tick actions: ATTACK other agents using ammo\n    #  NOTE that agents 1 & 3's attack are invalid due to out-of-range\n    env.step({ ent_id: { action.Attack:\n        { action.Style: env.realm.players[ent_id].agent.style[0],\n          action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } }\n        for ent_id in self.ammo })\n\n    # check combat status: agents 2 (attacker) and 1 (target) are in combat\n    self.assertTrue(env.realm.players[2].in_combat)\n    self.assertTrue(env.realm.players[1].in_combat)\n    self.assertFalse(env.realm.players[3].in_combat)\n\n    # check the action masks are all 0 during combat\n    for ent_id in [1, 2]:\n      self._assert_action_targets_zero(env.obs[ent_id].to_gym())\n\n    # check if the ammos were consumed\n    ammo_ids = []\n    for ent_id, ent_ammo in self.ammo.items():\n      inventory = env.obs[ent_id].inventory\n      inv_idx = inventory.sig(ent_ammo, 0)\n      item_info = ItemState.parse_array(inventory.values[inv_idx])\n      if ent_id == 2:\n        # only agent 2's attack is valid and consume ammo\n        self.assertEqual(self.ammo_quantity - 1, item_info.quantity)\n        ammo_ids.append(inventory.id(inv_idx))\n      else:\n        self.assertEqual(self.ammo_quantity, item_info.quantity)\n\n    # Third tick actions: ATTACK again to use up all the ammo, except agent 3\n    #  NOTE that agent 3's attack command is invalid due to out-of-range\n    env.step({ ent_id: { action.Attack:\n        { action.Style: env.realm.players[ent_id].agent.style[0],\n          action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } }\n        for ent_id in self.ammo })\n\n    # agents 1 and 2's latest_combat_tick should be updated\n    self.assertEqual(env.realm.tick, env.realm.players[1].latest_combat_tick.val)\n    self.assertEqual(env.realm.tick, env.realm.players[2].latest_combat_tick.val)\n    self.assertEqual(0, env.realm.players[3].latest_combat_tick.val)\n\n    # check if the ammos are depleted and the ammo slot is empty\n    ent_id = 2\n    self.assertTrue(env.obs[ent_id].inventory.len == len(self.item_sig[ent_id]) - 1)\n    self.assertTrue(env.realm.players[ent_id].inventory.equipment.ammunition.item is None)\n\n    for item_id in ammo_ids:\n      self.assertTrue(len(ItemState.Query.by_id(env.realm.datastore, item_id)) == 0)\n      self.assertTrue(item_id not in env.realm.items)\n\n    # invalid attacks\n    for ent_id in [1, 3]:\n      # agent 3 gathered arrow, so the item count increased\n      #self.assertTrue(env.obs[ent_id].inventory.len == len(self.item_sig[ent_id]))\n      self.assertTrue(env.realm.players[ent_id].inventory.equipment.ammunition.item is not None)\n\n    # after 3 ticks, combat status should be cleared\n    for _ in range(3):\n      env.step({ 0:0 }) # put dummy actions to prevent generating scripted actions\n\n    for ent_id in [1, 2, 3]:\n      self.assertFalse(env.realm.players[ent_id].in_combat)\n\n    # DONE\n\n  def test_use_ammo_only_when_attack_style_match(self):\n    env = self._setup_env(random_seed=RANDOM_SEED, remove_immunity=True)\n\n    # First tick actions: USE (equip) level-0 ammo\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) }\n      } for ent_id, ent_ammo in self.ammo.items() })\n\n    # Second tick actions: Melee attack should not consume Arrow\n    ent_id = 2\n    env.step({ 2: { action.Attack:\n        { action.Style: action.Melee,\n          action.Target: env.obs[ent_id].entities.index((ent_id+1)%3+1) } }})\n\n    ent_ammo = self.ammo[ent_id]\n    inventory = env.obs[ent_id].inventory\n    inv_idx = inventory.sig(ent_ammo, 0)\n    item_info = ItemState.parse_array(inventory.values[inv_idx])\n\n    # Did not consume ammo\n    self.assertEqual(self.ammo_quantity, item_info.quantity)\n\n    # DONE\n\n  def test_cannot_use_listed_items(self):\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    sell_price = 1\n\n    # provide extra whetstone to range to make its inventory full\n    # but level-0 whetstone overlaps with the listed item\n    ent_id = 2\n    provide_item(env.realm, ent_id, Item.Whetstone, level=0, quantity=3)\n    provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=3)\n\n    # provide extra whetstone to mage to make its inventory full\n    # there will be no overlapping item\n    ent_id = 3\n    provide_item(env.realm, ent_id, Item.Whetstone, level=5, quantity=3)\n    provide_item(env.realm, ent_id, Item.Whetstone, level=7, quantity=3)\n\n    # First tick actions: SELL level-0 ammo\n    env.step({ ent_id: { action.Sell:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0),\n          action.Price: action.Price.index(sell_price) } }\n        for ent_id, ent_ammo in self.ammo.items() })\n\n    # check if the ammos were listed\n    for ent_id, ent_ammo in self.ammo.items():\n      gym_obs = env.obs[ent_id].to_gym()\n      inventory = env.obs[ent_id].inventory\n      inv_idx = inventory.sig(ent_ammo, 0)\n      item_info = ItemState.parse_array(inventory.values[inv_idx])\n      # ItemState data\n      self.assertEqual(sell_price, item_info.listed_price)\n      # Exchange listing\n      self.assertTrue(item_info.id in env.realm.exchange._item_listings)\n      self.assertTrue(item_info.id in env.obs[ent_id].market.ids)\n\n      # check SELL InventoryItem mask -- one cannot sell listed item\n      mask = gym_obs[\"ActionTargets\"][\"Sell\"][\"InventoryItem\"][:inventory.len] > 0\n      self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask])\n\n      # check USE InventoryItem mask -- one cannot use listed item\n      mask = gym_obs[\"ActionTargets\"][\"Use\"][\"InventoryItem\"][:inventory.len] > 0\n      self.assertTrue(inventory.id(inv_idx) not in inventory.ids[mask])\n\n      # check BUY MarketItem mask -- there should be two ammo items in the market\n      mask = gym_obs[\"ActionTargets\"][\"Buy\"][\"MarketItem\"][:inventory.len] > 0\n      # agent 1 has inventory space\n      if ent_id == 1: self.assertTrue(sum(mask) == 2)\n      # agent 2's inventory is full but can buy level-0 whetstone (existing ammo)\n      if ent_id == 2: self.assertTrue(sum(mask) == 1)\n      # agent 3's inventory is full without overlapping ammo\n      if ent_id == 3: self.assertTrue(sum(mask) == 0)\n\n    # Second tick actions: USE ammo, which should NOT happen\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(ent_ammo, 0) }\n      } for ent_id, ent_ammo in self.ammo.items() })\n\n    # check if the agents have equipped the ammo\n    for ent_id, ent_ammo in self.ammo.items():\n      inventory = env.obs[ent_id].inventory\n      inv_idx = inventory.sig(ent_ammo, 0)\n      self.assertEqual(0, # False\n        ItemState.parse_array(inventory.values[inv_idx]).equipped)\n\n    # DONE\n\n  def test_receive_extra_ammo_swap(self):\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    extra_ammo = 500\n    wstone_lvl0 = (Item.Whetstone, 0)\n    wstone_lvl1 = (Item.Whetstone, 1)\n    wstone_lvl3 = (Item.Whetstone, 3)\n\n    def sig_int_tuple(sig):\n      return (sig[0].ITEM_TYPE_ID, sig[1])\n\n    for ent_id in self.policy:\n      # provide extra whetstone\n      provide_item(env.realm, ent_id, Item.Whetstone, level=0, quantity=extra_ammo)\n      provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=extra_ammo)\n\n    # level up the agent 1 (Melee) to 2\n    env.realm.players[1].skills.melee.level.update(2)\n\n    # check inventory\n    env._compute_observations()\n    for ent_id in self.ammo:\n      # realm data\n      inv_realm = { item.signature: item.quantity.val\n                    for item in env.realm.players[ent_id].inventory.items\n                    if isinstance(item, Item.Stack) }\n      self.assertTrue( sig_int_tuple(wstone_lvl0) in inv_realm )\n      self.assertTrue( sig_int_tuple(wstone_lvl1) in inv_realm )\n      self.assertEqual( inv_realm[sig_int_tuple(wstone_lvl1)], extra_ammo )\n\n      # item datastore\n      inv_obs = env.obs[ent_id].inventory\n      self.assertTrue(inv_obs.sig(*wstone_lvl0) is not None)\n      self.assertTrue(inv_obs.sig(*wstone_lvl1) is not None)\n      self.assertEqual( extra_ammo,\n        ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).quantity)\n      if ent_id == 1:\n        # if the ammo has the same signature, the quantity is added to the existing stack\n        self.assertEqual(inv_realm[sig_int_tuple(wstone_lvl0)],\n                         extra_ammo + self.ammo_quantity )\n        self.assertEqual(extra_ammo + self.ammo_quantity,\n          ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).quantity)\n        # so there should be 1 more space\n        self.assertEqual(inv_obs.len, self.config.ITEM_INVENTORY_CAPACITY - 1)\n\n      else:\n        # if the signature is different, it occupies a new inventory space\n        self.assertEqual(inv_realm[sig_int_tuple(wstone_lvl0)], extra_ammo )\n        self.assertEqual(extra_ammo,\n          ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).quantity)\n        # thus the inventory is full\n        self.assertEqual(inv_obs.len, self.config.ITEM_INVENTORY_CAPACITY)\n\n      if ent_id == 1:\n        gym_obs = env.obs[ent_id].to_gym()\n        # check USE InventoryItem mask\n        mask = gym_obs[\"ActionTargets\"][\"Use\"][\"InventoryItem\"][:inv_obs.len] > 0\n        # level-2 melee should be able to use level-0, level-1 whetstone but not level-3\n        self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl0)) in inv_obs.ids[mask])\n        self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl1)) in inv_obs.ids[mask])\n        self.assertTrue(inv_obs.id(inv_obs.sig(*wstone_lvl3)) not in inv_obs.ids[mask])\n\n    # First tick actions: USE (equip) level-0 ammo\n    #   execute only the agent 1's action\n    ent_id = 1\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl0) } }})\n\n    # check if the agents have equipped the ammo 0\n    inv_obs = env.obs[ent_id].inventory\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 1)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 0)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0)\n\n    # Second tick actions: USE (equip) level-1 ammo\n    #   this should unequip level-0 then equip level-1 ammo\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl1) } }})\n\n    # check if the agents have equipped the ammo 1\n    inv_obs = env.obs[ent_id].inventory\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 0)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 1)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0)\n\n    # Third tick actions: USE (equip) level-3 ammo\n    #   this should ignore USE action and leave level-1 ammo equipped\n    env.step({ ent_id: { action.Use:\n        { action.InventoryItem: env.obs[ent_id].inventory.sig(*wstone_lvl3) } }})\n\n    # check if the agents have equipped the ammo 1\n    inv_obs = env.obs[ent_id].inventory\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl0)]).equipped == 0)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl1)]).equipped == 1)\n    self.assertTrue(ItemState.parse_array(inv_obs.values[inv_obs.sig(*wstone_lvl3)]).equipped == 0)\n\n    # DONE\n\n  def test_use_ration_potion(self):\n    # cannot use level-3 ration & potion due to low level\n    # can use level-0 ration & potion to increase food/water/health\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # make food/water/health 20\n    res_dec_tick = env.config.RESOURCE_DEPLETION_RATE\n    init_res = 20\n    for ent_id in self.policy:\n      env.realm.players[ent_id].resources.food.update(init_res)\n      env.realm.players[ent_id].resources.water.update(init_res)\n      env.realm.players[ent_id].resources.health.update(init_res)\n\n    \"\"\"First tick: try to use level-3 ration & potion\"\"\"\n    ration_lvl3 = (Item.Ration, 3)\n    potion_lvl3 = (Item.Potion, 3)\n\n    actions = {}\n    ent_id = 1; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl3) } }\n    ent_id = 2; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl3) } }\n    ent_id = 3; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*potion_lvl3) } }\n\n    env.step(actions)\n\n    # check if the agents have used the ration & potion\n    for ent_id in [1, 2]:\n      # cannot use due to low level, so still in the inventory\n      self.assertFalse( env.obs[ent_id].inventory.sig(*ration_lvl3) is None)\n\n      # failed to restore food/water, so no change\n      resources = env.realm.players[ent_id].resources\n      self.assertEqual( resources.food.val, init_res - res_dec_tick)\n      self.assertEqual( resources.water.val, init_res - res_dec_tick)\n\n    ent_id = 3 # failed to use the item\n    self.assertFalse( env.obs[ent_id].inventory.sig(*potion_lvl3) is None)\n    self.assertEqual( env.realm.players[ent_id].resources.health.val, init_res)\n\n    \"\"\"Second tick: try to use level-0 ration & potion\"\"\"\n    ration_lvl0 = (Item.Ration, 0)\n    potion_lvl0 = (Item.Potion, 0)\n\n    actions = {}\n    ent_id = 1; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl0) } }\n    ent_id = 2; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*ration_lvl0) } }\n    ent_id = 3; actions[ent_id] = { action.Use:\n      { action.InventoryItem: env.obs[ent_id].inventory.sig(*potion_lvl0) } }\n\n    env.step(actions)\n\n    # check if the agents have successfully used the ration & potion\n    restore = env.config.PROFESSION_CONSUMABLE_RESTORE(0)\n    for ent_id in [1, 2]:\n      # items should be gone\n      self.assertTrue( env.obs[ent_id].inventory.sig(*ration_lvl0) is None)\n\n      # successfully restored food/water\n      resources = env.realm.players[ent_id].resources\n      self.assertEqual( resources.food.val, init_res + restore - 2*res_dec_tick)\n      self.assertEqual( resources.water.val, init_res + restore - 2*res_dec_tick)\n\n    ent_id = 3 # successfully restored health\n    self.assertTrue( env.obs[ent_id].inventory.sig(*potion_lvl0) is None) # item gone\n    self.assertEqual( env.realm.players[ent_id].resources.health.val, init_res + restore)\n\n    # DONE\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/action/test_destroy_give_gold.py",
    "content": "import unittest\nimport logging\n\nfrom tests.testhelpers import ScriptedTestTemplate, change_spawn_pos, provide_item\n\nfrom nmmo.core import action\nfrom nmmo.systems import item as Item\nfrom nmmo.systems.item import ItemState\nfrom scripted import baselines\n\nRANDOM_SEED = 985\n\nLOGFILE = None  # 'tests/action/test_destroy_give_gold.log'\n\nclass TestDestroyGiveGold(ScriptedTestTemplate):\n  # pylint: disable=protected-access,multiple-statements,no-member\n\n  @classmethod\n  def setUpClass(cls):\n    super().setUpClass()\n\n    # config specific to the tests here\n    cls.config.set(\"PLAYERS\", [baselines.Melee, baselines.Range])\n    cls.config.set(\"PLAYER_N\", 6)\n\n    cls.policy = { 1:'Melee', 2:'Range', 3:'Melee', 4:'Range', 5:'Melee', 6:'Range' }\n    cls.spawn_locs = { 1:(17,17), 2:(21,21), 3:(17,17), 4:(21,21), 5:(21,21), 6:(17,17) }\n    cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Whetstone,\n                 4:Item.Arrow, 5:Item.Whetstone, 6:Item.Arrow }\n\n    if LOGFILE:  # for debugging\n      logging.basicConfig(filename=LOGFILE, level=logging.INFO)\n\n  def test_destroy(self):\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # check if level-0 and level-3 ammo are in the correct place\n    for ent_id in self.policy:\n      for idx, lvl in enumerate(self.item_level):\n        assert self.item_sig[ent_id][idx] == (self.ammo[ent_id], lvl)\n\n    # equipped items cannot be destroyed, i.e. that action will be ignored\n    # this should be marked in the mask too\n\n    \"\"\" First tick \"\"\" # First tick actions: USE (equip) level-0 ammo\n    env.step({ ent_id: { action.Use: { action.InventoryItem:\n        env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0]) } # level-0 ammo\n      } for ent_id in self.policy })\n\n    # check if the agents have equipped the ammo\n    for ent_id in self.policy:\n      ent_obs = env.obs[ent_id]\n      inv_idx = ent_obs.inventory.sig(*self.item_sig[ent_id][0]) # level-0 ammo\n      self.assertEqual(1, # True\n        ItemState.parse_array(ent_obs.inventory.values[inv_idx]).equipped)\n\n      # check Destroy InventoryItem mask -- one cannot destroy equipped item\n      for item_sig in self.item_sig[ent_id]:\n        if item_sig == (self.ammo[ent_id], 0): # level-0 ammo\n          self.assertFalse(self._check_inv_mask(ent_obs, action.Destroy, item_sig))\n        else:\n          # other items can be destroyed\n          self.assertTrue(self._check_inv_mask(ent_obs, action.Destroy, item_sig))\n\n    \"\"\" Second tick \"\"\" # Second tick actions: DESTROY ammo\n    actions = {}\n\n    for ent_id in self.policy:\n      if ent_id in [1, 2]:\n        # agent 1 & 2, destroy the level-3 ammos, which are valid\n        actions[ent_id] = { action.Destroy:\n          { action.InventoryItem: env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][1]) } }\n      else:\n        # other agents: destroy the equipped level-0 ammos, which are invalid\n        actions[ent_id] = { action.Destroy:\n          { action.InventoryItem: env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0]) } }\n    env.step(actions)\n\n    # check if the ammos were destroyed\n    for ent_id in self.policy:\n      if ent_id in [1, 2]:\n        inv_idx = env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][1])\n        self.assertTrue(inv_idx is None) # valid actions, thus destroyed\n      else:\n        inv_idx = env.obs[ent_id].inventory.sig(*self.item_sig[ent_id][0])\n        self.assertTrue(inv_idx is not None) # invalid actions, thus not destroyed\n\n    # DONE\n\n  def test_give_tile_npc(self):\n    # cannot give to self (should be masked)\n    # cannot give if not on the same tile (should be masked)\n    # cannot give to the other team member (should be masked)\n    # cannot give to npc (should be masked)\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # teleport the npc -1 to agent 5's location\n    change_spawn_pos(env.realm, -1, self.spawn_locs[5])\n    env._compute_observations()\n\n    \"\"\" First tick actions \"\"\"\n    actions = {}\n    test_cond = {}\n\n    # agent 1: give ammo to agent 3 (valid: the same team, same tile)\n    test_cond[1] = { 'tgt_id': 3, 'item_sig': self.item_sig[1][0],\n                     'ent_mask': True, 'inv_mask': True, 'valid': True }\n    # agent 2: give ammo to agent 2 (invalid: cannot give to self)\n    test_cond[2] = { 'tgt_id': 2, 'item_sig': self.item_sig[2][0],\n                     'ent_mask': False, 'inv_mask': True, 'valid': False }\n    # agent 5: give ammo to npc -1 (invalid, should be masked)\n    test_cond[5] = { 'tgt_id': -1, 'item_sig': self.item_sig[5][0],\n                     'ent_mask': False, 'inv_mask': True, 'valid': False }\n\n    actions = self._check_assert_make_action(env, action.Give, test_cond)\n    env.step(actions)\n\n    # check the results\n    for ent_id, cond in test_cond.items():\n      self.assertEqual( cond['valid'],\n        env.obs[ent_id].inventory.sig(*cond['item_sig']) is None)\n\n      if ent_id == 1: # agent 1 gave ammo stack to agent 3\n        tgt_inv = env.obs[cond['tgt_id']].inventory\n        inv_idx = tgt_inv.sig(*cond['item_sig'])\n        self.assertEqual(2 * self.ammo_quantity,\n          ItemState.parse_array(tgt_inv.values[inv_idx]).quantity)\n\n    # DONE\n\n  def test_give_equipped_listed(self):\n    # cannot give equipped items (should be masked)\n    # cannot give listed items (should be masked)\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    \"\"\" First tick actions \"\"\"\n    actions = {}\n\n    # agent 1: equip the ammo\n    ent_id = 1; item_sig = self.item_sig[ent_id][0]\n    self.assertTrue(\n      self._check_inv_mask(env.obs[ent_id], action.Use, item_sig))\n    actions[ent_id] = { action.Use: { action.InventoryItem:\n        env.obs[ent_id].inventory.sig(*item_sig) } }\n\n    # agent 2: list the ammo for sale\n    ent_id = 2; price = 5; item_sig = self.item_sig[ent_id][0]\n    self.assertTrue(\n      self._check_inv_mask(env.obs[ent_id], action.Sell, item_sig))\n    actions[ent_id] = { action.Sell: {\n        action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig),\n        action.Price: action.Price.index(price) } }\n\n    env.step(actions)\n\n    # Check the first tick actions\n    # agent 1: equip the ammo\n    ent_id = 1; item_sig = self.item_sig[ent_id][0]\n    inv_idx = env.obs[ent_id].inventory.sig(*item_sig)\n    self.assertEqual(1,\n      ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).equipped)\n\n    # agent 2: list the ammo for sale\n    ent_id = 2; price = 5; item_sig = self.item_sig[ent_id][0]\n    inv_idx = env.obs[ent_id].inventory.sig(*item_sig)\n    self.assertEqual(price,\n      ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).listed_price)\n    self.assertTrue(env.obs[ent_id].inventory.id(inv_idx) in env.obs[ent_id].market.ids)\n\n    \"\"\" Second tick actions \"\"\"\n    actions = {}\n    test_cond = {}\n\n    # agent 1: give equipped ammo to agent 3 (invalid: should be masked)\n    test_cond[1] = { 'tgt_id': 3, 'item_sig': self.item_sig[1][0],\n                     'ent_mask': True, 'inv_mask': False, 'valid': False }\n    # agent 2: give listed ammo to agent 4 (invalid: should be masked)\n    test_cond[2] = { 'tgt_id': 4, 'item_sig': self.item_sig[2][0],\n                     'ent_mask': True, 'inv_mask': False, 'valid': False }\n\n    actions = self._check_assert_make_action(env, action.Give, test_cond)\n    env.step(actions)\n\n    # Check the second tick actions\n    # check the results\n    for ent_id, cond in test_cond.items():\n      self.assertEqual( cond['valid'],\n        env.obs[ent_id].inventory.sig(*cond['item_sig']) is None)\n\n    # DONE\n\n  def test_give_full_inventory(self):\n    # cannot give to an agent with the full inventory,\n    #   but it's possible if the agent has the same ammo stack\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # make the inventory full for agents 1, 2\n    extra_items = { (Item.Bottom, 0), (Item.Bottom, 3) }\n    for ent_id in [1, 2]:\n      for item_sig in extra_items:\n        self.item_sig[ent_id].append(item_sig)\n        provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1)\n    env._compute_observations()\n\n    # check if the inventory is full\n    for ent_id in [1, 2]:\n      self.assertEqual(env.obs[ent_id].inventory.len, env.config.ITEM_INVENTORY_CAPACITY)\n      self.assertTrue(env.realm.players[ent_id].inventory.space == 0)\n\n    \"\"\" First tick actions \"\"\"\n    actions = {}\n    test_cond = {}\n\n    # agent 3: give ammo to agent 1 (the same ammo stack, so valid)\n    test_cond[3] = { 'tgt_id': 1, 'item_sig': self.item_sig[3][0],\n                     'ent_mask': True, 'inv_mask': True, 'valid': True }\n    # agent 4: give gloves to agent 2 (not the stack, so invalid)\n    test_cond[4] = { 'tgt_id': 2, 'item_sig': self.item_sig[4][4],\n                     'ent_mask': True, 'inv_mask': True, 'valid': False }\n\n    actions = self._check_assert_make_action(env, action.Give, test_cond)\n    env.step(actions)\n\n    # Check the first tick actions\n    # check the results\n    for ent_id, cond in test_cond.items():\n      self.assertEqual( cond['valid'],\n        env.obs[ent_id].inventory.sig(*cond['item_sig']) is None)\n\n      if ent_id == 3: # successfully gave the ammo stack to agent 1\n        tgt_inv = env.obs[cond['tgt_id']].inventory\n        inv_idx = tgt_inv.sig(*cond['item_sig'])\n        self.assertEqual(2 * self.ammo_quantity,\n          ItemState.parse_array(tgt_inv.values[inv_idx]).quantity)\n\n    # DONE\n\n  def test_give_gold(self):\n    # cannot give to an npc (should be masked)\n    # cannot give to self (should be masked)\n    # cannot give if not on the same tile (should be masked)\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # teleport the npc -1 to agent 3's location\n    change_spawn_pos(env.realm, -1, self.spawn_locs[3])\n    env._compute_observations()\n\n    test_cond = {}\n\n    # NOTE: the below tests rely on the static execution order from 1 to N\n    # agent 1: give gold to agent 3 (valid: same tile)\n    test_cond[1] = { 'tgt_id': 3, 'gold': 1, 'ent_mask': True,\n                     'ent_gold': self.init_gold-1, 'tgt_gold': self.init_gold+1 }\n    # agent 2: give gold to agent 4 (valid: same tile)\n    test_cond[2] = { 'tgt_id': 4, 'gold': self.init_gold, 'ent_mask': True,\n                     'ent_gold': 0, 'tgt_gold': 2*self.init_gold }\n    # agent 3: give gold to npc -1 (invalid: cannot give to npc)\n    #  ent_gold is self.init_gold+1 because (3) got 1 gold from (1)\n    test_cond[3] = { 'tgt_id': -1, 'gold': 1, 'ent_mask': False,\n                     'ent_gold': self.init_gold+1, 'tgt_gold': self.init_gold }\n    # agent 4: give -1 gold to 2 (invalid: cannot give minus gold)\n    #  ent_gold is 2*self.init_gold because (4) got 5 gold from (2)\n    #  tgt_gold is 0 because (2) gave all gold to (4)\n    test_cond[4] = { 'tgt_id': 2, 'gold': -1, 'ent_mask': True,\n                     'ent_gold': 2*self.init_gold, 'tgt_gold': 0 }\n\n    actions = self._check_assert_make_action(env, action.GiveGold, test_cond)\n    env.step(actions)\n\n    # check the results\n    for ent_id, cond in test_cond.items():\n      self.assertEqual(cond['ent_gold'], env.realm.players[ent_id].gold.val)\n      if cond['tgt_id'] > 0:\n        self.assertEqual(cond['tgt_gold'], env.realm.players[cond['tgt_id']].gold.val)\n\n    # DONE\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/action/test_monkey_action.py",
    "content": "import unittest\nimport random\nfrom tqdm import tqdm\n\nimport numpy as np\n\nfrom tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv\n\nimport nmmo\n\n# 30 seems to be enough to test variety of agent actions\nTEST_HORIZON = 30\nRANDOM_SEED = random.randint(0, 1000000)\n\n\ndef make_random_actions(config, ent_obs):\n  assert 'ActionTargets' in ent_obs, 'ActionTargets is not provided in the obs'\n  actions = {}\n\n  # atn, arg, val\n  for atn in sorted(nmmo.Action.edges(config)):\n    actions[atn] = {}\n    for arg in sorted(atn.edges, reverse=True): # intentionally doing wrong\n      mask = ent_obs[\"ActionTargets\"][atn.__name__][arg.__name__]\n      actions[atn][arg] = 0\n      if np.any(mask):\n        actions[atn][arg] += int(np.random.choice(np.where(mask)[0]))\n\n  return actions\n\n# CHECK ME: this would be nice to include in the env._validate_actions()\ndef filter_item_actions(actions, use_str_key=False):\n  # when there are multiple actions on the same item, select one\n  flt_atns = {}\n  inventory_atn = {} # key: inventory idx, val: action\n  for atn in actions:\n    if atn in [nmmo.action.Use, nmmo.action.Sell, nmmo.action.Give, nmmo.action.Destroy]:\n      for arg, val in actions[atn].items():\n        if arg == nmmo.action.InventoryItem:\n          if val not in inventory_atn:\n            inventory_atn[val] = [( atn, actions[atn] )]\n          else:\n            inventory_atn[val].append(( atn, actions[atn] ))\n    else:\n      flt_atns[atn] = actions[atn]\n\n    # randomly select one action for each inventory item\n    for atns in inventory_atn.values():\n      if len(atns) > 1:\n        picked = random.choice(atns)\n        flt_atns[picked[0]] = picked[1]\n      else:\n        flt_atns[atns[0][0]] = atns[0][1]\n\n    # convert action keys to str\n    if use_str_key:\n      str_atns = {}\n      for atn, args in flt_atns.items():\n        str_atns[atn.__name__] = {}\n        for arg, val in args.items():\n          str_atns[atn.__name__][arg.__name__] = val\n      flt_atns = str_atns\n\n    return flt_atns\n\n\nclass TestMonkeyAction(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = ScriptedAgentTestConfig()\n    cls.config.PROVIDE_ACTION_TARGETS = True\n\n  @staticmethod\n  # NOTE: this can also be used for sweeping random seeds\n  def rollout_with_seed(config, seed, use_str_key=False):\n    env = ScriptedAgentTestEnv(config)\n    obs, _ = env.reset(seed=seed)\n\n    for _ in tqdm(range(TEST_HORIZON)):\n      # sample random actions for each player\n      actions = {}\n      for ent_id in env.realm.players:\n        ent_atns = make_random_actions(config, obs[ent_id])\n        actions[ent_id] = filter_item_actions(ent_atns, use_str_key)\n      obs, _, _, _, _ = env.step(actions)\n\n  def test_monkey_action(self):\n    try:\n      self.rollout_with_seed(self.config, RANDOM_SEED)\n    except: # pylint: disable=bare-except\n      assert False, f\"Monkey action failed. seed: {RANDOM_SEED}\"\n\n  def test_monkey_action_with_str_key(self):\n    self.rollout_with_seed(self.config, RANDOM_SEED, use_str_key=True)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/action/test_sell_buy.py",
    "content": "import unittest\nimport logging\n\nfrom tests.testhelpers import ScriptedTestTemplate, provide_item\n\nfrom nmmo.core import action\nfrom nmmo.systems import item as Item\nfrom nmmo.systems.item import ItemState\nfrom scripted import baselines\n\nRANDOM_SEED = 985\n\nLOGFILE = None  # 'tests/action/test_sell_buy.log'\n\nclass TestSellBuy(ScriptedTestTemplate):\n  # pylint: disable=protected-access,multiple-statements,unsubscriptable-object,no-member\n\n  @classmethod\n  def setUpClass(cls):\n    super().setUpClass()\n\n    # config specific to the tests here\n    cls.config.set(\"PLAYERS\", [baselines.Melee, baselines.Range])\n    cls.config.set(\"PLAYER_N\", 6)\n\n    cls.policy = { 1:'Melee', 2:'Range', 3:'Melee', 4:'Range', 5:'Melee', 6:'Range' }\n    cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Whetstone,\n                 4:Item.Arrow, 5:Item.Whetstone, 6:Item.Arrow }\n\n    if LOGFILE:  # for debugging\n      logging.basicConfig(filename=LOGFILE, level=logging.INFO)\n\n  def test_sell_buy(self):\n    # cannot list an item with 0 price --> impossible to do this\n    # cannot list an equipped item for sale (should be masked)\n    # cannot buy an item with the full inventory,\n    #   but it's possible if the agent has the same ammo stack\n    # cannot buy its own item (should be masked)\n    # cannot buy an item if gold is not enough (should be masked)\n    # cannot list an already listed item for sale (should be masked)\n    env = self._setup_env(random_seed=RANDOM_SEED)\n\n    # make the inventory full for agents 1, 2\n    extra_items = { (Item.Bottom, 0), (Item.Bottom, 3) }\n    for ent_id in [1, 2]:\n      for item_sig in extra_items:\n        self.item_sig[ent_id].append(item_sig)\n        provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1)\n    env._compute_observations()\n\n    # check if the inventory is full\n    for ent_id in [1, 2]:\n      self.assertEqual(env.obs[ent_id].inventory.len, env.config.ITEM_INVENTORY_CAPACITY)\n      self.assertTrue(env.realm.players[ent_id].inventory.space == 0)\n\n    \"\"\" First tick actions \"\"\"\n    # cannot list an item with 0 price\n    actions = {}\n\n    # agent 1-2: equip the ammo\n    for ent_id in [1, 2]:\n      item_sig = self.item_sig[ent_id][0]\n      self.assertTrue(\n        self._check_inv_mask(env.obs[ent_id], action.Use, item_sig))\n      actions[ent_id] = { action.Use: { action.InventoryItem:\n          env.obs[ent_id].inventory.sig(*item_sig) } }\n\n    # agent 4: list the ammo for sale with price 0\n    #   the zero in action.Price is deserialized into Discrete_1, so it's valid\n    ent_id = 4; price = 0; item_sig = self.item_sig[ent_id][0]\n    actions[ent_id] = { action.Sell: {\n        action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig),\n        action.Price: action.Price.edges[price] } }\n\n    env.step(actions)\n\n    # Check the first tick actions\n    # agent 1-2: the ammo equipped, thus should be masked for sale\n    for ent_id in [1, 2]:\n      item_sig = self.item_sig[ent_id][0]\n      inv_idx = env.obs[ent_id].inventory.sig(*item_sig)\n      self.assertEqual(1, # equipped = true\n        ItemState.parse_array(env.obs[ent_id].inventory.values[inv_idx]).equipped)\n      self.assertFalse( # not allowed to list\n        self._check_inv_mask(env.obs[ent_id], action.Sell, item_sig))\n\n    \"\"\" Second tick actions \"\"\"\n    # listing the level-0 ammo with different prices\n    # cannot list an equipped item for sale (should be masked)\n\n    listing_price = { 1:1, 2:5, 3:15, 5:2 } # gold\n    for ent_id, price in listing_price.items():\n      item_sig = self.item_sig[ent_id][0]\n      actions[ent_id] = { action.Sell: {\n          action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig),\n          action.Price: action.Price.edges[price-1] } }\n\n    env.step(actions)\n\n    # Check the second tick actions\n    # agent 1-2: the ammo equipped, thus not listed for sale\n    # agent 3-5's ammos listed for sale\n    for ent_id, price in listing_price.items():\n      item_id = env.obs[ent_id].inventory.id(0)\n\n      if ent_id in [1, 2]: # failed to list for sale\n        self.assertFalse(item_id in env.obs[ent_id].market.ids) # not listed\n        self.assertEqual(0,\n          ItemState.parse_array(env.obs[ent_id].inventory.values[0]).listed_price)\n\n      else: # should succeed to list for sale\n        self.assertTrue(item_id in env.obs[ent_id].market.ids) # listed\n        self.assertEqual(price, # sale price set\n          ItemState.parse_array(env.obs[ent_id].inventory.values[0]).listed_price)\n\n        # should not buy mine\n        self.assertFalse( self._check_mkt_mask(env.obs[ent_id], item_id))\n\n        # should not list the same item twice\n        self.assertFalse(\n          self._check_inv_mask(env.obs[ent_id], action.Sell, self.item_sig[ent_id][0]))\n\n    \"\"\" Third tick actions \"\"\"\n    # cannot buy an item with the full inventory,\n    #   but it's possible if the agent has the same ammo stack\n    # cannot buy its own item (should be masked)\n    # cannot buy an item if gold is not enough (should be masked)\n    # cannot list an already listed item for sale (should be masked)\n\n    test_cond = {}\n\n    # agent 1: buy agent 5's ammo (valid: 1 has the same ammo stack)\n    #   although 1's inventory is full, this action is valid\n    agent5_ammo = env.obs[5].inventory.id(0)\n    test_cond[1] = { 'item_id': agent5_ammo, 'mkt_mask': True }\n\n    # agent 2: buy agent 5's ammo (invalid: full space and no same stack)\n    test_cond[2] = { 'item_id': agent5_ammo, 'mkt_mask': False }\n\n    # agent 4: cannot buy its own item (invalid)\n    test_cond[4] = { 'item_id': env.obs[4].inventory.id(0), 'mkt_mask': False }\n\n    # agent 5: cannot buy agent 3's ammo (invalid: not enought gold)\n    test_cond[5] = { 'item_id': env.obs[3].inventory.id(0), 'mkt_mask': False }\n\n    actions = self._check_assert_make_action(env, action.Buy, test_cond)\n\n    # agent 3: list an already listed item for sale (try different price)\n    ent_id = 3; item_sig = self.item_sig[ent_id][0]\n    actions[ent_id] = { action.Sell: {\n        action.InventoryItem: env.obs[ent_id].inventory.sig(*item_sig),\n        action.Price: action.Price.edges[7] } } # try to set different price\n\n    env.step(actions)\n\n    # Check the third tick actions\n    # agent 1: buy agent 5's ammo (valid: 1 has the same ammo stack)\n    #   agent 5's ammo should be gone\n    seller_id = 5; buyer_id = 1\n    self.assertFalse( agent5_ammo in env.obs[seller_id].inventory.ids)\n    self.assertEqual( env.realm.players[seller_id].gold.val, # gold transfer\n                      self.init_gold + listing_price[seller_id])\n    self.assertEqual(2 * self.ammo_quantity, # ammo transfer\n          ItemState.parse_array(env.obs[buyer_id].inventory.values[0]).quantity)\n    self.assertEqual( env.realm.players[buyer_id].gold.val, # gold transfer\n                      self.init_gold - listing_price[seller_id])\n\n    # agent 2-4: invalid buy, no exchange, thus the same money\n    for ent_id in [2, 3, 4]:\n      self.assertEqual( env.realm.players[ent_id].gold.val, self.init_gold)\n\n    # DONE\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\n#pylint: disable=unused-argument\n\nimport logging\nlogging.basicConfig(level=logging.INFO, stream=None)\n\ndef pytest_benchmark_scale_unit(config, unit, benchmarks, best, worst, sort):\n  if unit == 'seconds':\n    prefix = 'millisec'\n    scale = 1000\n  elif unit == 'operations':\n    prefix = ''\n    scale = 1\n  else:\n    raise RuntimeError(f\"Unexpected measurement unit {unit}\")\n  return prefix, scale\n"
  },
  {
    "path": "tests/core/test_config.py",
    "content": "import unittest\n\nimport nmmo\nimport nmmo.core.config as cfg\n\n\nclass Config(cfg.Config, cfg.Terrain, cfg.Combat):\n  pass\n\nclass TestConfig(unittest.TestCase):\n  def test_config_attr_set_episode(self):\n    config = nmmo.config.Default()\n    self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, True)\n\n    config.set_for_episode(\"RESOURCE_SYSTEM_ENABLED\", False)\n    self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, False)\n\n    config.reset()\n    self.assertEqual(config.RESOURCE_SYSTEM_ENABLED, True)\n\n  def test_cannot_change_immutable_attr(self):\n    config = Config()\n    with self.assertRaises(AssertionError):\n      config.set_for_episode(\"PLAYER_N\", 100)\n\n  def test_cannot_change_obs_attr(self):\n    config = Config()\n    with self.assertRaises(AssertionError):\n      config.set_for_episode(\"PLAYER_N_OBS\", 50)\n\n  def test_cannot_use_noninit_system(self):\n    config = Config()\n    with self.assertRaises(AssertionError):\n      config.set_for_episode(\"ITEM_SYSTEM_ENABLED\", True)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_cython_masks.py",
    "content": "# pylint: disable=protected-access,bad-builtin\nimport unittest\nfrom timeit import timeit\nfrom copy import deepcopy\n#import random\nimport numpy as np\n\nimport nmmo\nfrom tests.testhelpers import ScriptedAgentTestConfig\n\nRANDOM_SEED = 3333  # random.randint(0, 10000)\nPERF_TEST = True\n\nclass TestCythonMasks(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = ScriptedAgentTestConfig()\n    cls.config.set(\"USE_CYTHON\", True)\n    cls.config.set(\"COMBAT_SPAWN_IMMUNITY\", 5)\n    cls.env = nmmo.Env(cls.config, RANDOM_SEED)\n    cls.env.reset()\n    for _ in range(7):\n      cls.env.step({})\n\n    cls.move_mask = cls.env._dummy_obs[\"ActionTargets\"][\"Move\"]\n    cls.attack_mask = cls.env._dummy_obs[\"ActionTargets\"][\"Attack\"]\n\n  def test_move_mask(self):\n    obs = self.env.obs\n    for agent_id in self.env.realm.players:\n      np_masks = deepcopy(self.move_mask)\n      cy_masks = deepcopy(self.move_mask)\n      obs[agent_id]._make_move_mask(np_masks, use_cython=False)\n      obs[agent_id]._make_move_mask(cy_masks, use_cython=True)\n      self.assertTrue(np.array_equal(np_masks[\"Direction\"], cy_masks[\"Direction\"]))\n    if PERF_TEST:\n      print('---test_move_mask---')\n      print('numpy:', timeit(\n        lambda: [obs[agent_id]._make_move_mask(np_masks, use_cython=False)\n                 for agent_id in self.env.realm.players], number=1000, globals=globals()))\n      print('cython:', timeit(\n        lambda: [obs[agent_id]._make_move_mask(cy_masks, use_cython=True)\n                 for agent_id in self.env.realm.players], number=1000, globals=globals()))\n\n  def test_attack_mask(self):\n    obs = self.env.obs\n    for agent_id in self.env.realm.players:\n      np_masks = deepcopy(self.attack_mask)\n      cy_masks = deepcopy(self.attack_mask)\n      obs[agent_id]._make_attack_mask(np_masks, use_cython=False)\n      obs[agent_id]._make_attack_mask(cy_masks, use_cython=True)\n      self.assertTrue(np.array_equal(np_masks[\"Target\"], cy_masks[\"Target\"]))\n    if PERF_TEST:\n      print('---test_attack_mask---')\n      print('numpy:', timeit(\n        lambda: [obs[agent_id]._make_attack_mask(np_masks, use_cython=False)\n                 for agent_id in self.env.realm.players], number=1000, globals=globals()))\n      print('cython:', timeit(\n        lambda: [obs[agent_id]._make_attack_mask(cy_masks, use_cython=True)\n                 for agent_id in self.env.realm.players], number=1000, globals=globals()))\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_entity.py",
    "content": "import unittest\nimport numpy as np\n\nimport nmmo\nfrom nmmo.entity.entity import Entity, EntityState\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom scripted.baselines import Random\n\n\nclass MockRealm:\n  def __init__(self):\n    self.config = nmmo.config.Default()\n    self.config.PLAYERS = range(100)\n    self.datastore = NumpyDatastore()\n    self.datastore.register_object_type(\"Entity\", EntityState.State.num_attributes)\n    self._np_random = np.random\n\n# pylint: disable=no-member\nclass TestEntity(unittest.TestCase):\n  def test_entity(self):\n    realm = MockRealm()\n    entity_id = 123\n    entity = Entity(realm, (10,20), entity_id, \"name\")\n\n    self.assertEqual(entity.id.val, entity_id)\n    self.assertEqual(entity.row.val, 10)\n    self.assertEqual(entity.col.val, 20)\n    self.assertEqual(entity.damage.val, 0)\n    self.assertEqual(entity.time_alive.val, 0)\n    self.assertEqual(entity.freeze.val, 0)\n    self.assertEqual(entity.item_level.val, 0)\n    self.assertEqual(entity.attacker_id.val, 0)\n    self.assertEqual(entity.message.val, 0)\n    self.assertEqual(entity.gold.val, 0)\n    self.assertEqual(entity.health.val, realm.config.PLAYER_BASE_HEALTH)\n    self.assertEqual(entity.food.val, realm.config.RESOURCE_BASE)\n    self.assertEqual(entity.water.val, realm.config.RESOURCE_BASE)\n    self.assertEqual(entity.melee_level.val, 0)\n    self.assertEqual(entity.range_level.val, 0)\n    self.assertEqual(entity.mage_level.val, 0)\n    self.assertEqual(entity.fishing_level.val, 0)\n    self.assertEqual(entity.herbalism_level.val, 0)\n    self.assertEqual(entity.prospecting_level.val, 0)\n    self.assertEqual(entity.carving_level.val, 0)\n    self.assertEqual(entity.alchemy_level.val, 0)\n\n  def test_query_by_ids(self):\n    realm = MockRealm()\n    entity_id = 123\n    entity = Entity(realm, (10,20), entity_id, \"name\")\n\n    entities = EntityState.Query.by_ids(realm.datastore, [entity_id])\n    self.assertEqual(len(entities), 1)\n    self.assertEqual(entities[0][Entity.State.attr_name_to_col[\"id\"]], entity_id)\n    self.assertEqual(entities[0][Entity.State.attr_name_to_col[\"row\"]], 10)\n    self.assertEqual(entities[0][Entity.State.attr_name_to_col[\"col\"]], 20)\n\n    entity.food.update(11)\n    e_row = EntityState.Query.by_id(realm.datastore, entity_id)\n    self.assertEqual(e_row[Entity.State.attr_name_to_col[\"food\"]], 11)\n\n  def test_recon_resurrect(self):\n    config = nmmo.config.Default()\n    config.set(\"PLAYERS\", [Random])\n    env = nmmo.Env(config)\n    env.reset()\n\n    # set player 1 to be a recon\n    # Recons are immortal and cannot act (move)\n    player1 = env.realm.players[1]\n    player1.make_recon()\n    spawn_pos = player1.pos\n\n    for _ in range(50):  # long enough to starve to death\n      env.step({})\n      self.assertEqual(player1.pos, spawn_pos)\n      self.assertEqual(player1.health.val, config.PLAYER_BASE_HEALTH)\n\n    # resurrect player1\n    player1.health.update(0)\n    self.assertEqual(player1.alive, False)\n    env.step({})\n\n    player1.resurrect(health_prop=0.5, freeze_duration=10)\n    self.assertEqual(player1.health.val, 50)\n    self.assertEqual(player1.freeze.val, 10)\n    self.assertEqual(player1.message.val, 0)\n    self.assertEqual(player1.npc_type, -1)  # immortal flag\n    self.assertEqual(player1.my_task.progress, 0)  # task progress should be reset\n    # pylint:disable=protected-access\n    self.assertEqual(player1._make_mortal_tick, env.realm.tick + 10)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_env.py",
    "content": "import unittest\nfrom typing import List\n\nimport random\nimport numpy as np\nfrom tqdm import tqdm\n\nimport nmmo\nfrom nmmo.core.realm import Realm\nfrom nmmo.core.tile import TileState\nfrom nmmo.entity.entity import Entity, EntityState\nfrom nmmo.systems.item import ItemState\nfrom scripted import baselines\n\n# Allow private access for testing\n# pylint: disable=protected-access\n\n# 30 seems to be enough to test variety of agent actions\nTEST_HORIZON = 30\nRANDOM_SEED = random.randint(0, 10000)\n\nclass Config(nmmo.config.Small, nmmo.config.AllGameSystems):\n  PLAYERS = [\n    baselines.Fisher, baselines.Herbalist, baselines.Prospector,\n    baselines.Carver, baselines.Alchemist,\n    baselines.Melee, baselines.Range, baselines.Mage]\n\nclass TestEnv(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = Config()\n    cls.env = nmmo.Env(cls.config, RANDOM_SEED)\n\n  def test_action_space(self):\n    action_space = self.env.action_space(0)\n    atn_str_keys = set(atn.__name__ for atn in nmmo.Action.edges(self.config))\n    self.assertSetEqual(\n        set(action_space.keys()),\n        atn_str_keys)\n\n  def test_observations(self):\n    obs, _ = self.env.reset()\n    self.assertEqual(obs.keys(), self.env.realm.players.keys())\n\n    for _ in tqdm(range(TEST_HORIZON)):\n      entity_locations = [\n        [ev.row.val, ev.col.val, e] for e, ev in self.env.realm.players.entities.items()\n      ] + [\n        [ev.row.val, ev.col.val, e] for e, ev in self.env.realm.npcs.entities.items()\n      ]\n\n      for player_id, player_obs in obs.items():\n        if player_id in self.env.realm.players: # alive agents\n          self._validate_tiles(player_obs, self.env.realm)\n          self._validate_entitites(\n              player_id, player_obs, self.env.realm, entity_locations)\n          self._validate_inventory(player_id, player_obs, self.env.realm)\n          self._validate_market(player_obs, self.env.realm)\n        else:\n          # the obs of dead agents are dummy, all zeros\n          self.assertEqual(np.sum(player_obs[\"Tile\"]), 0)\n          self.assertEqual(np.sum(player_obs[\"Entity\"]), 0)\n          self.assertEqual(np.sum(player_obs[\"Inventory\"]), 0)\n          self.assertEqual(np.sum(player_obs[\"Market\"]), 0)\n          self.assertEqual(np.sum(player_obs[\"ActionTargets\"][\"Move\"][\"Direction\"]), 1)  # no-op\n          self.assertEqual(np.sum(player_obs[\"ActionTargets\"][\"Attack\"][\"Style\"]), 3)  # all ones\n\n      obs, rewards, terminated, truncated, infos = self.env.step({})\n\n      # make sure dead agents return proper dones=True, dummy obs, and -1 reward\n      self.assertEqual(len(self.env.agents), len(self.env.realm.players))\n      # NOTE: the below is no longer true when mini games resurrect dead players\n      # self.assertEqual(len(self.env.possible_agents),\n      #                  len(self.env.realm.players) + len(self.env._dead_agents))\n      for agent_id in self.env.agents:\n        self.assertTrue(agent_id in obs)\n        self.assertTrue(agent_id in rewards)\n        self.assertTrue(agent_id in terminated)\n        self.assertTrue(agent_id in truncated)\n        self.assertTrue(agent_id in infos)\n      for dead_id in self.env._dead_this_tick:\n        self.assertEqual(rewards[dead_id], -1)\n        self.assertTrue(terminated[dead_id])\n\n      # check dead and alive\n      entity_all = EntityState.Query.table(self.env.realm.datastore)\n      alive_agents = entity_all[:, Entity.State.attr_name_to_col[\"id\"]]\n      alive_agents = set(alive_agents[alive_agents > 0])\n      for agent_id in alive_agents:\n        self.assertTrue(agent_id in self.env.realm.players)\n\n  def _validate_tiles(self, obs, realm: Realm):\n    for tile_obs in obs[\"Tile\"]:\n      tile_obs = TileState.parse_array(tile_obs)\n      tile = realm.map.tiles[(int(tile_obs.row), int(tile_obs.col))]\n      for key, val in tile_obs.__dict__.items():\n        if val != getattr(tile, key).val:\n          self.assertEqual(val, getattr(tile, key).val,\n            f\"Mismatch for {key} in tile {tile_obs.row}, {tile_obs.col}\")\n\n  def _validate_entitites(self, player_id, obs, realm: Realm, entity_locations: List[List[int]]):\n    observed_entities = set()\n\n    for entity_obs in obs[\"Entity\"]:\n      entity_obs = EntityState.parse_array(entity_obs)\n\n      if entity_obs.id == 0:\n        continue\n\n      entity: Entity = realm.entity(entity_obs.id)\n\n      observed_entities.add(entity.ent_id)\n\n      for key, val in entity_obs.__dict__.items():\n        if getattr(entity, key) is None:\n          raise ValueError(f\"Entity {entity} has no attribute {key}\")\n        self.assertEqual(val, getattr(entity, key).val,\n          f\"Mismatch for {key} in entity {entity_obs.id}\")\n\n    # Make sure that we see entities IFF they are in our vision radius\n    row = realm.players.entities[player_id].row.val\n    col = realm.players.entities[player_id].col.val\n    vision = realm.config.PLAYER_VISION_RADIUS\n    visible_entities = {\n      e for r, c, e in entity_locations\n      if row - vision <= r <= row + vision and col - vision <= c <= col + vision\n    }\n    self.assertSetEqual(visible_entities, observed_entities,\n      f\"Mismatch between observed: {observed_entities} \" \\\n        f\"and visible {visible_entities} for player {player_id}, \"\\\n        f\" step {self.env.realm.tick}\")\n\n  def _validate_inventory(self, player_id, obs, realm: Realm):\n    self._validate_items(\n        {i.id.val: i for i in realm.players[player_id].inventory.items},\n        obs[\"Inventory\"]\n    )\n\n  def _validate_market(self, obs, realm: Realm):\n    self._validate_items(\n        {i.item.id.val: i.item for i in realm.exchange._item_listings.values()},\n        obs[\"Market\"]\n    )\n\n  def _validate_items(self, items_dict, item_obs):\n    item_obs = item_obs[item_obs[:,0] != 0]\n    if len(items_dict) != len(item_obs):\n      assert len(items_dict) == len(item_obs),\\\n        f\"Mismatch in number of items. Random seed: {RANDOM_SEED}\"\n    for item_ob in item_obs:\n      item_ob = ItemState.parse_array(item_ob)\n      item = items_dict[item_ob.id]\n      for key, val in item_ob.__dict__.items():\n        self.assertEqual(val, getattr(item, key).val,\n          f\"Mismatch for {key} in item {item_ob.id}: {val} != {getattr(item, key).val}\")\n\n  def test_clean_item_after_reset(self):\n    # use the separate env\n    new_env = nmmo.Env(self.config, RANDOM_SEED)\n\n    # reset the environment after running\n    new_env.reset()\n    for _ in tqdm(range(TEST_HORIZON)):\n      new_env.step({})\n    new_env.reset()\n\n    # items are referenced in the realm.items, which must be empty\n    self.assertTrue(len(new_env.realm.items) == 0)\n    self.assertTrue(len(new_env.realm.exchange._item_listings) == 0)\n    self.assertTrue(len(new_env.realm.exchange._listings_queue) == 0)\n\n    # item state table must be empty after reset\n    self.assertTrue(ItemState.State.table(new_env.realm.datastore).is_empty())\n\n  def test_truncated(self):\n    test_horizon = 25\n    config = Config()\n    config.set(\"HORIZON\", test_horizon)\n    env = nmmo.Env(config, RANDOM_SEED)\n    obs, _ = env.reset()\n    for _ in tqdm(range(test_horizon)):\n      obs, _, terminated, truncated, _ = env.step({})\n      for agent_id in obs:\n        alive = agent_id in env.realm.players\n        self.assertEqual(terminated[agent_id], not alive)\n        # Test that the last step is truncated\n        self.assertEqual(truncated[agent_id], alive and env.realm.tick >= test_horizon)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_game_api.py",
    "content": "# pylint: disable=protected-access\nimport unittest\n\nimport nmmo\nfrom nmmo.core.game_api import AgentTraining, TeamTraining, TeamBattle\nfrom nmmo.lib.team_helper import TeamHelper\n\n\nNUM_TEAMS = 16\nTEAM_SIZE = 8\n\nclass TeamConfig(nmmo.config.Small, nmmo.config.AllGameSystems):\n  PLAYER_N = NUM_TEAMS * TEAM_SIZE\n  TEAMS = {\"Team\" + str(i+1): [i*TEAM_SIZE+j+1 for j in range(TEAM_SIZE)]\n           for i in range(NUM_TEAMS)}\n  CURRICULUM_FILE_PATH = \"tests/task/sample_curriculum.pkl\"\n\nclass TestGameApi(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = TeamConfig()\n    cls.env = nmmo.Env(cls.config)\n\n  def test_num_agents_in_teams(self):\n    # raise error if PLAYER_N is not equal to the number of agents in TEAMS\n    config = TeamConfig()\n    config.set(\"PLAYER_N\", 127)\n    env = nmmo.Env(config)\n    self.assertRaises(AssertionError, lambda: TeamTraining(env))\n\n  def test_agent_training_game(self):\n    game = AgentTraining(self.env)\n    self.env.reset(game=game)\n\n    # this should use the DefaultGame setup\n    self.assertTrue(isinstance(self.env.game, AgentTraining))\n    for task in self.env.tasks:\n      self.assertEqual(task.reward_to, \"agent\")  # all tasks are for agents\n\n    # every agent is assigned a task\n    self.assertEqual(len(self.env.possible_agents), len(self.env.tasks))\n    # for the training tasks, the task assignee and subject should be the same\n    for task in self.env.tasks:\n      self.assertEqual(task.assignee, task.subject)\n\n    # winners should be none when not determined\n    self.assertEqual(self.env.game.winners, None)\n    self.assertEqual(self.env.game.is_over, False)\n\n    # make agent 1 a winner by destroying all other agents\n    for agent_id in self.env.possible_agents[1:]:\n      self.env.realm.players[agent_id].resources.health.update(0)\n    self.env.step({})\n    self.assertEqual(self.env.game.winners, [1])\n\n    # when there are winners, the game is over\n    self.assertEqual(self.env.game.is_over, True)\n\n  def test_team_training_game_spawn(self):\n    # when TEAMS is set, the possible agents should include all agents\n    team_helper = TeamHelper(self.config.TEAMS)\n    self.assertListEqual(self.env.possible_agents,\n                         list(team_helper.team_and_position_for_agent.keys()))\n\n    game = TeamTraining(self.env)\n    self.env.reset(game=game)\n\n    for task in self.env.tasks:\n      self.assertEqual(task.reward_to, \"team\")  # all tasks are for teams\n\n    # agents in the same team should spawn together\n    team_locs = {}\n    for team_id, team_members in self.env.config.TEAMS.items():\n      team_locs[team_id] = self.env.realm.players[team_members[0]].pos\n      for agent_id in team_members:\n        self.assertEqual(team_locs[team_id], self.env.realm.players[agent_id].pos)\n\n    # teams should be apart from each other\n    for team_a in self.config.TEAMS.keys():\n      for team_b in self.config.TEAMS.keys():\n        if team_a != team_b:\n          self.assertNotEqual(team_locs[team_a], team_locs[team_b])\n\n  def test_team_battle_mode(self):\n    game = TeamBattle(self.env)\n    self.env.reset(game=game)\n    env = self.env\n\n    # battle mode: all teams share the same task\n    task_spec_name = env.tasks[0].spec_name\n    for task in env.tasks:\n      self.assertEqual(task.reward_to, \"team\")  # all tasks are for teams\n      self.assertEqual(task.spec_name, task_spec_name)  # all tasks are the same in competition\n\n    # set the first team to win\n    winner_team = \"Team1\"\n    for team_id, members in env.config.TEAMS.items():\n      if team_id != winner_team:\n        for agent_id in members:\n          env.realm.players[agent_id].resources.health.update(0)\n    env.step({})\n    self.assertEqual(env.game.winners, env.config.TEAMS[winner_team])\n\n  def test_competition_winner_task_completed(self):\n    game = TeamBattle(self.env)\n    self.env.reset(game=game)\n\n    # The first two tasks get completed\n    winners = []\n    for task in self.env.tasks[:2]:\n      task._completed_tick = 1\n      self.assertEqual(task.completed, True)\n      winners += task.assignee\n\n    self.env.step({})\n    self.assertEqual(self.env.game.winners, winners)\n\n  def test_game_via_config(self):\n    config = TeamConfig()\n    config.set(\"GAME_PACKS\", [(AgentTraining, 1),\n                              (TeamTraining, 1),\n                              (TeamBattle, 1)])\n    env = nmmo.Env(config)\n    env.reset()\n    for _ in range(3):\n      env.step({})\n\n    self.assertTrue(isinstance(env.game, game_cls)\n                    for game_cls in [AgentTraining, TeamTraining, TeamBattle])\n\n  def test_game_set_next_task(self):\n    game = AgentTraining(self.env)\n    tasks = game._define_tasks()  # sample tasks for testing\n    game.set_next_tasks(tasks)\n    self.env.reset(game=game)\n\n    # The tasks are successfully fed into the env\n    for a, b in zip(tasks, self.env.tasks):\n      self.assertIs(a, b)\n\n    # The next tasks is empty\n    self.assertIsNone(game._next_tasks)\n\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_gym_obs_spaces.py",
    "content": "import unittest\nfrom copy import deepcopy\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core.game_api import DefaultGame\n\nRANDOM_SEED = np.random.randint(0, 100000)\n\n\nclass TestGymObsSpaces(unittest.TestCase):\n  def _is_obs_valid(self, obs_spec, obs):\n    for agent_obs in obs.values():\n      for key, val in agent_obs.items():\n        self.assertTrue(obs_spec[key].contains(val),\n                        f\"Invalid obs format -- key: {key}, val: {val}\")\n\n  def _test_gym_obs_space(self, env):\n    obs_spec = env.observation_space(1)\n    obs, _, _, _, _ = env.step({})\n    self._is_obs_valid(obs_spec, obs)\n    for agent_obs in obs.values():\n      if \"ActionTargets\" in agent_obs:\n        val = agent_obs[\"ActionTargets\"]\n        for atn in nmmo.Action.edges(env.config):\n          if atn.enabled(env.config):\n            for arg in atn.edges: # pylint: disable=not-an-iterable\n              mask_spec = obs_spec[\"ActionTargets\"][atn.__name__][arg.__name__]\n              mask_val = val[atn.__name__][arg.__name__]\n              self.assertTrue(mask_spec.contains(mask_val),\n                              \"Invalid obs format -- \" + \\\n                              f\"key: {atn.__name__}/{arg.__name__}, val: {mask_val}\")\n    return obs\n\n  def test_env_without_noop(self):\n    config = nmmo.config.Default()\n    config.set(\"PROVIDE_NOOP_ACTION_TARGET\", False)\n    env = nmmo.Env(config)\n    env.reset(seed=1)\n    for _ in range(3):\n      env.step({})\n    self._test_gym_obs_space(env)\n\n  def test_env_with_noop(self):\n    config = nmmo.config.Default()\n    config.set(\"PROVIDE_NOOP_ACTION_TARGET\", True)\n    env = nmmo.Env(config)\n    env.reset(seed=1)\n    for _ in range(3):\n      env.step({})\n    self._test_gym_obs_space(env)\n\n  def test_env_with_fogmap(self):\n    config = nmmo.config.Default()\n    config.set(\"PROVIDE_DEATH_FOG_OBS\", True)\n    env = nmmo.Env(config)\n    env.reset(seed=1)\n    for _ in range(3):\n      env.step({})\n    self._test_gym_obs_space(env)\n\n  def test_system_disable(self):\n    class CustomGame(DefaultGame):\n      def _set_config(self):\n        self.config.reset()\n        self.config.set_for_episode(\"COMBAT_SYSTEM_ENABLED\", False)\n        self.config.set_for_episode(\"ITEM_SYSTEM_ENABLED\", False)\n        self.config.set_for_episode(\"EXCHANGE_SYSTEM_ENABLED\", False)\n        self.config.set_for_episode(\"COMMUNICATION_SYSTEM_ENABLED\", False)\n\n    config = nmmo.config.Default()\n    env = nmmo.Env(config)\n\n    # test the default game\n    env.reset()\n    for _ in range(3):\n      env.step({})\n    self._test_gym_obs_space(env)\n    org_obs_spec = deepcopy(env.observation_space(1))\n\n    # test the custom game\n    game = CustomGame(env)\n    env.reset(game=game, seed=RANDOM_SEED)\n    for _ in range(3):\n      env.step({})\n    new_obs = self._test_gym_obs_space(env)\n\n    # obs format must match between episodes\n    self._is_obs_valid(org_obs_spec, new_obs)\n\n    # check if the combat system is disabled\n    for agent_obs in new_obs.values():\n      self.assertEqual(sum(agent_obs[\"ActionTargets\"][\"Attack\"][\"Target\"]),\n                        int(config.PROVIDE_NOOP_ACTION_TARGET),\n                        f\"Incorrect gym obs. seed: {RANDOM_SEED}\")\n\nif __name__ == \"__main__\":\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_map_generation.py",
    "content": "# pylint: disable=protected-access\nimport unittest\nimport os\nimport shutil\nimport numpy as np\n\nimport nmmo\nfrom nmmo.lib import material\n\n\nclass TestMapGeneration(unittest.TestCase):\n  def test_insufficient_maps(self):\n    config = nmmo.config.Small()\n    config.set(\"PATH_MAPS\", \"maps/test_map_gen\")\n    config.set(\"MAP_N\", 20)\n\n    # clear the directory\n    path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS)\n    shutil.rmtree(path_maps, ignore_errors=True)\n\n    # this generates 20 maps\n    nmmo.Env(config)\n\n    # test if MAP_FORCE_GENERATION can be overriden, when the maps are insufficient\n    config2 = nmmo.config.Small()\n    config2.set(\"PATH_MAPS\", \"maps/test_map_gen\")  # the same map dir\n    config2.set(\"MAP_N\", 30)\n    config2.set(\"MAP_FORCE_GENERATION\", False)\n\n    test_env = nmmo.Env(config2)\n    test_env.reset(map_id=config.MAP_N)\n\n    # this should finish without error\n\n  def test_map_preview(self):\n    class MapConfig(\n      nmmo.config.Small, # no fractal, grass only\n      nmmo.config.Terrain, # water, grass, foilage, stone\n      nmmo.config.Item, # no additional effect on the map\n      nmmo.config.Profession, # add ore, tree, crystal, herb, fish\n    ):\n      PATH_MAPS = 'maps/test_preview'\n      MAP_FORCE_GENERATION = True\n      MAP_GENERATE_PREVIEWS = True\n    config = MapConfig()\n\n    # clear the directory\n    path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS)\n    shutil.rmtree(path_maps, ignore_errors=True)\n\n    nmmo.Env(config)\n\n    # this should finish without error\n\n  def test_map_reset_from_fractal(self):\n    class MapConfig(\n      nmmo.config.Small, # no fractal, grass only\n      nmmo.config.Terrain, # water, grass, foilage, stone\n      nmmo.config.Item, # no additional effect on the map\n      nmmo.config.Profession, # add ore, tree, crystal, herb, fish\n    ):\n      PATH_MAPS = 'maps/test_fractal'\n      MAP_FORCE_GENERATION = True\n      MAP_RESET_FROM_FRACTAL = True\n    config = MapConfig()\n    self.assertEqual(config.MAP_SIZE, 64)\n    self.assertEqual(config.MAP_CENTER, 32)\n\n    # clear the directory\n    path_maps = os.path.join(config.PATH_CWD, config.PATH_MAPS)\n    shutil.rmtree(path_maps, ignore_errors=True)\n\n    test_env = nmmo.Env(config)\n\n    # the fractals should be saved\n    fractal_file = os.path.join(path_maps, config.PATH_FRACTAL_SUFFIX.format(1))\n    self.assertTrue(os.path.exists(fractal_file))\n\n    config = test_env.config\n    map_size = config.MAP_SIZE\n    np_random = test_env._np_random\n\n    # Return the Grass map\n    config.set_for_episode(\"TERRAIN_SYSTEM_ENABLED\", False)\n    map_dict = test_env._load_map_file()\n    map_array = test_env.realm.map._process_map(map_dict, np_random)\n    self.assertEqual(np.sum(map_array == material.Void.index)+\\\n                     np.sum(map_array == material.Grass.index), map_size*map_size)\n    # NOTE: +1 to make the center tile, really the center\n    self.assertEqual((config.MAP_CENTER+1)**2, np.sum(map_array == material.Grass.index))\n\n    # Another way to make the grass map (which can place other tiles, if want to)\n    config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    config.set_for_episode(\"TERRAIN_RESET_TO_GRASS\", True)\n    config.set_for_episode(\"PROFESSION_SYSTEM_ENABLED\", False)  # harvestalbe tiles\n    config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", False)\n    map_dict = test_env._load_map_file()\n    map_array = test_env.realm.map._process_map(map_dict, np_random)\n    self.assertEqual(np.sum(map_array == material.Void.index)+\\\n                     np.sum(map_array == material.Grass.index), map_size*map_size)\n    # NOTE: +1 to make the center tile, really the center\n    self.assertEqual((config.MAP_CENTER+1)**2, np.sum(map_array == material.Grass.index))\n\n    # Generate from fractal, but not spawn profession tiles\n    config.reset()\n    config.set_for_episode(\"PROFESSION_SYSTEM_ENABLED\", False)\n    map_dict = test_env._load_map_file()\n    map_array = test_env.realm.map._process_map(map_dict, np_random)\n    self.assertEqual(np.sum(map_array == material.Void.index)+\\\n                     np.sum(map_array == material.Grass.index)+\\\n                     np.sum(map_array == material.Water.index)+\\\n                     np.sum(map_array == material.Stone.index)+\\\n                     np.sum(map_array == material.Foilage.index),\n                     map_size*map_size)\n\n    # Use the saved map, but disable stone\n    config.reset()\n    config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", False)\n    config.set_for_episode(\"TERRAIN_DISABLE_STONE\", True)\n    map_dict = test_env._load_map_file()\n    org_map = map_dict[\"map\"].copy()\n    self.assertTrue(\"fractal\" not in map_dict)\n    map_array = test_env.realm.map._process_map(map_dict, np_random)\n    self.assertTrue(np.sum(org_map == material.Stone.index) > 0)\n    self.assertTrue(np.sum(map_array == material.Stone.index) == 0)\n\n    # Generate from fractal, test add-on functions\n    config.reset()\n    config.set_for_episode(\"MAP_RESET_FROM_FRACTAL\", True)\n    config.set_for_episode(\"PROFESSION_SYSTEM_ENABLED\", True)\n    config.set_for_episode(\"TERRAIN_SCATTER_EXTRA_RESOURCES\", True)\n    map_dict = test_env._load_map_file()\n    map_array = test_env.realm.map._process_map(map_dict, np_random)\n\n    # this should finish without error\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_observation_tile.py",
    "content": "# pylint: disable=protected-access,bad-builtin\nimport unittest\nfrom timeit import timeit\nfrom collections import defaultdict\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core.tile import TileState\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.systems.item import ItemState\nfrom nmmo.lib.event_log import EventState\nfrom nmmo.core.observation import Observation\nfrom nmmo.core import action as Action\nfrom nmmo.lib import utils\nfrom tests.testhelpers import ScriptedAgentTestConfig\n\nTileAttr = TileState.State.attr_name_to_col\nEntityAttr = EntityState.State.attr_name_to_col\nItemAttr = ItemState.State.attr_name_to_col\nEventAttr = EventState.State.attr_name_to_col\n\n\nclass TestObservationTile(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = nmmo.config.Default()\n    cls.env = nmmo.Env(cls.config)\n    cls.env.reset(seed=1)\n    for _ in range(3):\n      cls.env.step({})\n\n  def test_tile_attr(self):\n    self.assertDictEqual(TileAttr, {'row': 0, 'col': 1, 'material_id': 2})\n\n  def test_action_target_consts(self):\n    self.assertEqual(len(Action.Style.edges), 3)\n    self.assertEqual(len(Action.Price.edges), self.config.PRICE_N_OBS)\n    self.assertEqual(len(Action.Token.edges), self.config.COMMUNICATION_NUM_TOKENS)\n\n  def test_obs_tile_correctness(self):\n    center = self.config.PLAYER_VISION_RADIUS\n    tile_dim = self.config.PLAYER_VISION_DIAMETER\n    self.env._compute_observations()\n    obs = self.env.obs\n\n    # pylint: disable=inconsistent-return-statements\n    def correct_tile(agent_obs: Observation, r_delta, c_delta):\n      agent = agent_obs.agent\n      if (0 <= agent.row + r_delta < self.config.MAP_SIZE) & \\\n        (0 <= agent.col + c_delta < self.config.MAP_SIZE):\n        r_cond = (agent_obs.tiles[:,TileState.State.attr_name_to_col[\"row\"]] == agent.row+r_delta)\n        c_cond = (agent_obs.tiles[:,TileState.State.attr_name_to_col[\"col\"]] == agent.col+c_delta)\n        return TileState.parse_array(agent_obs.tiles[r_cond & c_cond][0])\n\n    for agent_obs in obs.values():\n      # check if the tile obs size\n      self.assertEqual(len(agent_obs.tiles), self.config.MAP_N_OBS)\n\n      # check if the coord conversion is correct\n      row_map = agent_obs.tiles[:,TileAttr['row']].reshape(tile_dim,tile_dim)\n      col_map = agent_obs.tiles[:,TileAttr['col']].reshape(tile_dim,tile_dim)\n      mat_map = agent_obs.tiles[:,TileAttr['material_id']].reshape(tile_dim,tile_dim)\n      agent = agent_obs.agent\n      self.assertEqual(agent.row, row_map[center,center])\n      self.assertEqual(agent.col, col_map[center,center])\n      self.assertEqual(agent_obs.tile(0,0).material_id, mat_map[center,center])\n\n      # pylint: disable=not-an-iterable\n      for d in Action.Direction.edges:\n        self.assertTrue(np.array_equal(correct_tile(agent_obs, *d.delta),\n                                      agent_obs.tile(*d.delta)))\n\n    print('---test_correct_tile---')\n    print('reference:', timeit(lambda: correct_tile(agent_obs, *d.delta),\n                              number=1000, globals=globals()))\n    print('implemented:', timeit(lambda: agent_obs.tile(*d.delta),\n                                number=1000, globals=globals()))\n\n  def test_env_visible_tiles_correctness(self):\n    def correct_visible_tile(realm, agent_id):\n      # Based on numpy datatable window query\n      assert agent_id in realm.players, \"agent_id not in the realm\"\n      agent = realm.players[agent_id]\n      radius = realm.config.PLAYER_VISION_RADIUS\n      return TileState.Query.window(\n        realm.datastore, agent.row.val, agent.col.val, radius)\n\n    # implemented in the env._compute_observations()\n    def visible_tiles_by_index(realm, agent_id, tile_map):\n      assert agent_id in realm.players, \"agent_id not in the realm\"\n      agent = realm.players[agent_id]\n      radius = realm.config.PLAYER_VISION_RADIUS\n      return tile_map[agent.row.val-radius:agent.row.val+radius+1,\n                      agent.col.val-radius:agent.col.val+radius+1,:].reshape(225,3)\n\n    # get tile map, to bypass the expensive tile window query\n    tile_map = TileState.Query.get_map(self.env.realm.datastore, self.config.MAP_SIZE)\n\n    self.env._compute_observations()\n    obs = self.env.obs\n    for agent_id in self.env.realm.players:\n      self.assertTrue(np.array_equal(correct_visible_tile(self.env.realm, agent_id),\n                                     obs[agent_id].tiles))\n\n    print('---test_visible_tile_window---')\n    print('reference:', timeit(lambda: correct_visible_tile(self.env.realm, agent_id),\n                              number=1000, globals=globals()))\n    print('implemented:',\n          timeit(lambda: visible_tiles_by_index(self.env.realm, agent_id, tile_map),\n                 number=1000, globals=globals()))\n\n  def test_make_attack_mask_within_range(self):\n    def correct_within_range(entities, attack_range, agent_row, agent_col):\n      entities_pos = entities[:,[EntityAttr[\"row\"],EntityAttr[\"col\"]]]\n      within_range = utils.linf(entities_pos,(agent_row, agent_col)) <= attack_range\n      return within_range\n\n    # implemented in the Observation._make_attack_mask()\n    def simple_within_range(entities, attack_range, agent_row, agent_col):\n      return np.maximum(\n          np.abs(entities[:,EntityAttr[\"row\"]] - agent_row),\n          np.abs(entities[:,EntityAttr[\"col\"]] - agent_col)\n        ) <= attack_range\n\n    self.env._compute_observations()\n    obs = self.env.obs\n    attack_range = self.config.COMBAT_MELEE_REACH\n\n    for agent_obs in obs.values():\n      entities = agent_obs.entities.values\n      agent = agent_obs.agent\n      self.assertTrue(np.array_equal(\n        correct_within_range(entities, attack_range, agent.row, agent.col),\n        simple_within_range(entities, attack_range, agent.row, agent.col)))\n\n    print('---test_attack_within_range---')\n    print('reference:', timeit(\n      lambda: correct_within_range(entities, attack_range, agent.row, agent.col),\n      number=1000, globals=globals()))\n    print('implemented:', timeit(\n      lambda: simple_within_range(entities, attack_range, agent.row, agent.col),\n      number=1000, globals=globals()))\n\n  def test_gs_where_in_1d(self):\n    config = ScriptedAgentTestConfig()\n    env = nmmo.Env(config)\n    env.reset(seed=0)\n    for _ in range(5):\n      env.step({})\n\n    def correct_where_in_1d(event_data, subject):\n      flt_idx = np.in1d(event_data[:, EventAttr['ent_id']], subject)\n      return event_data[flt_idx]\n\n    def where_in_1d_with_index(event_data, subject, index):\n      flt_idx = [row for sbj in subject for row in index.get(sbj,[])]\n      return event_data[flt_idx]\n\n    event_data = EventState.Query.table(env.realm.datastore)\n    event_index = defaultdict()\n    for row, id_ in enumerate(event_data[:,EventAttr['ent_id']]):\n      if id_ in event_index:\n        event_index[id_].append(row)\n      else:\n        event_index[id_] = [row]\n\n    # NOTE: the index-based approach returns the data in different order,\n    #   and all the operations in the task system don't use the order info\n    def sort_event_data(event_data):\n      keys = [event_data[:,i] for i in range(1,8)]\n      sorted_idx = np.lexsort(keys)\n      return event_data[sorted_idx]\n    arr1 = sort_event_data(correct_where_in_1d(event_data, [1,2,3]))\n    arr2 = sort_event_data(where_in_1d_with_index(event_data, [1,2,3], event_index))\n    self.assertTrue(np.array_equal(arr1, arr2))\n\n    print('---test_gs_where_in_1d---')\n    print('reference:', timeit(\n      lambda: correct_where_in_1d(event_data, [1, 2, 3]),\n      number=1000, globals=globals()))\n    print('implemented:', timeit(\n      lambda: where_in_1d_with_index(event_data, [1, 2, 3], event_index),\n      number=1000, globals=globals()))\n\n\nif __name__ == '__main__':\n  unittest.main()\n\n  # from tests.testhelpers import profile_env_step\n  # profile_env_step()\n\n  # config = nmmo.config.Default()\n  # env = nmmo.Env(config)\n  # env.reset()\n  # for _ in range(10):\n  #   env.step({})\n\n  # obs = env._compute_observations()\n\n  # NOTE: the most of performance gain in _make_move_mask comes from the improved tile\n  # test_func = [\n  #   '_make_move_mask()', # 0.170 -> 0.012\n  #   '_make_attack_mask()', # 0.060 -> 0.037\n  #   '_make_use_mask()', # 0.0036 ->\n  #   '_make_sell_mask()',\n  #   '_make_give_target_mask()',\n  #   '_make_destroy_item_mask()',\n  #   '_make_buy_mask()', # 0.022 -> 0.011\n  #   '_make_give_gold_mask()',\n  #   '_existing_ammo_listings()',\n  #   'agent()',\n  #   'tile(1,-1)' # 0.020 (cache off) -> 0.012\n  # ]\n\n  # for func in test_func:\n  #   print(func, timeit(f'obs[1].{func}', number=1000, globals=globals()))\n"
  },
  {
    "path": "tests/core/test_tile_property.py",
    "content": "import unittest\n\nimport copy\nimport nmmo\nfrom scripted.baselines import Sleeper\n\nHORIZON = 32\n\n\nclass TestTileProperty(unittest.TestCase):\n\n  @classmethod\n  def setUpClass(cls):\n    cls.config = nmmo.config.Default()\n    cls.config.PLAYERS = [Sleeper]\n    env = nmmo.Env(cls.config)\n    env.reset()\n    cls.start = copy.deepcopy(env.realm)\n    for _ in range(HORIZON):\n      env.step({})\n    cls.end = copy.deepcopy(env.realm)\n\n  # Test immutable invariants assumed for certain optimizations\n  def test_fixed_habitability_passability(self):\n    # Used in optimization with habitability lookup table\n    start_habitable = [tile.habitable for tile in self.start.map.tiles.flatten()]\n    end_habitable = [tile.habitable for tile in self.end.map.tiles.flatten()]\n    self.assertListEqual(start_habitable, end_habitable)\n\n    # Used in optimization that caches the result of A*\n    start_passable = [tile.impassible for tile in self.start.map.tiles.flatten()]\n    end_passable = [tile.impassible for tile in self.end.map.tiles.flatten()]\n    self.assertListEqual(start_passable, end_passable)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/core/test_tile_seize.py",
    "content": "# pylint: disable=protected-access\nimport unittest\nimport numpy as np\n\nimport nmmo\nimport nmmo.core.map\nfrom nmmo.core.tile import Tile, TileState\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom nmmo.lib import material\n\nclass MockRealm:\n  def __init__(self):\n    self.datastore = NumpyDatastore()\n    self.datastore.register_object_type(\"Tile\", TileState.State.num_attributes)\n    self.config = nmmo.config.Small()\n    self._np_random = np.random\n    self.tick = 0\n    self.event_log = None\n\nclass MockTask:\n  def __init__(self, ent_id):\n    self.assignee = (ent_id,)\n\nclass MockEntity:\n  def __init__(self, ent_id):\n    self.ent_id = ent_id\n    self.my_task = None\n    if ent_id > 0:  # only for players\n      self.my_task = MockTask(ent_id)\n\nclass TestTileSeize(unittest.TestCase):\n  # pylint: disable=no-member\n  def test_tile(self):\n    mock_realm = MockRealm()\n    np_random = np.random\n    tile = Tile(mock_realm, 10, 20, np_random)\n\n    tile.reset(material.Foilage, nmmo.config.Small(), np_random)\n\n    self.assertEqual(tile.row.val, 10)\n    self.assertEqual(tile.col.val, 20)\n    self.assertEqual(tile.material_id.val, material.Foilage.index)\n    self.assertEqual(tile.seize_history, [])\n\n    mock_realm.tick = 1\n    tile.add_entity(MockEntity(1))\n    self.assertEqual(tile.occupied, True)\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (1, 1))\n\n    # Agent 1 stayed, so no change\n    mock_realm.tick = 2\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (1, 1))\n\n    # Two agents occupy the tile, so no change\n    mock_realm.tick = 3\n    tile.add_entity(MockEntity(2))\n    self.assertCountEqual(tile.entities.keys(), [1, 2])\n    self.assertEqual(tile.occupied, True)\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (1, 1))\n\n    mock_realm.tick = 5\n    tile.remove_entity(1)\n    self.assertCountEqual(tile.entities.keys(), [2])\n    self.assertEqual(tile.occupied, True)\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (2, 5))  # new seize history\n\n    # Two agents occupy the tile, so no change\n    mock_realm.tick = 7\n    tile.add_entity(MockEntity(-10))\n    self.assertListEqual(list(tile.entities.keys()), [2, -10])\n    self.assertEqual(tile.occupied, True)\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (2, 5))\n\n    # Should not change when occupied by an npc\n    mock_realm.tick = 9\n    tile.remove_entity(2)\n    self.assertListEqual(list(tile.entities.keys()), [-10])\n    self.assertEqual(tile.occupied, True)\n    tile.update_seize()\n    self.assertEqual(tile.seize_history[-1], (2, 5))\n\n    tile.harvest(True)\n    self.assertEqual(tile.depleted, True)\n    self.assertEqual(tile.material_id.val, material.Scrub.index)\n\n  def test_map_seize_targets(self):\n    mock_realm = MockRealm()\n    config = mock_realm.config\n    np_random = mock_realm._np_random\n    map_dict = {\"map\": np.ones((config.MAP_SIZE, config.MAP_SIZE))*2}  # all grass tiles\n    center_tile = (config.MAP_SIZE//2, config.MAP_SIZE//2)\n\n    test_map = nmmo.core.map.Map(config, mock_realm, np_random)\n    test_map.reset(map_dict, np_random, seize_targets=[\"center\"])\n    self.assertListEqual(test_map.seize_targets, [center_tile])\n    self.assertDictEqual(test_map.seize_status, {})\n\n    mock_realm.tick = 4\n    test_map.tiles[center_tile].add_entity(MockEntity(5))\n    test_map.step()\n    self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)})  # ent_id, tick\n\n    mock_realm.tick = 6\n    test_map.tiles[center_tile].remove_entity(5)\n    test_map.step()\n    self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)})  # should not change\n\n    mock_realm.tick = 9\n    test_map.tiles[center_tile].add_entity(MockEntity(6))\n    test_map.tiles[center_tile].add_entity(MockEntity(-7))\n    test_map.step()\n    self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)})  # should not change\n\n    mock_realm.tick = 11\n    test_map.tiles[center_tile].remove_entity(6)  # so that -7 is the only entity\n    test_map.step()\n    self.assertDictEqual(test_map.seize_status, {center_tile: (5, 4)})  # should not change\n\n    mock_realm.tick = 14\n    test_map.tiles[center_tile].remove_entity(-7)\n    test_map.tiles[center_tile].add_entity(MockEntity(10))\n    test_map.step()\n    self.assertDictEqual(test_map.seize_status, {center_tile: (10, 14)})\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/datastore/test_datastore.py",
    "content": "import unittest\n\nimport numpy as np\n\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\n\n\nclass TestDatastore(unittest.TestCase):\n\n  def testdatastore_record(self):\n    datastore = NumpyDatastore()\n    datastore.register_object_type(\"TestObject\", 2)\n    c1 = 0\n    c2 = 1\n\n    o = datastore.create_record(\"TestObject\")\n    self.assertEqual([o.get(c1), o.get(c2)], [0, 0])\n\n    o.update(c1, 1)\n    o.update(c2, 2)\n    self.assertEqual([o.get(c1), o.get(c2)], [1, 2])\n\n    np.testing.assert_array_equal(\n      datastore.table(\"TestObject\").get([o.id]),\n      np.array([[1, 2]]))\n\n    o2 = datastore.create_record(\"TestObject\")\n    o2.update(c2, 2)\n    np.testing.assert_array_equal(\n      datastore.table(\"TestObject\").get([o.id, o2.id]),\n      np.array([[1, 2], [0, 2]]))\n\n    np.testing.assert_array_equal(\n      datastore.table(\"TestObject\").where_eq(c2, 2),\n      np.array([[1, 2], [0, 2]]))\n\n    o.delete()\n    np.testing.assert_array_equal(\n      datastore.table(\"TestObject\").where_eq(c2, 2),\n      np.array([[0, 2]]))\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/datastore/test_id_allocator.py",
    "content": "import unittest\n\nfrom nmmo.datastore.id_allocator import IdAllocator\n\nclass TestIdAllocator(unittest.TestCase):\n  def test_id_allocator(self):\n    id_allocator = IdAllocator(10)\n\n    for i in range(1, 10):\n      row_id = id_allocator.allocate()\n      self.assertEqual(i, row_id)\n    self.assertTrue(id_allocator.full())\n\n    id_allocator.remove(5)\n    id_allocator.remove(6)\n    id_allocator.remove(1)\n    self.assertFalse(id_allocator.full())\n\n    self.assertSetEqual(\n      set(id_allocator.allocate() for i in range(3)),\n      set([5, 6, 1])\n    )\n    self.assertTrue(id_allocator.full())\n\n    id_allocator.expand(11)\n    self.assertFalse(id_allocator.full())\n\n    self.assertEqual(id_allocator.allocate(), 10)\n\n    with self.assertRaises(KeyError):\n      id_allocator.allocate()\n\n  def test_id_reuse(self):\n    id_allocator = IdAllocator(10)\n\n    for i in range(1, 10):\n      row_id = id_allocator.allocate()\n      self.assertEqual(i, row_id)\n    self.assertTrue(id_allocator.full())\n\n    id_allocator.remove(5)\n    id_allocator.remove(6)\n    id_allocator.remove(1)\n    self.assertFalse(id_allocator.full())\n\n    self.assertSetEqual(\n      set(id_allocator.allocate() for i in range(3)),\n      set([5, 6, 1])\n    )\n    self.assertTrue(id_allocator.full())\n\n    id_allocator.expand(11)\n    self.assertFalse(id_allocator.full())\n\n    self.assertEqual(id_allocator.allocate(), 10)\n\n    with self.assertRaises(KeyError):\n      id_allocator.allocate()\n\n    id_allocator.remove(10)\n    self.assertEqual(id_allocator.allocate(), 10)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/datastore/test_numpy_datastore.py",
    "content": "import unittest\n\nimport numpy as np\n\nfrom nmmo.datastore.numpy_datastore import NumpyTable\n\n# pylint: disable=protected-access\nclass TestNumpyTable(unittest.TestCase):\n  def test_continous_table(self):\n    table = NumpyTable(3, 10, np.float32)\n    table.update(2, 0, 2.1)\n    table.update(2, 1, 2.2)\n    table.update(5, 0, 5.1)\n    table.update(5, 2, 5.3)\n    np.testing.assert_array_equal(\n      table.get([1,2,5]),\n      np.array([[0, 0, 0], [2.1, 2.2, 0], [5.1, 0, 5.3]], dtype=np.float32)\n    )\n\n  def test_discrete_table(self):\n    table = NumpyTable(3, 10, np.int32)\n    table.update(2, 0, 11)\n    table.update(2, 1, 12)\n    table.update(5, 0, 51)\n    table.update(5, 2, 53)\n    np.testing.assert_array_equal(\n      table.get([1,2,5]),\n      np.array([[0, 0, 0], [11, 12, 0], [51, 0, 53]], dtype=np.int32)\n    )\n\n  def test_expand(self):\n    table = NumpyTable(3, 10, np.float32)\n\n    table.update(2, 0, 2.1)\n    with self.assertRaises(IndexError):\n      table.update(10, 0, 10.1)\n\n    table._expand(11)\n    table.update(10, 0, 10.1)\n\n    np.testing.assert_array_equal(\n      table.get([10, 2]),\n      np.array([[10.1, 0, 0], [2.1, 0, 0]], dtype=np.float32)\n    )\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/datastore/test_serialized.py",
    "content": "from collections import defaultdict\nimport unittest\n\nfrom nmmo.datastore.serialized import SerializedState\n\n# pylint: disable=no-member,unused-argument,unsubscriptable-object\n\nFooState = SerializedState.subclass(\"FooState\", [\n  \"a\", \"b\", \"col\"\n])\n\nFooState.Limits = {\n  \"a\": (-10, 10),\n}\n\nclass MockDatastoreRecord():\n  def __init__(self):\n    self._data = defaultdict(lambda: 0)\n\n  def get(self, name):\n    return self._data[name]\n\n  def update(self, name, value):\n    self._data[name] = value\n\nclass MockDatastore():\n  def create_record(self, name):\n    return MockDatastoreRecord()\n\n  def register_object_type(self, name, attributes):\n    assert name == \"FooState\"\n    assert attributes == [\"a\", \"b\", \"col\"]\n\nclass TestSerialized(unittest.TestCase):\n\n  def test_serialized(self):\n    state = FooState(MockDatastore(), FooState.Limits)\n\n    # initial value = 0\n    self.assertEqual(state.a.val, 0)\n\n    # if given value is within the range, set to the value\n    state.a.update(1)\n    self.assertEqual(state.a.val, 1)\n\n    # if given a lower value than the min, set to min\n    a_min, a_max = FooState.Limits[\"a\"]\n    state.a.update(a_min - 100)\n    self.assertEqual(state.a.val, a_min)\n\n    # if given a higher value than the max, set to max\n    state.a.update(a_max + 100)\n    self.assertEqual(state.a.val, a_max)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/render/test_load_replay.py",
    "content": "'''Manual test for rendering replay'''\n\nif __name__ == '__main__':\n  import time\n\n  # pylint: disable=import-error\n  from nmmo.render.render_client import DummyRenderer\n  from nmmo.render.replay_helper import FileReplayHelper\n\n  # open a client\n  renderer = DummyRenderer()\n  time.sleep(3)\n\n  # load a replay: replace 'replay_dev.json' with your replay file\n  replay = FileReplayHelper.load('replay_dev.json')\n\n  # run the replay\n  for packet in replay:\n    renderer.render_packet(packet)\n    time.sleep(1)\n"
  },
  {
    "path": "tests/render/test_render_save.py",
    "content": "# Deprecated test; old render system\n\n'''Manual test for render client connectivity and save replay\nimport nmmo\nfrom nmmo.core.config import (AllGameSystems, Combat, Communication,\n                              Equipment, Exchange, Item, Medium, Profession,\n                              Progression, Resource, Small, Terrain)\nfrom nmmo.render.render_client import DummyRenderer\nfrom nmmo.render.replay_helper import FileReplayHelper\nfrom scripted import baselines\n\ndef create_config(base, nent, *systems):\n  systems = (base, *systems)\n  name = '_'.join(cls.__name__ for cls in systems)\n  conf = type(name, systems, {})()\n  conf.TERRAIN_TRAIN_MAPS = 1\n  conf.TERRAIN_EVAL_MAPS  = 1\n  conf.IMMORTAL = True\n  conf.PLAYER_N = nent\n  conf.PLAYERS = [baselines.Random]\n  return conf\n\nno_npc_small_1_pop_conf = create_config(Small, 1, Terrain, Resource,\n  Combat, Progression, Item, Equipment, Profession, Exchange, Communication)\n\nno_npc_med_1_pop_conf = create_config(Medium, 1, Terrain, Resource,\n  Combat, Progression, Item, Equipment, Profession, Exchange, Communication)\n\nno_npc_med_100_pop_conf = create_config(Medium, 100, Terrain, Resource,\n  Combat, Progression, Item, Equipment, Profession, Exchange, Communication)\n\nall_small_1_pop_conf = create_config(Small, 1, AllGameSystems)\n\nall_med_1_pop_conf = create_config(Medium, 1, AllGameSystems)\n\nall_med_100_pop_conf = create_config(Medium, 100, AllGameSystems)\n\nconf_dict = {\n  'no_npc_small_1_pop': no_npc_small_1_pop_conf,\n  'no_npc_med_1_pop': no_npc_med_1_pop_conf,\n  'no_npc_med_100_pop': no_npc_med_100_pop_conf,\n  'all_small_1_pop': all_small_1_pop_conf,\n  'all_med_1_pop': all_med_1_pop_conf,\n  'all_med_100_pop': all_med_100_pop_conf\n}\n\nif __name__ == '__main__':\n  import random\n  from tqdm import tqdm\n\n  TEST_HORIZON = 100\n  RANDOM_SEED = random.randint(0, 9999)\n\n  replay_helper = FileReplayHelper()\n\n  # the renderer is external to the env, so need to manually initiate it\n  renderer = DummyRenderer()\n\n  for conf_name, config in conf_dict.items():\n    env = nmmo.Env(config)\n\n    # to make replay, one should create replay_helper\n    #   and run the below line\n    env.realm.record_replay(replay_helper)\n\n    env.reset(seed=RANDOM_SEED)\n    renderer.set_realm(env.realm)\n\n    for tick in tqdm(range(TEST_HORIZON)):\n      env.step({})\n      renderer.render_realm()\n\n    # NOTE: save the data in uncompressed json format, since\n    #   the web client has trouble loading the compressed replay file\n    replay_helper.save(f'replay_{conf_name}_seed_{RANDOM_SEED:04d}.json')\n\n'''\n"
  },
  {
    "path": "tests/systems/test_exchange.py",
    "content": "# pylint: disable=unnecessary-lambda,protected-access,no-member\nfrom types import SimpleNamespace\nimport unittest\nimport numpy as np\n\nimport nmmo\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom nmmo.systems.exchange import Exchange\nfrom nmmo.systems.item import ItemState\nfrom nmmo.systems import item\n\n\nclass MockRealm:\n  def __init__(self):\n    self.config = nmmo.config.Default()\n    self.config.EXCHANGE_LISTING_DURATION = 3\n    self.datastore = NumpyDatastore()\n    self.items = {}\n    self.datastore.register_object_type(\"Item\", ItemState.State.num_attributes)\n    self.tick = 0\n\nclass MockEntity:\n  def __init__(self) -> None:\n    self.items = []\n    self.inventory = SimpleNamespace(\n      receive = lambda item: self.items.append(item),\n      remove = lambda item: self.items.remove(item)\n     )\n\nclass TestExchange(unittest.TestCase):\n  def test_listings(self):\n    realm = MockRealm()\n    exchange = Exchange(realm)\n\n    entity_1 = MockEntity()\n\n    hat_1 = item.Hat(realm, 1)\n    hat_2 = item.Hat(realm, 10)\n    entity_1.inventory.receive(hat_1)\n    entity_1.inventory.receive(hat_2)\n    self.assertEqual(len(entity_1.items), 2)\n\n    tick = realm.tick = 0\n    exchange._list_item(hat_1, entity_1, 10, tick)\n    self.assertEqual(len(exchange._item_listings), 1)\n    self.assertEqual(exchange._listings_queue[0], (hat_1.id.val, 0))\n\n    tick = realm.tick = 1\n    exchange._list_item(hat_2, entity_1, 20, tick)\n    self.assertEqual(len(exchange._item_listings), 2)\n    self.assertEqual(exchange._listings_queue[0], (hat_1.id.val, 0))\n\n    tick = realm.tick = 4\n    exchange.step()\n    # hat_1 should expire and not be listed\n    self.assertEqual(len(exchange._item_listings), 1)\n    self.assertEqual(exchange._listings_queue[0], (hat_2.id.val, 1))\n\n    tick = realm.tick = 5\n    exchange._list_item(hat_2, entity_1, 10, tick)\n    exchange.step()\n    # hat_2 got re-listed, so should still be listed\n    self.assertEqual(len(exchange._item_listings), 1)\n    self.assertEqual(exchange._listings_queue[0], (hat_2.id.val, 5))\n\n    tick = realm.tick = 10\n    exchange.step()\n    self.assertEqual(len(exchange._item_listings), 0)\n\n  def test_for_sale_items(self):\n    realm = MockRealm()\n    exchange = Exchange(realm)\n    entity_1 = MockEntity()\n\n    hat_1 = item.Hat(realm, 1)\n    hat_2 = item.Hat(realm, 10)\n    exchange._list_item(hat_1, entity_1, 10, 0)\n    exchange._list_item(hat_2, entity_1, 20, 10)\n\n    np.testing.assert_array_equal(\n      item.Item.Query.for_sale(realm.datastore)[:,0], [hat_1.id.val, hat_2.id.val])\n\n    # first listing should expire\n    realm.tick = 10\n    exchange.step()\n    np.testing.assert_array_equal(\n      item.Item.Query.for_sale(realm.datastore)[:,0], [hat_2.id.val])\n\n    # second listing should expire\n    realm.tick = 100\n    exchange.step()\n    np.testing.assert_array_equal(\n      item.Item.Query.for_sale(realm.datastore)[:,0], [])\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/systems/test_item.py",
    "content": "import unittest\nimport numpy as np\n\nimport nmmo\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom nmmo.systems.item import Hat, Top, ItemState\nfrom nmmo.systems.exchange import Exchange\n\nclass MockRealm:\n  def __init__(self):\n    self.config = nmmo.config.Default()\n    self.datastore = NumpyDatastore()\n    self.items = {}\n    self.exchange = Exchange(self)\n    self.datastore.register_object_type(\"Item\", ItemState.State.num_attributes)\n    self.players = {}\n\n# pylint: disable=no-member\nclass TestItem(unittest.TestCase):\n  def test_item(self):\n    realm = MockRealm()\n\n    hat_1 = Hat(realm, 1)\n    self.assertTrue(ItemState.Query.by_id(realm.datastore, hat_1.id.val) is not None)\n    self.assertEqual(hat_1.type_id.val, Hat.ITEM_TYPE_ID)\n    self.assertEqual(hat_1.level.val, 1)\n    self.assertEqual(hat_1.mage_defense.val, realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE)\n\n    hat_2 = Hat(realm, 10)\n    self.assertTrue(ItemState.Query.by_id(realm.datastore, hat_2.id.val) is not None)\n    self.assertEqual(hat_2.level.val, 10)\n    self.assertEqual(hat_2.melee_defense.val,\n                     hat_2.level.val * realm.config.EQUIPMENT_ARMOR_LEVEL_DEFENSE)\n\n    self.assertDictEqual(realm.items, {hat_1.id.val: hat_1, hat_2.id.val: hat_2})\n\n    # also test destroy\n    ids = [hat_1.id.val, hat_2.id.val]\n    hat_1.destroy()\n    hat_2.destroy()\n    # after destroy(), the datastore entry is gone, but the class still exsits\n    # make sure that after destroy the owner_id is 0, at least\n    self.assertTrue(hat_1.owner_id.val == 0)\n    self.assertTrue(hat_2.owner_id.val == 0)\n    for item_id in ids:\n      self.assertTrue(len(ItemState.Query.by_id(realm.datastore, item_id)) == 0)\n    self.assertDictEqual(realm.items, {})\n\n    # create a new item with the hat_1's id, but it must still be void\n    new_top = Top(realm, 3)\n    new_top.id.update(ids[0]) # hat_1's id\n    new_top.owner_id.update(100)\n    # make sure that the hat_1 is not linked to the new_top\n    self.assertTrue(hat_1.owner_id.val == 0)\n\n  def test_owned_by(self):\n    realm = MockRealm()\n\n    hat_1 = Hat(realm, 1)\n    hat_2 = Hat(realm, 10)\n\n    hat_1.owner_id.update(1)\n    hat_2.owner_id.update(1)\n\n    np.testing.assert_array_equal(\n      ItemState.Query.owned_by(realm.datastore, 1)[:,0],\n      [hat_1.id.val, hat_2.id.val])\n\n    self.assertEqual(Hat.Query.owned_by(realm.datastore, 2).size, 0)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/systems/test_skill_level.py",
    "content": "import unittest\n\nimport numpy as np\n\nimport nmmo\nimport nmmo.systems.skill\nfrom tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv\n\n\nclass TestSkillLevel(unittest.TestCase):\n  @classmethod\n  def setUpClass(cls):\n    cls.config = ScriptedAgentTestConfig()\n    cls.config.set(\"PROGRESSION_EXP_THRESHOLD\", [0, 10, 20, 30, 40, 50])\n    cls.config.set(\"PROGRESSION_LEVEL_MAX\", len(cls.config.PROGRESSION_EXP_THRESHOLD))\n    cls.env = ScriptedAgentTestEnv(cls.config)\n\n  def test_experience_calculator(self):\n    exp_calculator = nmmo.systems.skill.ExperienceCalculator(self.config)\n\n    self.assertTrue(np.array_equal(self.config.PROGRESSION_EXP_THRESHOLD,\n                                   exp_calculator.exp_threshold))\n\n    for level in range(1, self.config.PROGRESSION_LEVEL_MAX + 1):\n      self.assertEqual(exp_calculator.level_at_exp(exp_calculator.exp_at_level(level)), level)\n\n    self.assertEqual(exp_calculator.exp_at_level(-1),  # invalid level\n                     min(self.config.PROGRESSION_EXP_THRESHOLD))\n    self.assertEqual(exp_calculator.exp_at_level(30),  # level above the max\n                     max(self.config.PROGRESSION_EXP_THRESHOLD))\n\n    self.assertEqual(exp_calculator.level_at_exp(0), 1)\n    self.assertEqual(exp_calculator.level_at_exp(5), 1)\n    self.assertEqual(exp_calculator.level_at_exp(45), 5)\n    self.assertEqual(exp_calculator.level_at_exp(50), 6)\n    self.assertEqual(exp_calculator.level_at_exp(100), 6)\n\n  def test_add_xp(self):\n    self.env.reset()\n    player = self.env.realm.players[1]\n\n    skill_list = [\"melee\", \"range\", \"mage\",\n                  \"fishing\", \"herbalism\", \"prospecting\", \"carving\", \"alchemy\"]\n\n    # check the initial levels and exp\n    for skill in skill_list:\n      self.assertEqual(getattr(player.skills, skill).level.val, 1)\n      self.assertEqual(getattr(player.skills, skill).exp.val, 0)\n\n    # add 1 exp to melee, does NOT level up\n    player.skills.melee.add_xp(1)\n    for skill in skill_list:\n      if skill == \"melee\":\n        self.assertEqual(getattr(player.skills, skill).level.val, 1)\n        self.assertEqual(getattr(player.skills, skill).exp.val, 1)\n      else:\n        self.assertEqual(getattr(player.skills, skill).level.val, 1)\n        self.assertEqual(getattr(player.skills, skill).exp.val, 0)\n\n    # add 30 exp to fishing, levels up to 3\n    player.skills.fishing.add_xp(30)\n    for skill in skill_list:\n      if skill == \"melee\":\n        self.assertEqual(getattr(player.skills, skill).level.val, 1)\n        self.assertEqual(getattr(player.skills, skill).exp.val, 1)\n      elif skill == \"fishing\":\n        self.assertEqual(getattr(player.skills, skill).level.val, 4)\n        self.assertEqual(getattr(player.skills, skill).exp.val, 30)\n      else:\n        self.assertEqual(getattr(player.skills, skill).level.val, 1)\n        self.assertEqual(getattr(player.skills, skill).exp.val, 0)\n\n\nif __name__ == '__main__':\n  unittest.main()\n\n  # config = nmmo.config.Default()\n  # exp_calculator = nmmo.systems.skill.ExperienceCalculator(config)\n\n  # print(exp_calculator.exp_threshold)\n  # print(exp_calculator.exp_at_level(10))\n  # print(exp_calculator.level_at_exp(150)) # 2\n  # print(exp_calculator.level_at_exp(300)) # 3\n  # print(exp_calculator.level_at_exp(1000)) # 7\n"
  },
  {
    "path": "tests/task/test_demo_task_creation.py",
    "content": "# pylint: disable=invalid-name,unused-argument,unused-variable\nimport unittest\nfrom tests.testhelpers import ScriptedAgentTestConfig\n\nfrom nmmo.core.env import Env\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.systems import skill\nfrom nmmo.task import predicate_api as p\nfrom nmmo.task import task_api as t\nfrom nmmo.task import task_spec as ts\nfrom nmmo.task import base_predicates as bp\nfrom nmmo.task.game_state import GameState\nfrom nmmo.task.group import Group\n\ndef rollout(env, tasks, steps=5):\n  env.reset(make_task_fn=lambda: tasks)\n  for _ in range(steps):\n    env.step({})\n  return env.step({})\n\nclass TestDemoTask(unittest.TestCase):\n\n  def test_baseline_tasks(self):\n    # Tasks from\n    # https://github.com/NeuralMMO/baselines/\n    # blob/4c1088d2bbe0f74a08dcf7d71b714cd30772557f/tasks.py\n    class Tier:\n      REWARD_SCALE = 15\n      EASY         = 4 / REWARD_SCALE\n      NORMAL       = 6 / REWARD_SCALE\n      HARD         = 11 / REWARD_SCALE\n\n    # Predicates defined below can be evaluated over one agent or several agents,\n    #   which are sepcified separately\n    # Reward multiplier is indendent from predicates and used by tasks.\n    #   The multipliers are just shown to indicate the difficulty level of predicates\n\n    # Usage of base predicates (see nmmo/task/base_predicates.py)\n    player_kills = [ # (predicate, kwargs, reward_multiplier)\n      (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 1}, Tier.EASY),\n      (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 2}, Tier.NORMAL),\n      (bp.CountEvent, {'event': 'PLAYER_KILL', 'N': 3}, Tier.HARD)]\n\n    exploration = [ # (predicate, reward_multiplier)\n      (bp.DistanceTraveled, {'dist': 16}, Tier.EASY),\n      (bp.DistanceTraveled, {'dist': 32}, Tier.NORMAL),\n      (bp.DistanceTraveled, {'dist': 64}, Tier.HARD)]\n\n    # Demonstrates custom predicate - return float/boolean\n    def EquipmentLevel(gs: GameState,\n                       subject: Group,\n                       number: int):\n      equipped = subject.item.equipped > 0\n      levels = subject.item.level[equipped]\n      return levels.sum() >= number\n\n    equipment = [ # (predicate, reward_multiplier)\n      (EquipmentLevel, {'number': 1}, Tier.EASY),\n      (EquipmentLevel, {'number': 5}, Tier.NORMAL),\n      (EquipmentLevel, {'number': 10}, Tier.HARD)]\n\n    def CombatSkill(gs, subject, lvl):\n      # OR on predicate functions: max over all progress\n      return max(bp.AttainSkill(gs, subject, skill.Melee, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Range, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Mage, lvl, 1))\n\n    combat = [ # (predicate, reward_multiplier)\n      (CombatSkill, {'lvl': 2}, Tier.EASY),\n      (CombatSkill, {'lvl': 3}, Tier.NORMAL),\n      (CombatSkill, {'lvl': 4}, Tier.HARD)]\n\n    def ForageSkill(gs, subject, lvl):\n      return max(bp.AttainSkill(gs, subject, skill.Fishing, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Herbalism, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Prospecting, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Carving, lvl, 1),\n                 bp.AttainSkill(gs, subject, skill.Alchemy, lvl, 1))\n\n    foraging = [ # (predicate, reward_multiplier)\n      (ForageSkill, {'lvl': 2}, Tier.EASY),\n      (ForageSkill, {'lvl': 3}, Tier.NORMAL),\n      (ForageSkill, {'lvl': 4}, Tier.HARD)]\n\n    # Test rollout\n    config = ScriptedAgentTestConfig()\n    config.set(\"ALLOW_MULTI_TASKS_PER_AGENT\", True)\n    env = Env(config)\n\n    # Creating and testing \"team\" tasks\n    # i.e., predicates are evalauated over all team members,\n    #   and all team members get the same reward from each task\n\n    # The team mapping can come from anywhere.\n    # The below is an arbitrary example and even doesn't include all agents\n    teams = {0: [1, 2, 3, 4], 1: [5, 6, 7, 8]}\n\n    # Making player_kills and exploration team tasks,\n    team_tasks = []\n    for pred_fn, kwargs, weight in player_kills + exploration:\n      pred_cls = p.make_predicate(pred_fn)\n      for team in teams.values():\n        team_tasks.append(\n          pred_cls(Group(team), **kwargs).create_task(reward_multiplier=weight))\n\n    # Run the environment with these tasks\n    #   check rewards and infos for the task info\n    obs, rewards, terminated, truncated, infos = rollout(env, team_tasks)\n\n    # Creating and testing the same task for all agents\n    # i.e, each agent gets evaluated and rewarded individually\n    same_tasks = []\n    for pred_fn, kwargs, weight in exploration + equipment + combat + foraging:\n      pred_cls = p.make_predicate(pred_fn)\n      for agent_id in env.possible_agents:\n        same_tasks.append(\n          pred_cls(Group([agent_id]), **kwargs).create_task(reward_multiplier=weight))\n\n    # Run the environment with these tasks\n    #   check rewards and infos for the task info\n    obs, rewards, terminated, truncated, infos = rollout(env, same_tasks)\n\n    # DONE\n\n  def test_player_kill_reward(self):\n    # pylint: disable=no-value-for-parameter\n    \"\"\" Design a predicate with a complex progress scheme\n    \"\"\"\n    config = ScriptedAgentTestConfig()\n    env = Env(config)\n\n    # PARTICIPANT WRITES\n    # ====================================\n    def KillPredicate(gs: GameState,\n                      subject: Group):\n      \"\"\"The progress, the max of which is 1, should\n           * increase small for each player kill\n           * increase big for the 1st and 3rd kills\n           * reach 1 with 10 kills\n      \"\"\"\n      num_kills = len(subject.event.PLAYER_KILL)\n      progress = num_kills * 0.06\n      if num_kills >= 1:\n        progress += .1\n      if num_kills >= 3:\n        progress += .3\n      return min(progress, 1.0)\n\n    # participants don't need to know about Predicate classes\n    kill_pred_cls = p.make_predicate(KillPredicate)\n    kill_tasks = [kill_pred_cls(Group(agent_id)).create_task()\n                  for agent_id in env.possible_agents]\n\n    # Test Reward\n    env.reset(make_task_fn=lambda: kill_tasks)\n    players = env.realm.players\n    code = EventCode.PLAYER_KILL\n    env.realm.event_log.record(code, players[1], target=players[3])\n    env.realm.event_log.record(code, players[2], target=players[4])\n    env.realm.event_log.record(code, players[2], target=players[5])\n    env.realm.event_log.record(EventCode.EAT_FOOD, players[2])\n\n    # Award given as designed\n    # Agent 1 kills 1 - reward .06 + .1\n    # Agent 2 kills 2 - reward .12 + .1\n    # Agent 3 kills 0 - reward 0\n    _, rewards, _, _, _ = env.step({})\n    self.assertEqual(rewards[1], 0.16)\n    self.assertEqual(rewards[2], 0.22)\n    self.assertEqual(rewards[3], 0)\n\n    # No reward when no changes\n    _, rewards, _, _, _ = env.step({})\n    self.assertEqual(rewards[1], 0)\n    self.assertEqual(rewards[2], 0)\n    self.assertEqual(rewards[3], 0)\n\n    # DONE\n\n  def test_predicate_math(self):\n    # pylint: disable=no-value-for-parameter\n    config = ScriptedAgentTestConfig()\n    env = Env(config)\n\n    # each predicate function returns float, so one can do math on them\n    def PredicateMath(gs, subject):\n      progress = 0.8 * bp.CountEvent(gs, subject, event='PLAYER_KILL', N=7) + \\\n                 1.1 * bp.TickGE(gs, subject, num_tick=3)\n      # NOTE: the resulting progress will be bounded from [0, 1] afterwards\n      return progress\n\n    # participants don't need to know about Predicate classes\n    pred_math_cls = p.make_predicate(PredicateMath)\n    task_for_agent_1 = pred_math_cls(Group(1)).create_task()\n\n    # Test Reward\n    env.reset(make_task_fn=lambda: [task_for_agent_1])\n    code = EventCode.PLAYER_KILL\n    players = env.realm.players\n    env.realm.event_log.record(code, players[1], target=players[2])\n    env.realm.event_log.record(code, players[1], target=players[3])\n\n    _, rewards, _, _, _ = env.step({})\n    self.assertAlmostEqual(rewards[1], 0.8*2/7 + 1.1*1/3)\n\n    for _ in range(2):\n      _, _, _, _, infos = env.step({})\n\n    # 0.8*2/7 + 1.1 > 1, but the progress is maxed at 1\n    self.assertEqual(infos[1]['task'][env.tasks[0].name]['progress'], 1.0)\n    self.assertTrue(env.tasks[0].completed) # because progress >= 1\n\n    # DONE\n\n  def test_task_spec_based_curriculum(self):\n    task_spec = [\n      ts.TaskSpec(eval_fn=bp.CountEvent, eval_fn_kwargs={'event': 'PLAYER_KILL', 'N': 1},\n                  reward_to='team'),\n      ts.TaskSpec(eval_fn=bp.CountEvent, eval_fn_kwargs={'event': 'PLAYER_KILL', 'N': 2},\n                  reward_to='agent'),\n      ts.TaskSpec(eval_fn=bp.AllDead, eval_fn_kwargs={'target': 'left_team'},\n                  reward_to='agent'),\n      ts.TaskSpec(eval_fn=bp.CanSeeAgent, eval_fn_kwargs={'target': 'right_team_leader'},\n                  task_cls=t.OngoingTask, reward_to='team'),\n    ]\n\n    # NOTE: len(teams) and len(task_spec) don't need to match\n    teams = {1:[1,2,3], 3:[4,5], 6:[6,7], 9:[8,9], 14:[10,11]}\n\n    config = ScriptedAgentTestConfig()\n    env = Env(config)\n\n    env.reset(make_task_fn=lambda: ts.make_task_from_spec(teams, task_spec))\n\n    self.assertEqual(len(env.tasks), 6) # 6 tasks were created\n    self.assertEqual(env.tasks[0].name, # team 0 task assigned to agents 1,2,3\n                     '(Task_eval_fn:(CountEvent_(1,2,3)_event:PLAYER_KILL_N:1)_assignee:(1,2,3))')\n    self.assertEqual(env.tasks[1].name, # team 1, agent task assigned to agent 4\n                     '(Task_eval_fn:(CountEvent_(4,)_event:PLAYER_KILL_N:2)_assignee:(4,))')\n    self.assertEqual(env.tasks[2].name, # team 1, agent task assigned to agent 5\n                     '(Task_eval_fn:(CountEvent_(5,)_event:PLAYER_KILL_N:2)_assignee:(5,))')\n    self.assertEqual(env.tasks[3].name, # team 2, agent 6 task, left_team is team 3 (agents 8,9)\n                     '(Task_eval_fn:(AllDead_(8,9))_assignee:(6,))')\n    self.assertEqual(env.tasks[5].name, # team 3 task, right_team is team 2 (6,7), leader 6\n                     '(OngoingTask_eval_fn:(CanSeeAgent_(8,9)_target:6)_assignee:(8,9))')\n\n    for _ in range(2):\n      env.step({})\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/task/test_manual_curriculum.py",
    "content": "'''Manual test for creating learning curriculum manually'''\n# pylint: disable=invalid-name,redefined-outer-name,bad-builtin\n# pylint: disable=wildcard-import,unused-wildcard-import\nfrom typing import List\n\nimport nmmo.lib.material as m\nimport nmmo.systems.item as i\nimport nmmo.systems.skill as s\nfrom nmmo.task.base_predicates import *\nfrom nmmo.task.task_api import OngoingTask\nfrom nmmo.task.task_spec import TaskSpec, check_task_spec\n\nEVENT_NUMBER_GOAL = [3, 4, 5, 7, 9, 12, 15, 20, 30, 50]\nINFREQUENT_GOAL = list(range(1, 10))\nSTAY_ALIVE_GOAL = [50, 100, 150, 200, 300, 500]\nTEAM_NUMBER_GOAL = [10, 20, 30, 50, 70, 100]\nLEVEL_GOAL = list(range(1, 10)) # TODO: get config\nAGENT_NUM_GOAL = [1, 2, 3, 4, 5] # competition team size: 8\nITEM_NUM_GOAL = AGENT_NUM_GOAL\nTEAM_ITEM_GOAL = [1, 3, 5, 7, 10, 15, 20]\nSKILLS = s.COMBAT_SKILL + s.HARVEST_SKILL\nCOMBAT_STYLE = s.COMBAT_SKILL\nALL_ITEM = i.ALL_ITEM\nEQUIP_ITEM = i.ARMOR + i.WEAPON + i.TOOL + i.AMMUNITION\nHARVEST_ITEM = i.WEAPON + i.AMMUNITION + i.CONSUMABLE\n\ntask_spec: List[TaskSpec] = []\n\n# explore, eat, drink, attack any agent, harvest any item, level up any skill\n#   which can happen frequently\nessential_skills = ['GO_FARTHEST', 'EAT_FOOD', 'DRINK_WATER',\n                    'SCORE_HIT', 'HARVEST_ITEM', 'LEVEL_UP']\nfor event_code in essential_skills:\n  for cnt in EVENT_NUMBER_GOAL:\n    task_spec.append(TaskSpec(eval_fn=CountEvent,\n                              eval_fn_kwargs={'event': event_code, 'N': cnt},\n                              sampling_weight=30))\n\n# item/market skills, which happen less frequently or should not do too much\nitem_skills = ['CONSUME_ITEM', 'GIVE_ITEM', 'DESTROY_ITEM', 'EQUIP_ITEM',\n               'GIVE_GOLD', 'LIST_ITEM', 'EARN_GOLD', 'BUY_ITEM']\nfor event_code in item_skills:\n  task_spec += [TaskSpec(eval_fn=CountEvent, eval_fn_kwargs={'event': event_code, 'N': cnt})\n                for cnt in INFREQUENT_GOAL] # less than 10\n\n# find resource tiles\nfor resource in m.Harvestable:\n  for reward_to in ['agent', 'team']:\n    task_spec.append(TaskSpec(eval_fn=CanSeeTile, eval_fn_kwargs={'tile_type': resource},\n                              reward_to=reward_to, sampling_weight=10))\n\n# stay alive ... like ... for 300 ticks\n# i.e., getting incremental reward for each tick alive as an individual or a team\nfor reward_to in ['agent', 'team']:\n  for num_tick in STAY_ALIVE_GOAL:\n    task_spec.append(TaskSpec(eval_fn=TickGE, eval_fn_kwargs={'num_tick': num_tick},\n                              reward_to=reward_to))\n\n# protect the leader: get reward for each tick the leader is alive\n# NOTE: a tuple of length four, to pass in the task_kwargs\ntask_spec.append(TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={'target': 'my_team_leader'},\n                          reward_to='team', task_cls=OngoingTask))\n\n# want the other team or team leader to die\nfor target in ['left_team', 'left_team_leader', 'right_team', 'right_team_leader']:\n  task_spec.append(TaskSpec(eval_fn=AllDead, eval_fn_kwargs={'target': target},\n                            reward_to='team'))\n\n# occupy the center tile, assuming the Medium map size\n# TODO: it'd be better to have some intermediate targets toward the center\nfor reward_to in ['agent', 'team']:\n  task_spec.append(TaskSpec(eval_fn=OccupyTile, eval_fn_kwargs={'row': 80, 'col': 80},\n                            reward_to=reward_to)) # TODO: get config for map size\n\n# form a tight formation, for a certain number of ticks\ndef PracticeFormation(gs, subject, dist, num_tick):\n  return AllMembersWithinRange(gs, subject, dist) * TickGE(gs, subject, num_tick)\nfor dist in [1, 3, 5, 10]:\n  task_spec += [TaskSpec(eval_fn=PracticeFormation,\n                         eval_fn_kwargs={'dist': dist, 'num_tick': num_tick},\n                         reward_to='team') for num_tick in STAY_ALIVE_GOAL]\n\n# find the other team leader\nfor reward_to in ['agent', 'team']:\n  for target in ['left_team_leader', 'right_team_leader']:\n    task_spec.append(TaskSpec(eval_fn=CanSeeAgent, eval_fn_kwargs={'target': target},\n                              reward_to=reward_to))\n\n# find the other team (any agent)\nfor reward_to in ['agent']: #, 'team']:\n  for target in ['left_team', 'right_team']:\n    task_spec.append(TaskSpec(eval_fn=CanSeeGroup, eval_fn_kwargs={'target': target},\n                              reward_to=reward_to))\n\n# explore the map -- sum the l-inf distance traveled by all subjects\nfor dist in [10, 20, 30, 50, 100]: # each agent\n  task_spec.append(TaskSpec(eval_fn=DistanceTraveled, eval_fn_kwargs={'dist': dist}))\nfor dist in [30, 50, 70, 100, 150, 200, 300, 500]: # summed over all team members\n  task_spec.append(TaskSpec(eval_fn=DistanceTraveled, eval_fn_kwargs={'dist': dist},\n                            reward_to='team'))\n\n# level up a skill\nfor skill in SKILLS:\n  for level in LEVEL_GOAL[1:]:\n    # since this is an agent task, num_agent must be 1\n    task_spec.append(TaskSpec(eval_fn=AttainSkill,\n                              eval_fn_kwargs={'skill': skill, 'level': level, 'num_agent': 1},\n                              reward_to='agent',\n                              sampling_weight=10*(5-level) if level < 5 else 1))\n\n# make attain skill a team task by varying the number of agents\nfor skill in SKILLS:\n  for level in LEVEL_GOAL[1:]:\n    for num_agent in AGENT_NUM_GOAL:\n      if level + num_agent <= 6 or num_agent == 1: # heuristic prune\n        task_spec.append(\n          TaskSpec(eval_fn=AttainSkill,\n                   eval_fn_kwargs={'skill': skill, 'level': level, 'num_agent': num_agent},\n                   reward_to='team'))\n\n# practice specific combat style\nfor style in COMBAT_STYLE:\n  for cnt in EVENT_NUMBER_GOAL:\n    task_spec.append(TaskSpec(eval_fn=ScoreHit, eval_fn_kwargs={'combat_style': style, 'N': cnt},\n                              sampling_weight=5))\n  for cnt in TEAM_NUMBER_GOAL:\n    task_spec.append(TaskSpec(eval_fn=ScoreHit, eval_fn_kwargs={'combat_style': style, 'N': cnt},\n                              reward_to='team'))\n\n# defeat agents of a certain level as a team\nfor agent_type in ['player', 'npc']: # c.AGENT_TYPE_CONSTRAINT\n  for level in LEVEL_GOAL:\n    for num_agent in AGENT_NUM_GOAL:\n      if level + num_agent <= 6 or num_agent == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=DefeatEntity,\n                                  eval_fn_kwargs={'agent_type': agent_type, 'level': level,\n                                                  'num_agent': num_agent},\n                                  reward_to='team'))\n\n# hoarding gold -- evaluated on the current gold\nfor amount in EVENT_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=HoardGold, eval_fn_kwargs={'amount': amount},\n                            sampling_weight=3))\nfor amount in TEAM_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=HoardGold, eval_fn_kwargs={'amount': amount},\n                            reward_to='team'))\n\n# earning gold -- evaluated on the total gold earned by selling items\n# does NOT include looted gold\nfor amount in EVENT_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=EarnGold, eval_fn_kwargs={'amount': amount},\n                            sampling_weight=3))\nfor amount in TEAM_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=EarnGold, eval_fn_kwargs={'amount': amount},\n                            reward_to='team'))\n\n# spending gold, by buying items\nfor amount in EVENT_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=SpendGold, eval_fn_kwargs={'amount': amount},\n                            sampling_weight=3))\nfor amount in TEAM_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=SpendGold, eval_fn_kwargs={'amount': amount},\n                            reward_to='team'))\n\n# making profits by trading -- only buying and selling are counted\nfor amount in EVENT_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=MakeProfit, eval_fn_kwargs={'amount': amount},\n                            sampling_weight=3))\nfor amount in TEAM_NUMBER_GOAL:\n  task_spec.append(TaskSpec(eval_fn=MakeProfit, eval_fn_kwargs={'amount': amount},\n                            reward_to='team'))\n\n# managing inventory space\ndef PracticeInventoryManagement(gs, subject, space, num_tick):\n  return InventorySpaceGE(gs, subject, space) * TickGE(gs, subject, num_tick)\nfor space in [2, 4, 8]:\n  task_spec += [TaskSpec(eval_fn=PracticeInventoryManagement,\n                         eval_fn_kwargs={'space': space, 'num_tick': num_tick})\n                for num_tick in STAY_ALIVE_GOAL]\n\n# own item, evaluated on the current inventory\nfor item in ALL_ITEM:\n  for level in LEVEL_GOAL:\n    # agent task\n    for quantity in ITEM_NUM_GOAL:\n      if level + quantity <= 6 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=OwnItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for quantity in TEAM_ITEM_GOAL:\n      if level + quantity <= 10 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=OwnItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  reward_to='team'))\n\n# equip item, evaluated on the current inventory and equipment status\nfor item in EQUIP_ITEM:\n  for level in LEVEL_GOAL:\n    # agent task\n    task_spec.append(TaskSpec(eval_fn=EquipItem,\n                              eval_fn_kwargs={'item': item, 'level': level, 'num_agent': 1},\n                              sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for num_agent in AGENT_NUM_GOAL:\n      if level + num_agent <= 6 or num_agent == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=EquipItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'num_agent': num_agent},\n                                  reward_to='team'))\n\n# consume items (ration, potion), evaluated based on the event log\nfor item in i.CONSUMABLE:\n  for level in LEVEL_GOAL:\n    # agent task\n    for quantity in ITEM_NUM_GOAL:\n      if level + quantity <= 6 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=ConsumeItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for quantity in TEAM_ITEM_GOAL:\n      if level + quantity <= 10 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=ConsumeItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  reward_to='team'))\n\n# harvest items, evaluated based on the event log\nfor item in HARVEST_ITEM:\n  for level in LEVEL_GOAL:\n    # agent task\n    for quantity in ITEM_NUM_GOAL:\n      if level + quantity <= 6 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=HarvestItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for quantity in TEAM_ITEM_GOAL:\n      if level + quantity <= 10 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=HarvestItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  reward_to='team'))\n\n# list items, evaluated based on the event log\nfor item in ALL_ITEM:\n  for level in LEVEL_GOAL:\n    # agent task\n    for quantity in ITEM_NUM_GOAL:\n      if level + quantity <= 6 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=ListItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for quantity in TEAM_ITEM_GOAL:\n      if level + quantity <= 10 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=ListItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  reward_to='team'))\n\n# buy items, evaluated based on the event log\nfor item in ALL_ITEM:\n  for level in LEVEL_GOAL:\n    # agent task\n    for quantity in ITEM_NUM_GOAL:\n      if level + quantity <= 6 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=BuyItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  sampling_weight=4-level if level < 4 else 1))\n    # team task\n    for quantity in TEAM_ITEM_GOAL:\n      if level + quantity <= 10 or quantity == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=BuyItem,\n                                  eval_fn_kwargs={'item': item, 'level': level,\n                                                  'quantity': quantity},\n                                  reward_to='team'))\n\n# fully armed, evaluated based on the current player/inventory status\nfor style in COMBAT_STYLE:\n  for level in LEVEL_GOAL:\n    for num_agent in AGENT_NUM_GOAL:\n      if level + num_agent <= 6 or num_agent == 1: # heuristic prune\n        task_spec.append(TaskSpec(eval_fn=FullyArmed,\n                                  eval_fn_kwargs={'combat_style': style, 'level': level,\n                                                  'num_agent': num_agent},\n                                  reward_to='team'))\n\n\nif __name__ == '__main__':\n  import psutil\n  from contextlib import contextmanager\n  import multiprocessing as mp\n  import numpy as np\n  import dill\n\n  @contextmanager\n  def create_pool(num_proc):\n    pool = mp.Pool(processes=num_proc)\n    yield pool\n    pool.close()\n    pool.join()\n\n  # 3495 task specs: divide the specs into chunks\n  num_workers = round(psutil.cpu_count(logical=False)*0.7)\n  spec_chunks = np.array_split(task_spec, num_workers)\n  with create_pool(num_workers) as pool:\n    chunk_results = pool.map(check_task_spec, spec_chunks)\n\n  num_error = 0\n  for results in chunk_results:\n    for result in results:\n      if result[\"runnable\"] is False:\n        print(\"ERROR: \", result[\"spec_name\"])\n        num_error += 1\n  print(\"Total number of errors: \", num_error)\n\n  # test if the task spec is pickalable\n  with open('sample_curriculum.pkl', 'wb') as f:\n    dill.dump(task_spec, f, recurse=True)\n"
  },
  {
    "path": "tests/task/test_predicates.py",
    "content": "import unittest\nfrom typing import List, Tuple, Union, Iterable\nimport random\n\nfrom tests.testhelpers import ScriptedAgentTestConfig, provide_item\nfrom tests.testhelpers import change_spawn_pos as change_agent_pos\n\nfrom scripted.baselines import Sleeper\n\nfrom nmmo.entity.entity import EntityState\nfrom nmmo.systems import item as Item\nfrom nmmo.systems import skill as Skill\nfrom nmmo.lib import material as Material\nfrom nmmo.lib.event_code import EventCode\n\n# pylint: disable=import-error\nfrom nmmo.core.env import Env\nfrom nmmo.task.predicate_api import Predicate, make_predicate\nfrom nmmo.task.task_api import OngoingTask\nfrom nmmo.task.group import Group\nimport nmmo.task.base_predicates as bp\n\n# use the constant reward of 1 for testing predicates\nNUM_AGENT = 6\nALL_AGENT = list(range(1, NUM_AGENT+1))\n\n\nclass TestBasePredicate(unittest.TestCase):\n  # pylint: disable=protected-access,no-member,invalid-name\n\n  def _get_taskenv(self,\n                   test_preds: List[Tuple[Predicate, Union[Iterable[int], int]]],\n                   grass_map=False):\n\n    config = ScriptedAgentTestConfig()\n    config.set(\"PLAYERS\", [Sleeper])\n    config.set(\"PLAYER_N\", NUM_AGENT)\n    config.set(\"IMMORTAL\", True)\n    config.set(\"ALLOW_MULTI_TASKS_PER_AGENT\", True)\n\n    # OngoingTask keeps evaluating and returns progress as the reward\n    #   vs. Task stops evaluating once the task is completed, returns reward = delta(progress)\n    test_tasks = [OngoingTask(pred, assignee) for pred, assignee in test_preds]\n\n    env = Env(config)\n    env.reset(make_task_fn=lambda: test_tasks)\n\n    if grass_map:\n      MS = env.config.MAP_SIZE\n      # Change entire map to grass to become habitable\n      for i in range(MS):\n        for j in range(MS):\n          tile = env.realm.map.tiles[i,j]\n          tile.material = Material.Grass\n          tile.material_id.update(Material.Grass.index)\n          tile.state = Material.Grass(env.config)\n\n    return env\n\n  def _check_result(self, env, test_preds, infos, true_task):\n    for tid, (predicate, assignee) in enumerate(test_preds):\n      # result is cached when at least one assignee is alive so that the task is evaled\n      if len(set(assignee) & set(infos)) > 0:\n        self.assertEqual(int(env.game_state.cache_result[predicate.name]),\n                         int(tid in true_task))\n\n      for ent_id in infos:\n        if ent_id in assignee:\n          # the agents that are assigned the task get evaluated for reward\n          self.assertEqual(int(infos[ent_id]['task'][env.tasks[tid].name]['reward']),\n                           int(tid in true_task))\n        else:\n          # the agents that are not assigned the task are not evaluated\n          self.assertTrue(env.tasks[tid].name not in infos[ent_id]['task'])\n\n  def _check_progress(self, task, infos, value):\n    \"\"\" Tasks return a float in the range 0-1 indicating completion progress.\n    \"\"\"\n    for ent_id in infos:\n      if ent_id in task.assignee:\n        self.assertAlmostEqual(infos[ent_id]['task'][task.name]['progress'],value)\n\n  def test_tickge_stay_alive_rip(self):\n    tickge_pred_cls = make_predicate(bp.TickGE)\n    stay_alive_pred_cls = make_predicate(bp.StayAlive)\n    all_dead_pred_cls = make_predicate(bp.AllDead)\n\n    tick_true = 5\n    death_note = [1, 2, 3]\n    test_preds = [ # (instantiated predicate, task assignee)\n      (tickge_pred_cls(Group([1]), tick_true), ALL_AGENT),\n      (stay_alive_pred_cls(Group([1,3])), ALL_AGENT),\n      (stay_alive_pred_cls(Group([3,4])), [1,2]),\n      (stay_alive_pred_cls(Group([4])), [5,6]),\n      (all_dead_pred_cls(Group([1,3])), ALL_AGENT),\n      (all_dead_pred_cls(Group([3,4])), [1,2]),\n      (all_dead_pred_cls(Group([4])), [5,6])]\n\n    env = self._get_taskenv(test_preds)\n\n    for _ in range(tick_true-1):\n      _, _, _, _, infos = env.step({})\n\n    # TickGE_5 is false. All agents are alive,\n    # so all StayAlive (ti in [1,2,3]) tasks are true\n    # and all AllDead tasks (ti in [4, 5, 6]) are false\n\n    true_task = [1, 2, 3]\n    self._check_result(env, test_preds, infos, true_task)\n    self._check_progress(env.tasks[0], infos, (tick_true-1) / tick_true)\n\n    # kill agents 1-3\n    for ent_id in death_note:\n      env.realm.players[ent_id].resources.health.update(0)\n\n    # 6th tick\n    _, _, _, _, infos = env.step({})\n\n    # those who have survived\n    entities = EntityState.Query.table(env.realm.datastore)\n    entities = list(entities[:, EntityState.State.attr_name_to_col['id']]) # ent_ids\n\n    # make sure the dead agents are not in the realm & datastore\n    for ent_id in env.realm.players:\n      if ent_id in death_note:\n        # make sure that dead players not in the realm nor the datastore\n        self.assertTrue(ent_id not in env.realm.players)\n        self.assertTrue(ent_id not in entities)\n\n    # TickGE_5 is true. Agents 1-3 are dead, so\n    # StayAlive(1,3) and StayAlive(3,4) are false, StayAlive(4) is true\n    # AllDead(1,3) is true, AllDead(3,4) and AllDead(4) are false\n    true_task = [0, 3, 4]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # 3 is dead but 4 is alive. Half of agents killed, 50% completion.\n    self._check_progress(env.tasks[5], infos, 0.5)\n\n    # DONE\n\n  def test_can_see_tile(self):\n    canseetile_pred_cls = make_predicate(bp.CanSeeTile)\n\n    a1_target = Material.Foilage\n    a2_target = Material.Water\n    test_preds = [ # (instantiated predicate, task assignee)\n      (canseetile_pred_cls(Group([1]), a1_target), ALL_AGENT), # True\n      (canseetile_pred_cls(Group([1,3,5]), a2_target), ALL_AGENT), # False\n      (canseetile_pred_cls(Group([2]), a2_target), [1,2,3]), # True\n      (canseetile_pred_cls(Group([2,5,6]), a1_target), ALL_AGENT), # False\n      (canseetile_pred_cls(Group(ALL_AGENT), a2_target), [2,3,4])] # True\n\n    # setup env with all grass map\n    env = self._get_taskenv(test_preds, grass_map=True)\n\n    # Two corners to the target materials\n    BORDER = env.config.MAP_BORDER\n    MS = env.config.MAP_CENTER + BORDER\n    tile = env.realm.map.tiles[BORDER,MS-2]\n    tile.material = Material.Foilage\n    tile.material_id.update(Material.Foilage.index)\n\n    tile = env.realm.map.tiles[MS-1,BORDER]\n    tile.material = Material.Water\n    tile.material_id.update(Material.Water.index)\n\n    # All agents to one corner\n    for ent_id in env.realm.players:\n      change_agent_pos(env.realm,ent_id,(BORDER,BORDER))\n    _, _, _, _, infos = env.step({})\n    # no target tiles are found, so all are false\n    true_task = []\n    self._check_result(env, test_preds, infos, true_task)\n\n    # Team one to foilage, team two to water\n    change_agent_pos(env.realm,1,(BORDER,MS-2)) # agent 1, team 0, foilage\n    change_agent_pos(env.realm,2,(MS-2,BORDER)) # agent 2, team 1, water\n    _, _, _, _, infos = env.step({})\n    # t0, t2, t4 are true\n    true_task = [0, 2, 4]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_can_see_agent(self):\n    cansee_agent_pred_cls = make_predicate(bp.CanSeeAgent)\n    cansee_group_pred_cls = make_predicate(bp.CanSeeGroup)\n\n    search_target = 1\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (cansee_agent_pred_cls(Group([1]), search_target), ALL_AGENT), # Always True\n      (cansee_agent_pred_cls(Group([2]), search_target), [2,3,4]), # False -> True -> True\n      (cansee_agent_pred_cls(Group([3,4,5]), search_target), [1,2,3]), # False -> False -> True\n      (cansee_group_pred_cls(Group([1]), [3,4]), ALL_AGENT)] # False -> False -> True\n\n    env = self._get_taskenv(test_preds, grass_map=True)\n\n    # All agents to one corner\n    BORDER = env.config.MAP_BORDER\n    MS = env.config.MAP_CENTER + BORDER\n    for ent_id in env.realm.players:\n      change_agent_pos(env.realm,ent_id,(BORDER,BORDER)) # the map border\n\n    # Teleport agent 1 to the opposite corner\n    change_agent_pos(env.realm,1,(MS-2,MS-2))\n    _, _, _, _, infos = env.step({})\n    # Only CanSeeAgent(Group([1]), search_target) is true, others are false\n    true_task = [0]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # Teleport agent 2 to agent 1's pos\n    change_agent_pos(env.realm,2,(MS-2,MS-2))\n    _, _, _, _, infos = env.step({})\n    # SearchAgent(Team([2]), search_target) is also true\n    true_task = [0,1]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # Teleport agent 3 to agent 1s position\n    change_agent_pos(env.realm,3,(MS-2,MS-2))\n    _, _, _, _, infos = env.step({})\n    true_task = [0,1,2,3]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_occupy_tile(self):\n    occupy_tile_pred_cls = make_predicate(bp.OccupyTile)\n\n    target_tile = (30, 30)\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (occupy_tile_pred_cls(Group([1]), *target_tile), ALL_AGENT), # False -> True\n      (occupy_tile_pred_cls(Group([1,2,3]), *target_tile), [4,5,6]), # False -> True\n      (occupy_tile_pred_cls(Group([2]), *target_tile), [2,3,4]), # False\n      (occupy_tile_pred_cls(Group([3,4,5]), *target_tile), [1,2,3])] # False\n\n    # make all tiles habitable\n    env = self._get_taskenv(test_preds, grass_map=True)\n\n    # All agents to one corner\n    BORDER = env.config.MAP_BORDER\n    for ent_id in env.realm.players:\n      change_agent_pos(env.realm,ent_id,(BORDER,BORDER))\n    _, _, _, _, infos = env.step({})\n    # all tasks must be false\n    true_task = []\n    self._check_result(env, test_preds, infos, true_task)\n\n    # teleport agent 1 to the target tile, agent 2 to the adjacent tile\n    change_agent_pos(env.realm,1,target_tile)\n    change_agent_pos(env.realm,2,(target_tile[0],target_tile[1]-1))\n    _, _, _, _, infos = env.step({})\n    # tid 0 and 1 should be true: OccupyTile(Group([1]), *target_tile)\n    #  & OccupyTile(Group([1,2,3]), *target_tile)\n    true_task = [0, 1]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_distance_traveled(self):\n    distance_traveled_pred_cls = make_predicate(bp.DistanceTraveled)\n\n    agent_dist = 6\n    team_dist = 10\n    # NOTE: when evaluating predicates, to whom tasks are assigned are irrelevant\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (distance_traveled_pred_cls(Group([1]), agent_dist), ALL_AGENT), # False -> True\n      (distance_traveled_pred_cls(Group([2, 5]), agent_dist), ALL_AGENT), # False\n      (distance_traveled_pred_cls(Group([3, 4]), agent_dist), ALL_AGENT), # False\n      (distance_traveled_pred_cls(Group([1, 2, 3]), team_dist), ALL_AGENT), # False -> True\n      (distance_traveled_pred_cls(Group([6]), agent_dist), ALL_AGENT)] # False\n\n    # make all tiles habitable\n    env = self._get_taskenv(test_preds, grass_map=True)\n    _, _, _, _, infos = env.step({})\n    # one cannot accomplish these goals in the first tick, so all false\n    true_task = []\n    self._check_result(env, test_preds, infos, true_task)\n\n    # all are sleeper, so they all stay in the spawn pos\n    spawn_pos = { ent_id: ent.pos for ent_id, ent in env.realm.players.items() }\n    ent_id = 1 # move 6 tiles, to reach the goal\n    change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0]+6, spawn_pos[ent_id][1]))\n    ent_id = 2 # move 2, fail to reach agent_dist, but reach team_dist if add all\n    change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0]+2, spawn_pos[ent_id][1]))\n    ent_id = 3 # move 3, fail to reach agent_dist, but reach team_dist if add all\n    change_agent_pos(env.realm, ent_id, (spawn_pos[ent_id][0], spawn_pos[ent_id][1]+3))\n    _, _, _, _, infos = env.step({})\n    true_task = [0, 3]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_all_members_within_range(self):\n    within_range_pred_cls = make_predicate(bp.AllMembersWithinRange)\n\n    dist_123 = 1\n    dist_135 = 5\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (within_range_pred_cls(Group([1]), dist_123), ALL_AGENT), # Always true for group of 1\n      (within_range_pred_cls(Group([1,2]), dist_123), ALL_AGENT), # True\n      (within_range_pred_cls(Group([1,3]), dist_123), ALL_AGENT), # True\n      (within_range_pred_cls(Group([2,3]), dist_123), ALL_AGENT), # False\n      (within_range_pred_cls(Group([1,3,5]), dist_123), ALL_AGENT), # False\n      (within_range_pred_cls(Group([1,3,5]), dist_135), ALL_AGENT), # True\n      (within_range_pred_cls(Group([2,4,6]), dist_135), ALL_AGENT)] # False\n\n    # make all tiles habitable\n    env = self._get_taskenv(test_preds, grass_map=True)\n\n    MS = env.config.MAP_SIZE\n\n    # team 0: staying within goal_dist\n    change_agent_pos(env.realm, 1, (MS//2, MS//2))\n    change_agent_pos(env.realm, 3, (MS//2-1, MS//2)) # also StayCloseTo a1 = True\n    change_agent_pos(env.realm, 5, (MS//2-5, MS//2))\n\n    # team 1: staying goal_dist+1 apart\n    change_agent_pos(env.realm, 2, (MS//2+1, MS//2)) # also StayCloseTo a1 = True\n    change_agent_pos(env.realm, 4, (MS//2+5, MS//2))\n    change_agent_pos(env.realm, 6, (MS//2+8, MS//2))\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [0, 1, 2, 5]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_attain_skill(self):\n    attain_skill_pred_cls = make_predicate(bp.AttainSkill)\n\n    goal_level = 5\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (attain_skill_pred_cls(Group([1]), Skill.Melee, goal_level, 1), ALL_AGENT), # False\n      (attain_skill_pred_cls(Group([2]), Skill.Melee, goal_level, 1), ALL_AGENT), # False\n      (attain_skill_pred_cls(Group([1]), Skill.Range, goal_level, 1), ALL_AGENT), # True\n      (attain_skill_pred_cls(Group([1,3]), Skill.Fishing, goal_level, 1), ALL_AGENT), # True\n      (attain_skill_pred_cls(Group([1,2,3]), Skill.Carving, goal_level, 3), ALL_AGENT), # False\n      (attain_skill_pred_cls(Group([2,4]), Skill.Carving, goal_level, 2), ALL_AGENT)] # True\n\n    env = self._get_taskenv(test_preds)\n\n    # AttainSkill(Group([1]), Skill.Melee, goal_level, 1) is false\n    # AttainSkill(Group([2]), Skill.Melee, goal_level, 1) is false\n    env.realm.players[1].skills.melee.level.update(goal_level-1)\n    # AttainSkill(Group([1]), Skill.Range, goal_level, 1) is true\n    env.realm.players[1].skills.range.level.update(goal_level)\n    # AttainSkill(Group([1,3]), Skill.Fishing, goal_level, 1) is true\n    env.realm.players[1].skills.fishing.level.update(goal_level)\n    # AttainSkill(Group([1,2,3]), Skill.Carving, goal_level, 3) is false\n    env.realm.players[1].skills.carving.level.update(goal_level)\n    env.realm.players[2].skills.carving.level.update(goal_level)\n    # AttainSkill(Group([2,4]), Skill.Carving, goal_level, 2) is true\n    env.realm.players[4].skills.carving.level.update(goal_level+2)\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [2, 3, 5]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_gain_experience(self):\n    attain_gain_exp_cls = make_predicate(bp.GainExperience)\n\n    goal_exp = 5\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (attain_gain_exp_cls(Group([1]), Skill.Melee, goal_exp, 1), ALL_AGENT), # False\n      (attain_gain_exp_cls(Group([2]), Skill.Melee, goal_exp, 1), ALL_AGENT), # False\n      (attain_gain_exp_cls(Group([1]), Skill.Range, goal_exp, 1), ALL_AGENT), # True\n      (attain_gain_exp_cls(Group([1,3]), Skill.Fishing, goal_exp, 1), ALL_AGENT), # True\n      (attain_gain_exp_cls(Group([1,2,3]), Skill.Carving, goal_exp, 3), ALL_AGENT), # False\n      (attain_gain_exp_cls(Group([2,4]), Skill.Carving, goal_exp, 2), ALL_AGENT)] # True\n\n    env = self._get_taskenv(test_preds)\n\n    # AttainSkill(Group([1]), Skill.Melee, goal_level, 1) is false\n    # AttainSkill(Group([2]), Skill.Melee, goal_level, 1) is false\n    env.realm.players[1].skills.melee.exp.update(goal_exp-1)\n    # AttainSkill(Group([1]), Skill.Range, goal_level, 1) is true\n    env.realm.players[1].skills.range.exp.update(goal_exp)\n    # AttainSkill(Group([1,3]), Skill.Fishing, goal_level, 1) is true\n    env.realm.players[1].skills.fishing.exp.update(goal_exp)\n    # AttainSkill(Group([1,2,3]), Skill.Carving, goal_level, 3) is false\n    env.realm.players[1].skills.carving.exp.update(goal_exp)\n    env.realm.players[2].skills.carving.exp.update(goal_exp)\n    # AttainSkill(Group([2,4]), Skill.Carving, goal_level, 2) is true\n    env.realm.players[4].skills.carving.exp.update(goal_exp+2)\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [2, 3, 5]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_inventory_space_ge_not(self):\n    inv_space_ge_pred_cls = make_predicate(bp.InventorySpaceGE)\n\n    # also test NOT InventorySpaceGE\n    target_space = 3\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (inv_space_ge_pred_cls(Group([1]), target_space), ALL_AGENT), # True -> False\n      (inv_space_ge_pred_cls(Group([2,3]), target_space), ALL_AGENT), # True\n      (inv_space_ge_pred_cls(Group([1,2,3]), target_space), ALL_AGENT), # True -> False\n      (inv_space_ge_pred_cls(Group([1,2,3,4]), target_space+1), ALL_AGENT), # False\n      (~inv_space_ge_pred_cls(Group([1]), target_space+1), ALL_AGENT), # True\n      (~inv_space_ge_pred_cls(Group([1,2,3]), target_space), ALL_AGENT), # False -> True\n      (~inv_space_ge_pred_cls(Group([1,2,3,4]), target_space+1), ALL_AGENT)] # True\n\n    env = self._get_taskenv(test_preds)\n\n    # add one items to agent 1 within the limit\n    capacity = env.realm.players[1].inventory.capacity\n    provide_item(env.realm, 1, Item.Ration, level=1, quantity=capacity-target_space)\n    _, _, _, _, infos = env.step({})\n    self.assertTrue(env.realm.players[1].inventory.space >= target_space)\n    true_task = [0, 1, 2, 4, 6]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # add one more item to agent 1\n    provide_item(env.realm, 1, Item.Ration, level=1, quantity=1)\n    _, _, _, _, infos = env.step({})\n    self.assertTrue(env.realm.players[1].inventory.space < target_space)\n    true_task = [1, 4, 5, 6]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_own_equip_item(self):\n    own_item_pred_cls = make_predicate(bp.OwnItem)\n    equip_item_pred_cls = make_predicate(bp.EquipItem)\n\n    # ration, level 2, quantity 3 (non-stackable)\n    # ammo level 2, quantity 3 (stackable, equipable)\n    goal_level = 2\n    goal_quantity = 3\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (own_item_pred_cls(Group([1]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # False\n      (own_item_pred_cls(Group([2]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # False\n      (own_item_pred_cls(Group([1,2]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # True\n      (own_item_pred_cls(Group([3]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # True\n      (own_item_pred_cls(Group([4,5,6]), Item.Ration, goal_level, goal_quantity), ALL_AGENT), # F\n      (equip_item_pred_cls(Group([4]), Item.Whetstone, goal_level, 1), ALL_AGENT), # False\n      (equip_item_pred_cls(Group([4,5]), Item.Whetstone, goal_level, 1), ALL_AGENT), # True\n      (equip_item_pred_cls(Group([4,5,6]), Item.Whetstone, goal_level, 2), ALL_AGENT)] # True\n\n    env = self._get_taskenv(test_preds)\n\n    # set the level, so that agents 4-6 can equip the Whetstone\n    equip_stone = [4, 5, 6]\n    for ent_id in equip_stone:\n      env.realm.players[ent_id].skills.melee.level.update(6) # melee skill level=6\n\n    # provide items\n    ent_id = 1 # OwnItem(Group([1]), Item.Ration, goal_level, goal_quantity) is false\n    provide_item(env.realm, ent_id, Item.Ration, level=1, quantity=4)\n    provide_item(env.realm, ent_id, Item.Ration, level=2, quantity=2)\n    # OwnItem(Group([2]), Item.Ration, goal_level, goal_quantity) is false\n    ent_id = 2 # OwnItem(Group([1,2]), Item.Ration, goal_level, goal_quantity) is true\n    provide_item(env.realm, ent_id, Item.Ration, level=4, quantity=1)\n    ent_id = 3 # OwnItem(Group([3]), Item.Ration, goal_level, goal_quantity) is true\n    provide_item(env.realm, ent_id, Item.Ration, level=3, quantity=3)\n    # OwnItem(Group([4,5,6]), Item.Ration, goal_level, goal_quantity) is false\n\n    # provide and equip items\n    ent_id = 4 # EquipItem(Group([4]), Item.Whetstone, goal_level, 1) is false\n    provide_item(env.realm, ent_id, Item.Whetstone, level=1, quantity=4)\n    ent_id = 5 # EquipItem(Group([4,5]), Item.Whetstone, goal_level, 1) is true\n    provide_item(env.realm, ent_id, Item.Whetstone, level=4, quantity=1)\n    ent_id = 6 # EquipItem(Group([4,5,6]), Item.Whetstone, goal_level, 2) is true\n    provide_item(env.realm, ent_id, Item.Whetstone, level=2, quantity=4)\n    for ent_id in [4, 5, 6]:\n      whetstone = env.realm.players[ent_id].inventory.items[0]\n      whetstone.use(env.realm.players[ent_id])\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [2, 3, 6, 7]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_fully_armed(self):\n    fully_armed_pred_cls = make_predicate(bp.FullyArmed)\n\n    goal_level = 5\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (fully_armed_pred_cls(Group([1,2,3]), Skill.Range, goal_level, 1), ALL_AGENT), # False\n      (fully_armed_pred_cls(Group([3,4]), Skill.Range, goal_level, 1), ALL_AGENT), # True\n      (fully_armed_pred_cls(Group([4]), Skill.Melee, goal_level, 1), ALL_AGENT), # False\n      (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level, 3), ALL_AGENT), # True\n      (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level+3, 1), ALL_AGENT), # False\n      (fully_armed_pred_cls(Group([4,5,6]), Skill.Range, goal_level, 4), ALL_AGENT)] # False\n\n    env = self._get_taskenv(test_preds)\n\n    # fully equip agents 4-6\n    fully_equip = [4, 5, 6]\n    for ent_id in fully_equip:\n      env.realm.players[ent_id].skills.range.level.update(goal_level+2)\n      # prepare the items\n      item_list = [ itm(env.realm, goal_level) for itm in [\n        Item.Hat, Item.Top, Item.Bottom, Item.Bow, Item.Arrow]]\n      for itm in item_list:\n        env.realm.players[ent_id].inventory.receive(itm)\n        itm.use(env.realm.players[ent_id])\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [1, 3]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_hoard_gold_and_team(self): # HoardGold, TeamHoardGold\n    hoard_gold_pred_cls = make_predicate(bp.HoardGold)\n\n    agent_gold_goal = 10\n    team_gold_goal = 30\n    test_preds = [ # (Predicate, Team), the reward is 1 by default\n      (hoard_gold_pred_cls(Group([1]), agent_gold_goal), ALL_AGENT), # True\n      (hoard_gold_pred_cls(Group([4,5,6]), agent_gold_goal), ALL_AGENT), # False\n      (hoard_gold_pred_cls(Group([1,3,5]), team_gold_goal), ALL_AGENT), # True\n      (hoard_gold_pred_cls(Group([2,4,6]), team_gold_goal), ALL_AGENT)] # False\n\n    env = self._get_taskenv(test_preds)\n\n    # give gold to agents 1-3\n    gold_struck = [1, 2, 3]\n    for ent_id in gold_struck:\n      env.realm.players[ent_id].gold.update(ent_id * 10)\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [0, 2]\n    self._check_result(env, test_preds, infos, true_task)\n    g = sum(env.realm.players[eid].gold.val for eid in Group([2,4,6]).agents)\n    self._check_progress(env.tasks[3], infos, g / team_gold_goal)\n\n    # DONE\n\n  def test_exchange_gold_predicates(self): # Earn Gold, Spend Gold, Make Profit\n    earn_gold_pred_cls = make_predicate(bp.EarnGold)\n    spend_gold_pred_cls = make_predicate(bp.SpendGold)\n    make_profit_pred_cls = make_predicate(bp.MakeProfit)\n\n    gold_goal = 10\n    test_preds = [\n      (earn_gold_pred_cls(Group([1,2]), gold_goal), ALL_AGENT), # True\n      (earn_gold_pred_cls(Group([2,4]), gold_goal), ALL_AGENT), # False\n      (spend_gold_pred_cls(Group([1]), 5), ALL_AGENT), # False -> True\n      (spend_gold_pred_cls(Group([1]), 6), ALL_AGENT), # False,\n      (make_profit_pred_cls(Group([1,2]), 5), ALL_AGENT), # True,\n      (make_profit_pred_cls(Group([1]), 5), ALL_AGENT) # True -> False\n    ]\n\n    env = self._get_taskenv(test_preds)\n    players = env.realm.players\n\n    # 8 gold earned for agent 1\n    # 2 for agent 2\n    env.realm.event_log.record(EventCode.EARN_GOLD, players[1], amount = 5)\n    env.realm.event_log.record(EventCode.EARN_GOLD, players[1], amount = 3)\n    env.realm.event_log.record(EventCode.EARN_GOLD, players[2], amount = 2)\n    _, _, _, _, infos = env.step({})\n    true_task = [0,4,5]\n    self._check_result(env, test_preds, infos, true_task)\n    self._check_progress(env.tasks[1], infos, 2 / gold_goal)\n\n    env.realm.event_log.record(EventCode.BUY_ITEM, players[1],\n                               item=Item.Ration(env.realm,1),\n                               price=5)\n    _, _, _, _, infos = env.step({})\n    true_task = [0,2,4]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_count_event(self): # CountEvent\n    count_event_pred_cls = make_predicate(bp.CountEvent)\n\n    test_preds = [\n      (count_event_pred_cls(Group([1]),\"EAT_FOOD\",1), ALL_AGENT), # True\n      (count_event_pred_cls(Group([1]),\"EAT_FOOD\",2), ALL_AGENT), # False\n      (count_event_pred_cls(Group([1]),\"DRINK_WATER\",1), ALL_AGENT), # False\n      (count_event_pred_cls(Group([1,2]),\"GIVE_GOLD\",1), ALL_AGENT) # True\n    ]\n\n    # 1 Drinks water once\n    # 2 Gives gold once\n    env = self._get_taskenv(test_preds)\n    players = env.realm.players\n    env.realm.event_log.record(EventCode.EAT_FOOD, players[1])\n    env.realm.event_log.record(EventCode.GIVE_GOLD, players[2])\n    _, _, _, _, infos = env.step({})\n    true_task = [0,3]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_score_hit(self): # ScoreHit\n    score_hit_pred_cls = make_predicate(bp.ScoreHit)\n\n    test_preds = [\n      (score_hit_pred_cls(Group([1]), Skill.Mage, 2), ALL_AGENT), # False -> True\n      (score_hit_pred_cls(Group([1]), Skill.Melee, 1), ALL_AGENT) # True\n    ]\n    env = self._get_taskenv(test_preds)\n    players = env.realm.players\n\n    env.realm.event_log.record(EventCode.SCORE_HIT,\n                               players[1], target=players[2],\n                               combat_style = Skill.Mage,\n                               damage=1)\n    env.realm.event_log.record(EventCode.SCORE_HIT,\n                               players[1], target=players[2],\n                               combat_style = Skill.Melee,\n                               damage=1)\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [1]\n    self._check_result(env, test_preds, infos, true_task)\n    self._check_progress(env.tasks[0], infos, 0.5)\n\n    env.realm.event_log.record(EventCode.SCORE_HIT,\n                               players[1], target=players[2],\n                               combat_style = Skill.Mage,\n                               damage=1)\n    env.realm.event_log.record(EventCode.SCORE_HIT,\n                               players[1], target=players[2],\n                               combat_style = Skill.Melee,\n                               damage=1)\n\n    _, _, _, _, infos = env.step({})\n\n    true_task = [0,1]\n    self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\n  def test_defeat_entity(self): # PlayerKill\n    defeat_pred_cls = make_predicate(bp.DefeatEntity)\n\n    test_preds = [\n      (defeat_pred_cls(Group([1]), 'npc', level=1, num_agent=1), ALL_AGENT),\n      (defeat_pred_cls(Group([1]), 'player', level=2, num_agent=2), ALL_AGENT)]\n    env = self._get_taskenv(test_preds)\n    players = env.realm.players\n    npcs = env.realm.npcs\n\n    # set levels\n    npcs[-1].skills.melee.level.update(1)\n    npcs[-1].skills.range.level.update(1)\n    npcs[-1].skills.mage.level.update(1)\n    self.assertEqual(npcs[-1].attack_level, 1)\n    self.assertEqual(players[2].attack_level, 1)\n    players[3].skills.melee.level.update(3)\n    players[4].skills.melee.level.update(2)\n\n    # killing player 2 does not progress the both tasks\n    env.realm.event_log.record(EventCode.PLAYER_KILL, players[1],\n                               target=players[2]) # level 1 player\n    _, _, _, _, infos = env.step({})\n\n    true_task = [] # all false\n    self._check_result(env, test_preds, infos, true_task)\n    for task in env.tasks:\n      self._check_progress(task, infos, 0)\n\n    # killing npc -1 completes the first task\n    env.realm.event_log.record(EventCode.PLAYER_KILL, players[1],\n                               target=npcs[-1]) # level 1 npc\n    _, _, _, _, infos = env.step({})\n\n    true_task = [0]\n    self._check_result(env, test_preds, infos, true_task)\n    self._check_progress(env.tasks[0], infos, 1)\n\n    # killing player 3 makes half progress on the second task\n    env.realm.event_log.record(EventCode.PLAYER_KILL, players[1],\n                               target=players[3]) # level 3 player\n    _, _, _, _, infos = env.step({})\n    self._check_progress(env.tasks[1], infos, .5)\n\n    # killing player 4 completes the second task\n    env.realm.event_log.record(EventCode.PLAYER_KILL, players[1],\n                               target=players[4]) # level 2 player\n    _, _, _, _, infos = env.step({})\n\n    true_task = [0,1]\n    self._check_result(env, test_preds, infos, true_task)\n    self._check_progress(env.tasks[1], infos, 1)\n\n    # DONE\n\n  def test_item_event_predicates(self): # Consume, Harvest, List, Buy\n    for pred_fn, event_type in [(bp.ConsumeItem, 'CONSUME_ITEM'),\n                                  (bp.HarvestItem, 'HARVEST_ITEM'),\n                                  (bp.ListItem, 'LIST_ITEM'),\n                                  (bp.BuyItem, 'BUY_ITEM')]:\n      predicate = make_predicate(pred_fn)\n      id_ = getattr(EventCode, event_type)\n      lvl = random.randint(5,10)\n      quantity = random.randint(5,10)\n      true_item = Item.Ration\n      false_item = Item.Potion\n      test_preds = [\n        (predicate(Group([1,3,5]), true_item, lvl, quantity), ALL_AGENT), # True\n        (predicate(Group([2]), true_item, lvl, quantity), ALL_AGENT), # False\n        (predicate(Group([4]), true_item, lvl, quantity), ALL_AGENT), # False\n        (predicate(Group([6]), true_item, lvl, quantity), ALL_AGENT) # False\n      ]\n\n      env = self._get_taskenv(test_preds)\n      players = env.realm.players\n      # True case: split the required items between 3 and 5\n      for player in (1,3):\n        for _ in range(quantity // 2 + 1):\n          env.realm.event_log.record(id_, players[player], price=1,\n                                     item=true_item(env.realm, lvl+random.randint(0,3)))\n\n      # False case 1: Quantity\n      for _ in range(quantity-1):\n        env.realm.event_log.record(id_, players[2], price=1,\n                                   item=true_item(env.realm, lvl))\n\n      # False case 2: Type\n      for _ in range(quantity+1):\n        env.realm.event_log.record(id_, players[4], price=1,\n                                   item=false_item(env.realm, lvl))\n\n      # False case 3: Level\n      for _ in range(quantity+1):\n        env.realm.event_log.record(id_, players[4], price=1,\n                                   item=true_item(env.realm, random.randint(0,lvl-1)))\n\n      _, _, _, _, infos = env.step({})\n      true_task = [0]\n      self._check_result(env, test_preds, infos, true_task)\n\n    # DONE\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/task/test_sample_task_from_file.py",
    "content": "import unittest\n\nimport nmmo\nfrom tests.testhelpers import ScriptedAgentTestConfig\n\nclass TestSampleTaskFromFile(unittest.TestCase):\n  def test_sample_task_from_file(self):\n    # init the env with the pickled training task spec\n    config = ScriptedAgentTestConfig()\n    config.CURRICULUM_FILE_PATH = 'tests/task/sample_curriculum.pkl'\n    env = nmmo.Env(config)\n\n    # env.reset() samples and instantiates a task for each agent\n    #   when sample_traning_tasks is set True\n    env.reset()\n\n    self.assertEqual(len(env.possible_agents), len(env.tasks))\n    # for the training tasks, the task assignee and subject should be the same\n    for task in env.tasks:\n      self.assertEqual(task.assignee, task.subject)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/task/test_task_api.py",
    "content": "# pylint: disable=unused-argument,invalid-name\nimport unittest\nfrom types import FunctionType\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core.env import Env\nfrom nmmo.task.predicate_api import make_predicate, Predicate\nfrom nmmo.task.task_api import Task, OngoingTask, HoldDurationTask\nfrom nmmo.task.task_spec import TaskSpec, make_task_from_spec\nfrom nmmo.task.group import Group\nfrom nmmo.task.base_predicates import (\n    TickGE, AllMembersWithinRange, StayAlive, HoardGold\n)\n\nfrom nmmo.systems import item as Item\nfrom nmmo.core import action as Action\n\nfrom scripted.baselines import Sleeper\nfrom tests.testhelpers import ScriptedAgentTestConfig, change_spawn_pos\n\n# define predicates in the function form\n#   with the required signatures: gs, subject\ndef Success(gs, subject: Group):\n  return True\n\ndef Failure(gs, subject: Group):\n  return False\n\ndef Fake(gs, subject, a,b,c):\n  return False\n\nclass MockGameState():\n  def __init__(self):\n    # pylint: disable=super-init-not-called\n    self.config = nmmo.config.Default()\n    self.current_tick = -1\n    self.cache_result = {}\n    self.get_subject_view = lambda _: None\n\n  def clear_cache(self):\n    pass\n\nclass TestTaskAPI(unittest.TestCase):\n  def test_predicate_operators(self):\n    # pylint: disable=unsupported-binary-operation,invalid-unary-operand-type\n    # pylint: disable=no-value-for-parameter,not-callable,no-member\n\n    self.assertTrue(isinstance(Success, FunctionType))\n    self.assertTrue(isinstance(Failure, FunctionType))\n\n    # make predicate class from function\n    success_pred_cls = make_predicate(Success)\n    failure_pred_cls = make_predicate(Failure)\n    self.assertTrue(isinstance(success_pred_cls, type)) # class\n    self.assertTrue(isinstance(failure_pred_cls, type))\n\n    # then instantiate predicates\n    SUCCESS = success_pred_cls(Group(0))\n    FAILURE = failure_pred_cls(Group(0))\n    self.assertTrue(isinstance(SUCCESS, Predicate))\n    self.assertTrue(isinstance(FAILURE, Predicate))\n\n    # NOTE: only the instantiated predicate can be used with operators like below\n    mock_gs = MockGameState()\n\n    # get the individual predicate\"s source code\n    self.assertEqual(SUCCESS.get_source_code(),\n                     \"def Success(gs, subject: Group):\\n  return True\")\n    self.assertEqual(FAILURE.get_source_code(),\n                     \"def Failure(gs, subject: Group):\\n  return False\")\n\n    # AND (&), OR (|), NOT (~)\n    pred1 = SUCCESS & FAILURE\n    self.assertFalse(pred1(mock_gs))\n    # NOTE: get_source_code() of the combined predicates returns the joined str\n    #   of each predicate\"s source code, which may NOT represent what the actual\n    #   predicate is doing\n    self.assertEqual(pred1.get_source_code(),\n                     \"def Success(gs, subject: Group):\\n  return True\\n\\n\"+\n                     \"def Failure(gs, subject: Group):\\n  return False\")\n\n    pred2 = SUCCESS | FAILURE | SUCCESS\n    self.assertTrue(pred2(mock_gs))\n    self.assertEqual(pred2.get_source_code(),\n                     \"def Success(gs, subject: Group):\\n  return True\\n\\n\"+\n                     \"def Failure(gs, subject: Group):\\n  return False\\n\\n\"+\n                     \"def Success(gs, subject: Group):\\n  return True\")\n\n    pred3 = SUCCESS & ~ FAILURE & SUCCESS\n    self.assertTrue(pred3(mock_gs))\n    # NOTE: demonstrating the above point -- it just returns the functions\n    #   NOT what this predicate actually evaluates.\n    self.assertEqual(pred2.get_source_code(),\n                     pred3.get_source_code())\n\n    # predicate math\n    pred4 = 0.1 * SUCCESS + 0.3\n    self.assertEqual(pred4(mock_gs), 0.4)\n    self.assertEqual(pred4.name,\n                     \"(ADD_(MUL_(Success_(0,))_0.1)_0.3)\")\n    # NOTE: demonstrating the above point again, -- it just returns the functions\n    #   NOT what this predicate actually evaluates.\n    self.assertEqual(pred4.get_source_code(),\n                     \"def Success(gs, subject: Group):\\n  return True\")\n\n    pred5 = 0.3 * SUCCESS - 1\n    self.assertEqual(pred5(mock_gs), 0.0) # cannot go below 0\n\n    pred6 = 0.3 * SUCCESS + 1\n    self.assertEqual(pred6(mock_gs), 1.0) # cannot go over 1\n\n  def test_team_assignment(self):\n    team =  Group([1, 2, 8, 9], \"TeamFoo\")\n\n    self.assertEqual(team.name, \"TeamFoo\")\n    self.assertEqual(team[2].name, \"TeamFoo.2\")\n    self.assertEqual(team[2], (8,))\n\n    # don\"t allow member of one-member team\n    self.assertEqual(team[2][0].name, team[2].name)\n\n  def test_predicate_name(self):\n    # pylint: disable=no-value-for-parameter,no-member\n    # make predicate class from function\n    success_pred_cls = make_predicate(Success)\n    failure_pred_cls = make_predicate(Failure)\n    fake_pred_cls = make_predicate(Fake)\n\n    # instantiate the predicates\n    SUCCESS = success_pred_cls(Group([0,2]))\n    FAILURE = failure_pred_cls(Group(0))\n    fake_pred = fake_pred_cls(Group(2), 1, Item.Hat, Action.Melee)\n    combination = (SUCCESS & ~ (FAILURE | fake_pred)) | (FAILURE * fake_pred + .3) - .4\n    self.assertEqual(combination.name,\n      \"(OR_(AND_(Success_(0,2))_(NOT_(OR_(Failure_(0,))_(Fake_(2,)_1_Hat_Melee))))_\"+\\\n      \"(SUB_(ADD_(MUL_(Failure_(0,))_(Fake_(2,)_1_Hat_Melee))_0.3)_0.4))\")\n\n  def test_task_api_with_predicate(self):\n    # pylint: disable=no-value-for-parameter,no-member\n    fake_pred_cls = make_predicate(Fake)\n\n    mock_gs = MockGameState()\n    group = Group(2)\n    item = Item.Hat\n    action = Action.Melee\n    predicate = fake_pred_cls(group, a=1, b=item, c=action)\n    self.assertEqual(predicate.get_source_code(),\n                     \"def Fake(gs, subject, a,b,c):\\n  return False\")\n    self.assertEqual(predicate.get_signature(), [\"gs\", \"subject\", \"a\", \"b\", \"c\"])\n    self.assertEqual(predicate.args, tuple(group,))\n    self.assertDictEqual(predicate.kwargs, {\"a\": 1, \"b\": item, \"c\": action})\n\n    assignee = [1,2,3] # list of agent ids\n    task = predicate.create_task(assignee=assignee)\n    rewards, infos = task.compute_rewards(mock_gs)\n\n    self.assertEqual(task.name, # contains predicate name and assignee list\n                     \"(Task_eval_fn:(Fake_(2,)_a:1_b:Hat_c:Melee)_assignee:(1,2,3))\")\n    self.assertEqual(task.get_source_code(),\n                     \"def Fake(gs, subject, a,b,c):\\n  return False\")\n    self.assertEqual(task.get_signature(), [\"gs\", \"subject\", \"a\", \"b\", \"c\"])\n    self.assertEqual(task.args, tuple(group,))\n    self.assertDictEqual(task.kwargs, {\"a\": 1, \"b\": item, \"c\": action})\n    for agent_id in assignee:\n      self.assertEqual(rewards[agent_id], 0)\n      self.assertEqual(infos[agent_id][\"progress\"], 0) # progress (False -> 0)\n      self.assertFalse(task.completed)\n\n  def test_task_api_with_function(self):\n    mock_gs = MockGameState()\n    def eval_with_subject_fn(subject: Group):\n      def is_agent_1(gs):\n        return any(agent_id == 1 for agent_id in subject.agents)\n      return is_agent_1\n\n    assignee = [1,2,3] # list of agent ids\n    task = Task(eval_with_subject_fn(Group(assignee)), assignee)\n    rewards, infos = task.compute_rewards(mock_gs)\n\n    self.assertEqual(task.name, # contains predicate name and assignee list\n                     \"(Task_eval_fn:is_agent_1_assignee:(1,2,3))\")\n    self.assertEqual(task.get_source_code(),\n                     \"def is_agent_1(gs):\\n        \" +\n                     \"return any(agent_id == 1 for agent_id in subject.agents)\")\n    self.assertEqual(task.get_signature(), [\"gs\"])\n    self.assertEqual(task.args, [])\n    self.assertDictEqual(task.kwargs, {})\n    self.assertEqual(task.subject, tuple(assignee))\n    self.assertEqual(task.assignee, tuple(assignee))\n    for agent_id in assignee:\n      self.assertEqual(rewards[agent_id], 1)\n      self.assertEqual(infos[agent_id][\"progress\"], 1) # progress (True -> 1)\n      self.assertTrue(task.completed)\n\n  def test_predicate_fn_using_other_predicate_fn(self):\n    # define a predicate: to form a tight formation, for a certain number of ticks\n    def PracticeFormation(gs, subject, dist, num_tick):\n      return AllMembersWithinRange(gs, subject, dist) * TickGE(gs, subject, num_tick)\n\n    # team should stay together within 1 tile for 10 ticks\n    goal_tick = 10\n    task_spec = TaskSpec(eval_fn=PracticeFormation,\n                         eval_fn_kwargs={\"dist\": 1, \"num_tick\": goal_tick},\n                         reward_to=\"team\")\n\n    # create the test task from the task spec\n    teams = {1:[1,2,3], 3:[4,5], 6:[6,7], 9:[8,9], 14:[10,11]}\n    team_ids= list(teams.keys())\n\n    config = ScriptedAgentTestConfig()\n    config.set(\"PLAYERS\", [Sleeper])\n    config.set(\"IMMORTAL\", True)\n\n    env = Env(config)\n    env.reset(make_task_fn=lambda: make_task_from_spec(teams, [task_spec]))\n\n    # check the task information\n    task = env.tasks[0]\n    self.assertEqual(task.name,\n                     \"(Task_eval_fn:(PracticeFormation_(1,2,3)_dist:1_num_tick:10)\"+\n                     \"_assignee:(1,2,3))\")\n    self.assertEqual(task.get_source_code(),\n                     \"def PracticeFormation(gs, subject, dist, num_tick):\\n      \"+\n                     \"return AllMembersWithinRange(gs, subject, dist) * \"+\n                     \"TickGE(gs, subject, num_tick)\")\n    self.assertEqual(task.get_signature(), [\"gs\", \"subject\", \"dist\", \"num_tick\"])\n    self.assertEqual(task.subject, tuple(teams[team_ids[0]]))\n    self.assertEqual(task.kwargs, task_spec.eval_fn_kwargs)\n    self.assertEqual(task.assignee, tuple(teams[team_ids[0]]))\n\n    # check the agent-task map\n    for agent_id, agent_tasks in env.agent_task_map.items():\n      for task in agent_tasks:\n        self.assertTrue(agent_id in task.assignee)\n\n    # move agent 2, 3 to agent 1\"s pos\n    for agent_id in [2,3]:\n      change_spawn_pos(env.realm, agent_id,\n                       env.realm.players[1].pos)\n\n    for tick in range(goal_tick+2):\n      _, rewards, _, _, infos = env.step({})\n\n      if tick < 10:\n        target_reward = 1/goal_tick\n        self.assertAlmostEqual(rewards[1], target_reward)\n        self.assertAlmostEqual((1+tick)/goal_tick,\n                               infos[1][\"task\"][env.tasks[0].name][\"progress\"])\n      else:\n        # tick 11, task should be completed\n        self.assertEqual(rewards[1], 0)\n        self.assertEqual(infos[1][\"task\"][env.tasks[0].name][\"progress\"], 1)\n        self.assertEqual(infos[1][\"task\"][env.tasks[0].name][\"completed\"], True)\n\n    # test the task_spec_with_embedding\n    task_embedding = np.ones(config.TASK_EMBED_DIM, dtype=np.float16)\n    task_spec_with_embedding = TaskSpec(eval_fn=PracticeFormation,\n                                        eval_fn_kwargs={\"dist\": 1, \"num_tick\": goal_tick},\n                                        reward_to=\"team\",\n                                        embedding=task_embedding)\n    env.reset(make_task_fn=lambda: make_task_from_spec(teams, [task_spec_with_embedding]))\n\n    task = env.tasks[0]\n    self.assertEqual(task.spec_name, # without the subject and assignee agent ids\n                     \"Task_PracticeFormation_(dist:1_num_tick:10)_reward_to:team\")\n    self.assertEqual(task.name,\n                     \"(Task_eval_fn:(PracticeFormation_(1,2,3)_dist:1_num_tick:10)\"+\n                     \"_assignee:(1,2,3))\")\n    self.assertEqual(task.get_source_code(),\n                     \"def PracticeFormation(gs, subject, dist, num_tick):\\n      \"+\n                     \"return AllMembersWithinRange(gs, subject, dist) * \"+\n                     \"TickGE(gs, subject, num_tick)\")\n    self.assertEqual(task.get_signature(), [\"gs\", \"subject\", \"dist\", \"num_tick\"])\n    self.assertEqual(task.subject, tuple(teams[team_ids[0]]))\n    self.assertEqual(task.kwargs, task_spec.eval_fn_kwargs)\n    self.assertEqual(task.assignee, tuple(teams[team_ids[0]]))\n    self.assertTrue(np.array_equal(task.embedding, task_embedding))\n\n    obs_spec = env.observation_space(1)\n    self.assertTrue(obs_spec[\"Task\"].contains(task.embedding))\n\n  def test_completed_tasks_in_info(self):\n    # pylint: disable=no-value-for-parameter,no-member\n    config = ScriptedAgentTestConfig()\n    config.set(\"ALLOW_MULTI_TASKS_PER_AGENT\", True)\n    env = Env(config)\n\n    # make predicate class from function\n    success_pred_cls = make_predicate(Success)\n    failure_pred_cls = make_predicate(Failure)\n    fake_pred_cls = make_predicate(Fake)\n\n    # instantiate the predicates\n    same_team = [1, 2, 3, 4]\n    predicates = [\n      success_pred_cls(Group(1)), # task 1\n      failure_pred_cls(Group(2)), # task 2\n      fake_pred_cls(Group(3), 1, Item.Hat, Action.Melee), # task 3\n      success_pred_cls(Group(same_team))] # task 4\n\n    # tasks can be created directly from predicate instances\n    test_tasks = [pred.create_task() for pred in predicates]\n\n    # tasks are all instantiated with the agent ids\n    env.reset(make_task_fn=lambda: test_tasks)\n    _, _, _, _, infos = env.step({})\n\n    # agent 1: assigned only task 1, which is always True\n    self.assertEqual(infos[1][\"task\"][env.tasks[0].name][\"reward\"], 1.0)\n    for i in [1, 2]: # task 2 and 3\n      self.assertTrue(env.tasks[i].name not in infos[1][\"task\"])\n\n    # agent 2: assigned task 2 (Failure) and task 4 (Success)\n    self.assertEqual(infos[2][\"task\"][env.tasks[1].name][\"reward\"], 0.0) # task 2\n    self.assertEqual(infos[2][\"task\"][env.tasks[3].name][\"reward\"], 1.0) # task 4\n\n    # agent 3 assigned task 3, Fake(), which is always False (0)\n    self.assertEqual(infos[3][\"task\"][env.tasks[2].name][\"reward\"], 0.0) # task 3\n\n    # all agents in the same team with agent 2 have SUCCESS\n    # other agents don\"t have any tasks assigned\n    for ent_id in env.possible_agents:\n      if ent_id in same_team:\n        self.assertEqual(infos[ent_id][\"task\"][env.tasks[3].name][\"reward\"], 1.0)\n      else:\n        self.assertTrue(env.tasks[3].name not in infos[ent_id][\"task\"])\n\n    # DONE\n\n  def test_make_task_from_spec(self):\n    teams = {0:[1,2,3], 1:[4,5,6]}\n    test_embedding = np.array([1,2,3])\n    task_spec = [\n      TaskSpec(eval_fn=TickGE, eval_fn_kwargs={\"num_tick\": 20}),\n      TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={}, task_cls=OngoingTask),\n      TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={\"target\": \"my_team_leader\"},\n               task_cls=OngoingTask, reward_to=\"team\"),\n      TaskSpec(eval_fn=StayAlive, eval_fn_kwargs={\"target\": \"left_team\"},\n               task_cls=OngoingTask, task_kwargs={\"reward_multiplier\": 2},\n               reward_to=\"team\", embedding=test_embedding),\n    ]\n\n    task_list = []\n    # testing each task spec, individually\n    for single_spec in task_spec:\n      task_list.append(make_task_from_spec(teams, [single_spec]))\n\n    # check the task spec names\n    self.assertEqual(task_list[0][0].spec_name,\n                     \"Task_TickGE_(num_tick:20)_reward_to:agent\")\n    self.assertEqual(task_list[1][0].spec_name,\n                     \"OngoingTask_StayAlive_()_reward_to:agent\")\n    self.assertEqual(task_list[2][0].spec_name,\n                     \"OngoingTask_StayAlive_(target:my_team_leader)_reward_to:team\")\n    self.assertEqual(task_list[3][0].spec_name,\n                     \"OngoingTask_StayAlive_(target:left_team)_reward_to:team\")\n\n    # check the task names\n    self.assertEqual(task_list[0][0].name,\n                     \"(Task_eval_fn:(TickGE_(1,)_num_tick:20)_assignee:(1,))\")\n    self.assertEqual(task_list[1][0].name,\n                     \"(OngoingTask_eval_fn:(StayAlive_(1,))_assignee:(1,))\")\n    self.assertEqual(task_list[2][0].name,\n                     \"(OngoingTask_eval_fn:(StayAlive_(1,))_assignee:(1,2,3))\")\n    self.assertEqual(task_list[3][0].name,\n                     \"(OngoingTask_eval_fn:(StayAlive_(4,5,6))_assignee:(1,2,3))\")\n    self.assertEqual(task_list[3][0].reward_multiplier, 2)\n    self.assertTrue(np.array_equal(task_list[3][0].embedding, np.array([1,2,3])))\n\n  def test_hold_duration_task(self):\n    # pylint: disable=protected-access\n    # each agent should hoard gold for 10 ticks\n    goal_tick = goal_gold = 10\n    task_spec = [TaskSpec(eval_fn=HoardGold,\n                          eval_fn_kwargs={\"amount\": goal_gold},\n                          task_cls=HoldDurationTask,\n                          task_kwargs={\"hold_duration\": goal_tick})] * 3\n\n    config = ScriptedAgentTestConfig()\n    config.PLAYERS =[Sleeper]\n    config.IMMORTAL = True\n\n    teams = {id: [id] for id in range(1,4)}\n    env = Env(config)\n    env.reset(make_task_fn=lambda: make_task_from_spec(teams, task_spec))\n\n    # give agent 1, 2 enough gold\n    for agent_id in [1,2]:\n      env.realm.players[agent_id].gold.update(goal_gold+1)\n\n    for _ in range(5):\n      env.step({})\n\n    # check the task information\n    self.assertEqual(env.tasks[0].spec_name,\n                     \"HoldDurationTask_HoardGold_(amount:10)_reward_to:agent\")\n    for idx in [0, 1]:\n      self.assertEqual(env.tasks[idx]._progress, 0.5) # agent 1 & 2 has enough gold\n      self.assertEqual(env.tasks[idx]._max_progress, 0.5)\n      self.assertEqual(env.tasks[idx].reward_signal_count, 5)\n    self.assertTrue(env.tasks[2]._progress == 0.0) # agent 3 has no gold\n    for task in env.tasks:\n      self.assertTrue(task.completed is False) # not completed yet\n\n    # take away gold from agent 2\n    env.realm.players[2].gold.update(goal_gold-1)\n\n    env.step({})\n    self.assertEqual(env.tasks[0]._progress, 0.6) # agent 1 has enough gold\n    self.assertEqual(env.tasks[0]._max_progress, 0.6)\n    self.assertEqual(env.tasks[0].reward_signal_count, 6)\n    self.assertEqual(env.tasks[1]._progress, 0) # agent 2 has not enough gold\n    self.assertEqual(env.tasks[1]._max_progress, 0.5) # max values are preserved\n    self.assertEqual(env.tasks[1]._positive_reward_count, 5)\n    self.assertEqual(env.tasks[1].reward_signal_count, 6) # 5 positive + 1 negative\n\n    for _ in range(4):\n      env.step({})\n\n    # only agent 1 successfully held 10 gold for 10 ticks\n    self.assertTrue(env.tasks[0].completed is True)\n    self.assertTrue(env.tasks[1].completed is False)\n    self.assertTrue(env.tasks[2].completed is False)\n\n  def test_task_spec_with_predicate(self):\n    teams = {0:[1,2,3], 1:[4,5,6]}\n    SUCCESS = make_predicate(Success)(Group(1))\n    FAILURE = make_predicate(Failure)(Group([2,3]))\n    predicate = SUCCESS & FAILURE\n    predicate.name = \"SuccessAndFailure\"\n\n    # make task spec\n    task_spec = [TaskSpec(predicate=predicate,\n                          eval_fn=None,\n                          eval_fn_kwargs={\"success_target\": 1,\n                                          \"test_item\": Item.Hat})]\n    tasks = make_task_from_spec(teams, task_spec)\n\n    env = Env(ScriptedAgentTestConfig())\n    env.reset(make_task_fn=lambda: tasks)\n    env.step({})\n\n    # check the task information\n    self.assertEqual(env.tasks[0].spec_name,\n                     \"Task_SuccessAndFailure_(success_target:1_test_item:Hat)_reward_to:agent\")\n\nif __name__ == \"__main__\":\n  unittest.main()\n"
  },
  {
    "path": "tests/task/test_task_system_perf.py",
    "content": "import unittest\n\nimport nmmo\nfrom nmmo.core.env import Env\nfrom nmmo.task.task_api import Task, nmmo_default_task\nfrom tests.testhelpers import profile_env_step\n\nPROFILE_PERF = False\n\nclass TestTaskSystemPerf(unittest.TestCase):\n  def test_nmmo_default_task(self):\n    config = nmmo.config.Default()\n    env = Env(config)\n    agent_list = env.possible_agents\n\n    for test_mode in [None, 'no_task', 'dummy_eval_fn', 'pure_func_eval']:\n\n      # create tasks\n      if test_mode == 'pure_func_eval':\n        def create_stay_alive_eval_wo_group(agent_id: int):\n          return lambda gs: agent_id in gs.alive_agents\n        tasks = [Task(create_stay_alive_eval_wo_group(agent_id), assignee=agent_id)\n                for agent_id in agent_list]\n      else:\n        tasks = nmmo_default_task(agent_list, test_mode)\n\n      # check tasks\n      for agent_id in agent_list:\n        if test_mode is None:\n          self.assertTrue('TickGE' in tasks[agent_id-1].name) # default task\n        if test_mode != 'no_task':\n          self.assertTrue(f'assignee:({agent_id},)' in tasks[agent_id-1].name)\n\n      # pylint: disable=cell-var-from-loop\n      if PROFILE_PERF:\n        test_cond = 'default' if test_mode is None else test_mode\n        profile_env_step(tasks=tasks, condition=test_cond)\n      else:\n        env.reset(make_task_fn=lambda: tasks)\n        for _ in range(3):\n          env.step({})\n\n    # DONE\n\n\nif __name__ == '__main__':\n  unittest.main()\n\n  # \"\"\" Tested on Win 11, docker\n  # === Test condition: default (StayAlive-based Predicate) ===\n  # - env.step({}): 13.398321460997977\n  # - env.realm.step(): 3.6524868449996575\n  # - env._compute_observations(): 3.2038183499971638\n  # - obs.to_gym(), ActionTarget: 2.30746804500086\n  # - env._compute_rewards(): 2.7206644940015394\n\n  # === Test condition: no_task ===\n  # - env.step({}): 10.576253965999058\n  # - env.realm.step(): 3.674701832998835\n  # - env._compute_observations(): 3.260661373002222\n  # - obs.to_gym(), ActionTarget: 2.313872797996737\n  # - env._compute_rewards(): 0.009020475001307204\n\n  # === Test condition: dummy_eval_fn -based Predicate ===\n  # - env.step({}): 12.797982947995479\n  # - env.realm.step(): 3.604593793003005\n  # - env._compute_observations(): 3.2095355240016943\n  # - obs.to_gym(), ActionTarget: 2.313207338003849\n  # - env._compute_rewards(): 2.266267291997792\n\n  # === Test condition: pure_func_eval WITHOUT Predicate ===\n  # - env.step({}): 10.637560240997118\n  # - env.realm.step(): 3.633970066999609\n  # - env._compute_observations(): 3.2308093659958104\n  # - obs.to_gym(), ActionTarget: 2.331246039000689\n  # - env._compute_rewards(): 0.0988905300037004\n  # \"\"\"\n"
  },
  {
    "path": "tests/test_death_fog.py",
    "content": "# pylint: disable=protected-access, no-member\nimport unittest\nimport nmmo\n\n\nclass TestDeathFog(unittest.TestCase):\n  def test_death_fog(self):\n    config = nmmo.config.Default()\n    config.set(\"DEATH_FOG_ONSET\", 3)\n    config.set(\"DEATH_FOG_SPEED\", 1/2)\n    config.set(\"DEATH_FOG_FINAL_SIZE\", 16)\n    config.set(\"PROVIDE_DEATH_FOG_OBS\", True)\n\n    env = nmmo.Env(config)\n    env.reset()\n\n    # check the initial fog map\n    border = config.MAP_BORDER\n    other_border = config.MAP_SIZE - config.MAP_BORDER - 1\n    center = config.MAP_SIZE // 2\n    safe = config.DEATH_FOG_FINAL_SIZE\n    self.assertEqual(env.realm.fog_map[border,border], 0)\n    self.assertEqual(env.realm.fog_map[other_border,other_border], 0)\n    self.assertEqual(env.realm.fog_map[border+1,border+1], -1)\n\n    # Safe area should be marked with the negative map size\n    self.assertEqual(env.realm.fog_map[center-safe,center-safe], -config.MAP_SIZE)\n    self.assertEqual(env.realm.fog_map[center+safe-1,center+safe-1], -config.MAP_SIZE)\n\n    for _ in range(config.DEATH_FOG_ONSET):\n      env.step({})\n\n    # check the fog map after the death fog onset\n    self.assertEqual(env.realm.fog_map[border,border], config.DEATH_FOG_SPEED)\n    self.assertEqual(env.realm.fog_map[border+1,border+1], -1 + config.DEATH_FOG_SPEED)\n\n    for _ in range(3):\n      env.step({})\n\n    # check the fog map after 3 ticks after the death fog onset\n    self.assertEqual(env.realm.fog_map[border,border], config.DEATH_FOG_SPEED*4)\n    self.assertEqual(env.realm.fog_map[border+1,border+1], -1 + config.DEATH_FOG_SPEED*4)\n\nif __name__ == '__main__':\n  unittest.main()\n"
  },
  {
    "path": "tests/test_determinism.py",
    "content": "import unittest\nfrom timeit import timeit\nimport numpy as np\nfrom tqdm import tqdm\n\nimport nmmo\nfrom nmmo.lib import seeding\nfrom tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv\nfrom tests.testhelpers import observations_are_equal\n\n# 30 seems to be enough to test variety of agent actions\nTEST_HORIZON = 30\nRANDOM_SEED = np.random.randint(0, 100000)\n\n\nclass TestDeterminism(unittest.TestCase):\n  def test_np_random_get_direction(self):\n    # pylint: disable=protected-access,bad-builtin,unnecessary-lambda\n    np_random_1, np_seed_1 = seeding.np_random(RANDOM_SEED)\n    np_random_2, np_seed_2 = seeding.np_random(RANDOM_SEED)\n    self.assertEqual(np_seed_1, np_seed_2)\n\n    # also test get_direction, which was added for speed optimization\n    self.assertTrue(np.array_equal(np_random_1._dir_seq, np_random_2._dir_seq))\n\n    print(\"---test_np_random_get_direction---\")\n    print(\"np_random.integers():\", timeit(lambda: np_random_1.integers(0,4),\n                                          number=100000, globals=globals()))\n    print(\"np_random.get_direction():\", timeit(lambda: np_random_1.get_direction(),\n                                                number=100000, globals=globals()))\n\n  def test_map_determinism(self):\n    config = nmmo.config.Default()\n    config.set(\"MAP_FORCE_GENERATION\", True)\n    config.set(\"TERRAIN_FLIP_SEED\", False)\n\n    map_generator = config.MAP_GENERATOR(config)\n    np_random1, _ = seeding.np_random(RANDOM_SEED)\n    np_random1_1, _ = seeding.np_random(RANDOM_SEED)\n\n    terrain1, tiles1 = map_generator.generate_map(0, np_random1)\n    terrain1_1, tiles1_1 = map_generator.generate_map(0, np_random1_1)\n\n    self.assertTrue(np.array_equal(terrain1, terrain1_1))\n    self.assertTrue(np.array_equal(tiles1, tiles1_1))\n\n    # test flip seed\n    config2 = nmmo.config.Default()\n    config2.set(\"MAP_FORCE_GENERATION\", True)\n    config2.set(\"TERRAIN_FLIP_SEED\", True)\n\n    map_generator2 = config2.MAP_GENERATOR(config2)\n    np_random2, _ = seeding.np_random(RANDOM_SEED)\n    terrain2, tiles2 = map_generator2.generate_map(0, np_random2)\n\n    self.assertFalse(np.array_equal(terrain1, terrain2))\n    self.assertFalse(np.array_equal(tiles1, tiles2))\n\n  def test_env_level_rng(self):\n    # two envs running independently should return the same results\n\n    # config to always generate new maps, to test map determinism\n    config1 = ScriptedAgentTestConfig()\n    config1.set(\"MAP_FORCE_GENERATION\", True)\n    config1.set(\"PATH_MAPS\", \"maps/det1\")\n    config1.set(\"RESOURCE_RESILIENT_POPULATION\", 0.2)  # uses np_random\n    config2 = ScriptedAgentTestConfig()\n    config2.set(\"MAP_FORCE_GENERATION\", True)\n    config2.set(\"PATH_MAPS\", \"maps/det2\")\n    config2.set(\"RESOURCE_RESILIENT_POPULATION\", 0.2)\n\n    # to create the same maps, seed must be provided\n    env1 = ScriptedAgentTestEnv(config1, seed=RANDOM_SEED)\n    env2 = ScriptedAgentTestEnv(config2, seed=RANDOM_SEED)\n    envs = [env1, env2]\n\n    init_obs = [env.reset(seed=RANDOM_SEED+1)[0] for env in envs]\n\n    self.assertTrue(observations_are_equal(init_obs[0], init_obs[0])) # sanity check\n    self.assertTrue(observations_are_equal(init_obs[0], init_obs[1]),\n                    f\"The multi-env determinism failed. Seed: {RANDOM_SEED}.\")\n\n    for _ in tqdm(range(TEST_HORIZON)):\n      # step returns a tuple of (obs, rewards, dones, infos)\n      step_results = [env.step({}) for env in envs]\n      self.assertTrue(observations_are_equal(step_results[0][0], step_results[1][0]),\n                      f\"The multi-env determinism failed. Seed: {RANDOM_SEED}.\")\n\n    event_logs = [env.realm.event_log.get_data() for env in envs]\n    self.assertTrue(np.array_equal(event_logs[0], event_logs[1]),\n                    f\"The multi-env determinism failed. Seed: {RANDOM_SEED}.\")\n\n\nif __name__ == \"__main__\":\n  unittest.main()\n"
  },
  {
    "path": "tests/test_eventlog.py",
    "content": "import unittest\n\nimport nmmo\nfrom nmmo.datastore.numpy_datastore import NumpyDatastore\nfrom nmmo.lib.event_log import EventState, EventLogger\nfrom nmmo.lib.event_code import EventCode\nfrom nmmo.entity.entity import Entity\nfrom nmmo.systems.item import ItemState\nfrom nmmo.systems.item import Whetstone, Ration, Hat\nfrom nmmo.systems import skill as Skill\n\n\nclass MockRealm:\n  def __init__(self):\n    self.config = nmmo.config.Default()\n    self.datastore = NumpyDatastore()\n    self.items = {}\n    self.datastore.register_object_type(\"Event\", EventState.State.num_attributes)\n    self.datastore.register_object_type(\"Item\", ItemState.State.num_attributes)\n    self.tick = 0\n    self.event_log = None\n\n  def step(self):\n    self.tick += 1\n    self.event_log.update()\n\nclass MockEntity(Entity):\n  # pylint: disable=super-init-not-called\n  def __init__(self, ent_id, **kwargs):\n    self.id = ent_id\n    self.level = kwargs.pop('attack_level', 0)\n\n  @property\n  def ent_id(self):\n    return self.id\n\n  @property\n  def attack_level(self):\n    return self.level\n\n\nclass TestEventLog(unittest.TestCase):\n\n  def test_event_logging(self):\n    mock_realm = MockRealm()\n    mock_realm.event_log = EventLogger(mock_realm)\n    event_log = mock_realm.event_log\n\n    event_log.record(EventCode.EAT_FOOD, MockEntity(1))\n    event_log.record(EventCode.DRINK_WATER, MockEntity(2))\n    event_log.record(EventCode.SCORE_HIT, MockEntity(2),\n                     target=MockEntity(1), combat_style=Skill.Melee, damage=50)\n    event_log.record(EventCode.PLAYER_KILL, MockEntity(3),\n                     target=MockEntity(5, attack_level=5))\n    mock_realm.step()\n\n    event_log.record(EventCode.CONSUME_ITEM, MockEntity(4),\n                     item=Ration(mock_realm, 8))\n    event_log.record(EventCode.GIVE_ITEM, MockEntity(4))\n    event_log.record(EventCode.DESTROY_ITEM, MockEntity(5))\n    event_log.record(EventCode.HARVEST_ITEM, MockEntity(6),\n                     item=Whetstone(mock_realm, 3))\n    mock_realm.step()\n\n    event_log.record(EventCode.GIVE_GOLD, MockEntity(7))\n    event_log.record(EventCode.LIST_ITEM, MockEntity(8),\n                     item=Ration(mock_realm, 5), price=11)\n    event_log.record(EventCode.EARN_GOLD, MockEntity(9), amount=15)\n    event_log.record(EventCode.BUY_ITEM, MockEntity(10),\n                     item=Whetstone(mock_realm, 7), price=21)\n    #event_log.record(EventCode.SPEND_GOLD, env.realm.players[11], amount=25)\n    mock_realm.step()\n\n    event_log.record(EventCode.LEVEL_UP, MockEntity(12),\n                     skill=Skill.Fishing, level=3)\n    mock_realm.step()\n\n    event_log.record(EventCode.GO_FARTHEST, MockEntity(12), distance=6)\n    event_log.record(EventCode.EQUIP_ITEM, MockEntity(12),\n                     item=Hat(mock_realm, 4))\n    mock_realm.step()\n\n    log_data = [list(row) for row in event_log.get_data()]\n    self.assertListEqual(log_data, [\n      [1,  1, 1, EventCode.EAT_FOOD, 0, 0, 0, 0, 0],\n      [1,  2, 1, EventCode.DRINK_WATER, 0, 0, 0, 0, 0],\n      [1,  2, 1, EventCode.SCORE_HIT, 1, 0, 50, 0, 1],\n      [1,  3, 1, EventCode.PLAYER_KILL, 0, 5, 0, 0, 5],\n      [1,  4, 2, EventCode.CONSUME_ITEM, 16, 8, 1, 0, 1],\n      [1,  4, 2, EventCode.GIVE_ITEM, 0, 0, 0, 0, 0],\n      [1,  5, 2, EventCode.DESTROY_ITEM, 0, 0, 0, 0, 0],\n      [1,  6, 2, EventCode.HARVEST_ITEM, 13, 3, 1, 0, 2],\n      [1,  7, 3, EventCode.GIVE_GOLD, 0, 0, 0, 0, 0],\n      [1,  8, 3, EventCode.LIST_ITEM, 16, 5, 1, 11, 3],\n      [1,  9, 3, EventCode.EARN_GOLD, 0, 0, 0, 15, 0],\n      [1, 10, 3, EventCode.BUY_ITEM, 13, 7, 1, 21, 4],\n      [1, 12, 4, EventCode.LEVEL_UP, 4, 3, 0, 0, 0],\n      [1, 12, 5, EventCode.GO_FARTHEST, 0, 0, 6, 0, 0],\n      [1, 12, 5, EventCode.EQUIP_ITEM, 2, 4, 1, 0, 5]])\n\n    log_by_tick = [list(row) for row in event_log.get_data(tick = 4)]\n    self.assertListEqual(log_by_tick, [\n      [1, 12, 4, EventCode.LEVEL_UP, 4, 3, 0, 0, 0]])\n\n    log_by_event = [list(row) for row in event_log.get_data(event_code = EventCode.CONSUME_ITEM)]\n    self.assertListEqual(log_by_event, [\n      [1,  4, 2, EventCode.CONSUME_ITEM, 16, 8, 1, 0, 1]])\n\n    log_by_tick_agent = [list(row) for row in \\\n                         event_log.get_data(tick = 5,\n                                            agents = [12],\n                                            event_code = EventCode.EQUIP_ITEM)]\n    self.assertListEqual(log_by_tick_agent, [\n      [1, 12, 5, EventCode.EQUIP_ITEM, 2, 4, 1, 0, 5]])\n\n    empty_log = event_log.get_data(tick = 10)\n    self.assertTrue(empty_log.shape[0] == 0)\n\nif __name__ == '__main__':\n  unittest.main()\n\n  \"\"\"\n  TEST_HORIZON = 50\n  RANDOM_SEED = 338\n\n  from tests.testhelpers import ScriptedAgentTestConfig, ScriptedAgentTestEnv\n\n  config = ScriptedAgentTestConfig()\n  env = ScriptedAgentTestEnv(config)\n\n  env.reset(seed=RANDOM_SEED)\n\n  from tqdm import tqdm\n  for tick in tqdm(range(TEST_HORIZON)):\n    env.step({})\n\n    # events to check\n    log = env.realm.event_log.get_data()    \n    idx = (log[:,2] == tick+1) & (log[:,3] == EventCode.EQUIP_ITEM)\n    if sum(idx):\n      print(log[idx])\n      print()\n\n  print('done')\n  \"\"\"\n"
  },
  {
    "path": "tests/test_memory_usage.py",
    "content": "# pylint: disable=bad-builtin, unused-variable\nimport psutil\n\nimport nmmo\n\ndef test_memory_usage():\n  env = nmmo.Env()\n  process = psutil.Process()\n  print(\"memory\", process.memory_info().rss)\n\nif __name__ == '__main__':\n  test_memory_usage()\n"
  },
  {
    "path": "tests/test_mini_games.py",
    "content": "# pylint: disable=protected-access\nimport unittest\nimport numpy as np\nimport nmmo\nfrom nmmo import minigames as mg\nfrom nmmo.lib import team_helper\n\nTEST_HORIZON = 10\n\n\nclass TestMinigames(unittest.TestCase):\n  def test_mini_games(self):\n    config = nmmo.config.Default()\n    config.set(\"TEAMS\", team_helper.make_teams(config, num_teams=16))\n    env = nmmo.Env(config)\n\n    for game_cls in mg.AVAILABLE_GAMES:\n      game = game_cls(env)\n      env.reset(game=game)\n      game.test(env, TEST_HORIZON)\n\n      # Check if the gym_obs is correctly set, on alive agents\n      for agent_id in env.realm.players:\n        gym_obs = env.obs[agent_id].to_gym()\n        self.assertEqual(gym_obs[\"AgentId\"], agent_id)\n        self.assertEqual(gym_obs[\"CurrentTick\"], env.realm.tick)\n        self.assertTrue(\n          np.array_equal(gym_obs[\"Task\"], env.agent_task_map[agent_id][0].embedding))\n\nif __name__ == \"__main__\":\n  unittest.main()\n"
  },
  {
    "path": "tests/test_performance.py",
    "content": "# pylint: disable=no-member\n# import time\nimport cProfile\nimport io\nimport pstats\nfrom tqdm import tqdm\n\nimport nmmo\nfrom nmmo.core.config import (NPC, AllGameSystems, Combat, Communication,\n                              Equipment, Exchange, Item, Medium, Profession,\n                              Progression, Resource, Small, Terrain)\nfrom nmmo.task.task_api import nmmo_default_task, make_same_task\nfrom nmmo.task.base_predicates import CountEvent, FullyArmed\nfrom nmmo.systems.skill import Melee\nfrom tests.testhelpers import profile_env_step\nfrom scripted import baselines\n\n\n# Test utils\ndef create_and_reset(conf):\n  env = nmmo.Env(conf())\n  env.reset(map_id=1)\n\ndef create_config(base, *systems):\n  systems   = (base, *systems)\n  name      = '_'.join(cls.__name__ for cls in systems)\n\n  conf                    = type(name, systems, {})()\n\n  conf.set(\"TERRAIN_TRAIN_MAPS\", 1)\n  conf.set(\"TERRAIN_EVAL_MAPS\", 1)\n  conf.set(\"IMMORTAL\", True)\n\n  return conf\n\ndef benchmark_config(benchmark, base, nent, *systems):\n  conf = create_config(base, *systems)\n  conf.set(\"PLAYER_N\", nent)\n  conf.set(\"PLAYERS\", [baselines.Random])\n\n  env = nmmo.Env(conf)\n  env.reset()\n\n  benchmark(env.step, actions={})\n\n# Small map tests -- fast with greater coverage for individual game systems\ndef test_small_env_creation(benchmark):\n  benchmark(lambda: nmmo.Env(Small()))\n\ndef test_small_env_reset(benchmark):\n  config = Small()\n  config.set(\"PLAYERS\", [baselines.Random])\n  env = nmmo.Env(config)\n  benchmark(lambda: env.reset(map_id=1))\n\ndef test_fps_base_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1)\n\ndef test_fps_minimal_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression)\n\ndef test_fps_npc_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression, NPC)\n\ndef test_fps_test_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1, Terrain, Resource, Combat, Progression, Item, Exchange)\n\ndef test_fps_no_npc_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1, Terrain, Resource,\n    Combat, Progression, Item, Equipment, Profession, Exchange, Communication)\n\ndef test_fps_all_small_1_pop(benchmark):\n  benchmark_config(benchmark, Small, 1, AllGameSystems)\n\ndef test_fps_base_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1)\n\ndef test_fps_minimal_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat)\n\ndef test_fps_npc_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat, NPC)\n\ndef test_fps_test_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1, Terrain, Resource, Combat, Progression, Item, Exchange)\n\ndef test_fps_no_npc_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1, Terrain, Resource,\n    Combat, Progression, Item, Equipment, Profession, Exchange, Communication)\n\ndef test_fps_all_med_1_pop(benchmark):\n  benchmark_config(benchmark, Medium, 1, AllGameSystems)\n\ndef test_fps_base_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100)\n\ndef test_fps_minimal_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat)\n\ndef test_fps_npc_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat, NPC)\n\ndef test_fps_test_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat, Progression, Item, Exchange)\n\ndef test_fps_no_npc_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100, Terrain, Resource, Combat,\n    Progression, Item, Equipment, Profession, Exchange, Communication)\n\ndef test_fps_all_med_100_pop(benchmark):\n  benchmark_config(benchmark, Medium, 100, AllGameSystems)\n\ndef set_seed_test():\n  random_seed = 5000\n  # conf = create_config(Medium, Terrain, Resource, Combat, NPC, Communication)\n  # conf.set(\"PLAYER_N\", 7)\n  # conf.set(\"PLAYERS\", [baselines.Random])\n  conf = nmmo.config.Default()\n  conf.set(\"TERRAIN_TRAIN_MAPS\", 1)\n  conf.set(\"TERRAIN_EVAL_MAPS\", 1)\n  conf.set(\"IMMORTAL\", True)\n  conf.set(\"NPC_N\", 128)\n  conf.set(\"USE_CYTHON\", True)\n  conf.set(\"PROVIDE_DEATH_FOG_OBS\", True)\n\n  env = nmmo.Env(conf)\n  env.reset(seed=random_seed)\n  for _ in tqdm(range(1024)):\n    env.step({})\n\ndef set_seed_test_complex():\n  tasks = nmmo_default_task(range(1, 129))\n  tasks += make_same_task(CountEvent, range(128),\n                          pred_kwargs={'event': 'EAT_FOOD', 'N': 10})\n  tasks += make_same_task(FullyArmed, range(128),\n                          pred_kwargs={'combat_style': Melee, 'level': 3, 'num_agent': 1})\n  profile_env_step(tasks=tasks)\n\nif __name__ == '__main__':\n  pr = cProfile.Profile()\n  pr.enable()\n  #set_seed_test_complex()\n  set_seed_test()\n  pr.disable()\n  with open('profile.run','a', encoding=\"utf-8\") as f:\n    s = io.StringIO()\n    ps = pstats.Stats(pr,stream=s).sort_stats('tottime')\n    ps.print_stats(100)\n    f.write(s.getvalue())\n\n'''\ndef benchmark_env(benchmark, env, nent):\n  env.config.PLAYER_N = nent\n  env.config.PLAYERS = [nmmo.agent.Random]\n  env.reset()\n\n  benchmark(env.step, actions={})\n# Reuse large maps since we aren't benchmarking the reset function\ndef test_large_env_creation(benchmark):\n  benchmark(lambda: nmmo.Env(Large()))\n\ndef test_large_env_reset(benchmark):\n  env = nmmo.Env(Large())\n  benchmark(lambda: env.reset(idx=1))\n\nLargeMapsRCP = nmmo.Env(create_config(Large, Resource, Terrain, Combat, Progression))\nLargeMapsAll = nmmo.Env(create_config(Large, AllGameSystems))\n\ndef test_fps_large_rcp_1_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsRCP, 1)\n\ndef test_fps_large_rcp_100_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsRCP, 100)\n\ndef test_fps_large_rcp_1000_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsRCP, 1000)\n\ndef test_fps_large_all_1_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsAll, 1)\n\ndef test_fps_large_all_100_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsAll, 100)\n\ndef test_fps_large_all_1000_pop(benchmark):\n  benchmark_env(benchmark, LargeMapsAll, 1000)\n'''\n"
  },
  {
    "path": "tests/test_pettingzoo.py",
    "content": "import unittest\nfrom pettingzoo.test import parallel_api_test\n\nimport nmmo\nfrom scripted import baselines\n\n\nclass TestPettingZoo(unittest.TestCase):\n  def test_pettingzoo_api(self):\n    config = nmmo.config.Default()\n    config.set(\"PLAYERS\", [baselines.Random])\n    config.set(\"HORIZON\", 290)\n    env = nmmo.Env(config)\n    parallel_api_test(env, num_cycles=300)\n\nif __name__ == \"__main__\":\n  unittest.main()\n"
  },
  {
    "path": "tests/test_rollout.py",
    "content": "import nmmo\nfrom scripted.baselines import Random\n\nclass SimpleConfig(nmmo.config.Small, nmmo.config.Combat):\n  pass\n\ndef test_rollout():\n  config = nmmo.config.Default()  # SimpleConfig()\n  config.set(\"PLAYERS\", [Random])\n  config.set(\"USE_CYTHON\", True)\n\n  env = nmmo.Env(config)\n  env.reset()\n  for _ in range(64):\n    env.step({})\n\n  env.reset()\n\nif __name__ == '__main__':\n  test_rollout()\n"
  },
  {
    "path": "tests/testhelpers.py",
    "content": "import logging\nimport unittest\n\nfrom copy import deepcopy\nfrom timeit import timeit\nimport numpy as np\n\nimport nmmo\nfrom nmmo.core import action\nfrom nmmo.systems import item as Item\nfrom nmmo.core.realm import Realm\nfrom nmmo.lib import material as Material\n\nfrom scripted import baselines\n\n# this function can be replaced by assertDictEqual\n# but might be still useful for debugging\ndef actions_are_equal(source_atn, target_atn, debug=True):\n\n  # compare the numbers and player ids\n  player_src = list(source_atn.keys())\n  player_tgt = list(target_atn.keys())\n  if player_src != player_tgt:\n    if debug:\n      logging.error(\"players don't match\")\n    return False\n\n  # for each player, compare the actions\n  for ent_id in player_src:\n    atn1 = source_atn[ent_id]\n    atn2 = target_atn[ent_id]\n\n    if list(atn1.keys()) != list(atn2.keys()):\n      if debug:\n        logging.error(\"action keys don't match. player: %s\", str(ent_id))\n      return False\n\n    for atn, args in atn1.items():\n      if atn2[atn] != args:\n        if debug:\n          logging.error(\"action args don't match. player: %s, action: %s\", str(ent_id), str(atn))\n        return False\n\n  return True\n\n\n# this function CANNOT be replaced by assertDictEqual\ndef observations_are_equal(source_obs, target_obs, debug=True):\n\n  keys_src = list(source_obs.keys())\n  keys_obs = list(target_obs.keys())\n  if keys_src != keys_obs:\n    if debug:\n      #print(\"entities don't match\")\n      logging.error(\"entities don't match\")\n    return False\n\n  for k in keys_src:\n    ent_src = source_obs[k]\n    ent_tgt = target_obs[k]\n    if list(ent_src.keys()) != list(ent_tgt.keys()):\n      if debug:\n        #print(f\"entries don't match. key: {k}\")\n        logging.error(\"entries don't match. key: %s\", str(k))\n      return False\n\n    obj = ent_src.keys()\n    for o in obj:\n\n      # ActionTargets causes a problem here, so skip it\n      if o == \"ActionTargets\":\n        continue\n\n      obj_src = ent_src[o]\n      obj_tgt = ent_tgt[o]\n      if np.sum(obj_src != obj_tgt) > 0:\n        if debug:\n          #print(f\"objects don't match. key: {k}, obj: {o}\")\n          logging.error(\"objects don't match. key: %s, obj: %s\", str(k), str(o))\n        return False\n\n  return True\n\n\ndef player_total(env):\n  return sum(ent.gold.val for ent in env.realm.players.values())\n\n\ndef count_actions(tick, actions):\n  cnt_action = {}\n  for atn in (action.Move, action.Attack, action.Sell, action.Use, action.Give, action.Buy):\n    cnt_action[atn] = 0\n\n  for ent_id in actions:\n    for atn, _ in actions[ent_id].items():\n      if atn in cnt_action:\n        cnt_action[atn] += 1\n      else:\n        cnt_action[atn] = 1\n\n  info_str = f\"Tick: {tick}, acting agents: {len(actions)}, action counts \" + \\\n             f\"move: {cnt_action[action.Move]}, attack: {cnt_action[action.Attack]}, \" + \\\n             f\"sell: {cnt_action[action.Sell]}, use: {cnt_action[action.Move]}, \" + \\\n             f\"give: {cnt_action[action.Give]}, buy: {cnt_action[action.Buy]}\"\n  logging.info(info_str)\n\n  return cnt_action\n\n\nclass ScriptedAgentTestConfig(nmmo.config.Small, nmmo.config.AllGameSystems):\n\n  __test__ = False\n\n  DEATH_FOG_ONSET = 5\n  PLAYERS = [\n    baselines.Fisher, baselines.Herbalist,\n    baselines.Prospector,baselines.Carver, baselines.Alchemist,\n    baselines.Melee, baselines.Range, baselines.Mage]\n\n\n# pylint: disable=abstract-method,duplicate-code\nclass ScriptedAgentTestEnv(nmmo.Env):\n  '''\n  EnvTest step() bypasses some differential treatments for scripted agents\n  To do so, actions of scripted must be serialized using the serialize_actions function above\n  '''\n  __test__ = False\n\n  def __init__(self, config: nmmo.config.Config, seed=None):\n    super().__init__(config=config, seed=seed)\n\n    # all agent must be scripted agents when using ScriptedAgentTestEnv\n    for ent in self.realm.players.values():\n      assert isinstance(ent.agent, baselines.Scripted), 'All agent must be scripted.'\n\n    # this is to cache the actions generated by scripted policies\n    self.actions = {}\n\n  def reset(self, map_id=None, seed=None, options=None):\n    self.actions = {}\n    return super().reset(map_id=map_id, seed=seed, options=options)\n\n  def _compute_scripted_agent_actions(self, actions):\n    assert actions is not None, \"actions must be provided, even it's {}\"\n    # if actions are not provided, generate actions using the scripted policy\n    if actions == {}:\n      for eid, ent in self.realm.players.items():\n        actions[eid] = ent.agent(self.obs[eid])\n\n      # cache the actions for replay before deserialization\n      self.actions = deepcopy(actions)\n\n    # if actions are provided, just run ent.agent() to set the RNG to the same state\n    else:\n      # NOTE: This is a hack to set the random number generator to the same state\n      # since scripted agents also use RNG. Without this, the RNG is in different state,\n      # and the env.step() does not give the same results in the deterministic replay.\n      for eid, ent in self.realm.players.items():\n        ent.agent(self.obs[eid])\n\n    return actions\n\n\ndef change_spawn_pos(realm: Realm, ent_id: int, new_pos):\n  # check if the position is valid\n  assert realm.map.tiles[new_pos].habitable, \"Given pos is not habitable.\"\n  assert realm.entity(ent_id), \"No such entity in the realm\"\n\n  entity = realm.entity(ent_id)\n  old_pos = entity.pos\n  realm.map.tiles[old_pos].remove_entity(ent_id)\n\n  # set to new pos\n  entity.set_pos(*new_pos)\n  entity.spawn_pos = new_pos\n  realm.map.tiles[new_pos].add_entity(entity)\n\ndef provide_item(realm: Realm, ent_id: int,\n                 item: Item.Item, level: int, quantity: int):\n  for _ in range(quantity):\n    realm.players[ent_id].inventory.receive(\n      item(realm, level=level))\n\n\n# pylint: disable=invalid-name,protected-access\nclass ScriptedTestTemplate(unittest.TestCase):\n\n  @classmethod\n  def setUpClass(cls):\n    # only use Combat agents\n    cls.config = ScriptedAgentTestConfig()\n    cls.config.set(\"PROVIDE_ACTION_TARGETS\", True)\n\n    cls.config.set(\"PLAYERS\", [baselines.Melee, baselines.Range, baselines.Mage])\n    cls.config.set(\"PLAYER_N\", 3)\n    #cls.config.IMMORTAL = True\n\n    # set up agents to test ammo use\n    cls.policy = { 1:'Melee', 2:'Range', 3:'Mage' }\n    # 1 cannot hit 3, 2 can hit 1, 3 cannot hit 2\n    cls.spawn_locs = { 1:(17, 17), 2:(17, 19), 3:(21, 21) }\n    cls.ammo = { 1:Item.Whetstone, 2:Item.Arrow, 3:Item.Runes }\n    cls.ammo_quantity = 2\n\n    # items to provide\n    cls.init_gold = 5\n    # TODO: there should not be level 0 items\n    cls.item_level = [0, 3] # 0 can be used, 3 cannot be used\n    cls.item_sig = {}\n\n  def _make_item_sig(self):\n    item_sig = {}\n    for ent_id, ammo in self.ammo.items():\n      item_sig[ent_id] = []\n      for item in [ammo, Item.Top, Item.Gloves, Item.Ration, Item.Potion]:\n        for lvl in self.item_level:\n          item_sig[ent_id].append((item, lvl))\n\n    return item_sig\n\n  def _setup_env(self, random_seed, check_assert=True, remove_immunity=False):\n    \"\"\" set up a new env and perform initial checks \"\"\"\n    config = deepcopy(self.config)\n\n    if remove_immunity:\n      config.set(\"COMBAT_SPAWN_IMMUNITY\", 0)\n\n    env = ScriptedAgentTestEnv(config, seed=random_seed)\n    env.reset()\n\n    # provide money for all\n    for ent_id in env.realm.players:\n      env.realm.players[ent_id].gold.update(self.init_gold)\n\n    # provide items that are in item_sig\n    self.item_sig = self._make_item_sig()\n    for ent_id, items in self.item_sig.items():\n      for item_sig in items:\n        if item_sig[0] == self.ammo[ent_id]:\n          provide_item(env.realm, ent_id, item_sig[0], item_sig[1], self.ammo_quantity)\n        else:\n          provide_item(env.realm, ent_id, item_sig[0], item_sig[1], 1)\n\n    # teleport the players, if provided with specific locations\n    for ent_id, pos in self.spawn_locs.items():\n      change_spawn_pos(env.realm, ent_id, pos)\n\n    # Change entire map to grass to become habitable and non-harvestable\n    MS = env.config.MAP_SIZE\n    for i in range(MS):\n      for j in range(MS):\n        tile = env.realm.map.tiles[i,j]\n        tile.material = Material.Grass\n        tile.material_id.update(Material.Grass.index)\n        tile.state = Material.Grass(env.config)\n\n    env._compute_observations()\n\n    if check_assert:\n      self._check_default_asserts(env)\n\n    return env\n\n  def _check_ent_mask(self, ent_obs, atn, target_id):\n    assert atn in [action.Give, action.GiveGold], \"Invalid action\"\n    gym_obs = ent_obs.to_gym()\n    mask = gym_obs[\"ActionTargets\"][atn.__name__][\"Target\"][:ent_obs.entities.len] > 0\n\n    return target_id in ent_obs.entities.ids[mask]\n\n  def _check_inv_mask(self, ent_obs, atn, item_sig):\n    assert atn in [action.Destroy, action.Give, action.Sell, action.Use], \"Invalid action\"\n    gym_obs = ent_obs.to_gym()\n    mask = gym_obs[\"ActionTargets\"][atn.__name__][\"InventoryItem\"][:ent_obs.inventory.len] > 0\n    inv_idx = ent_obs.inventory.sig(*item_sig)\n\n    return ent_obs.inventory.id(inv_idx) in ent_obs.inventory.ids[mask]\n\n  def _check_mkt_mask(self, ent_obs, item_id):\n    gym_obs = ent_obs.to_gym()\n    mask = gym_obs[\"ActionTargets\"][\"Buy\"][\"MarketItem\"][:ent_obs.market.len] > 0\n\n    return item_id in ent_obs.market.ids[mask]\n\n  def _check_default_asserts(self, env):\n    \"\"\" The below asserts are based on the hardcoded values in setUpClass()\n        This should not run when different values were used\n    \"\"\"\n    # check if the agents are in specified positions\n    for ent_id, pos in self.spawn_locs.items():\n      self.assertEqual(env.realm.players[ent_id].pos, pos)\n\n    for ent_id, sig_list in self.item_sig.items():\n      # ammo instances are in the datastore and global item registry (realm)\n      inventory = env.obs[ent_id].inventory\n      self.assertTrue(inventory.len == len(sig_list))\n      for inv_idx in range(inventory.len):\n        item_id = inventory.id(inv_idx)\n        self.assertTrue(Item.ItemState.Query.by_id(env.realm.datastore, item_id) is not None)\n        self.assertTrue(item_id in env.realm.items)\n\n      for lvl in self.item_level:\n        inv_idx = inventory.sig(self.ammo[ent_id], lvl)\n        self.assertTrue(inv_idx is not None)\n        self.assertEqual(self.ammo_quantity, # provided 2 ammos\n          Item.ItemState.parse_array(inventory.values[inv_idx]).quantity)\n\n      # check ActionTargets\n      ent_obs = env.obs[ent_id]\n\n      if env.config.ITEM_SYSTEM_ENABLED:\n        # USE InventoryItem mask\n        for item_sig in sig_list:\n          if item_sig[1] == 0:\n            # items that can be used\n            self.assertTrue(self._check_inv_mask(ent_obs, action.Use, item_sig))\n          else:\n            # items that are too high to use\n            self.assertFalse(self._check_inv_mask(ent_obs, action.Use, item_sig))\n\n      if env.config.EXCHANGE_SYSTEM_ENABLED:\n        # SELL InventoryItem mask\n        for item_sig in sig_list:\n          # the agent can sell anything now\n          self.assertTrue(self._check_inv_mask(ent_obs, action.Sell, item_sig))\n\n        # BUY MarketItem mask -- there is nothing on the market, so mask should be all 0\n        self.assertTrue(len(env.obs[ent_id].market.ids) == 0)\n\n  def _check_assert_make_action(self, env, atn, test_cond):\n    assert atn in [action.Give, action.GiveGold, action.Buy], \"Invalid action\"\n    actions = {}\n    for ent_id, cond in test_cond.items():\n      ent_obs = env.obs[ent_id]\n\n      if atn in [action.Give, action.GiveGold]:\n        # self should be always masked\n        self.assertFalse(self._check_ent_mask(ent_obs, atn, ent_id))\n\n        # check if the target is masked as expected\n        self.assertEqual(\n          cond['ent_mask'],\n          self._check_ent_mask(ent_obs, atn, cond['tgt_id']),\n          f\"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['tgt_id']}\"\n        )\n\n      if atn in [action.Give]:\n        self.assertEqual(\n          cond['inv_mask'],\n          self._check_inv_mask(ent_obs, atn, cond['item_sig']),\n          f\"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['item_sig']}\"\n        )\n\n      if atn in [action.Buy]:\n        self.assertEqual(\n          cond['mkt_mask'],\n          self._check_mkt_mask(ent_obs, cond['item_id']),\n          f\"ent_id: {ent_id}, atn: {ent_id}, tgt_id: {cond['item_id']}\"\n        )\n\n      # append the actions\n      if atn == action.Give:\n        actions[ent_id] = { action.Give: {\n          action.InventoryItem: env.obs[ent_id].inventory.sig(*cond['item_sig']),\n          action.Target: env.obs[ent_id].entities.index(cond['tgt_id']) } }\n\n      elif atn == action.GiveGold:\n        actions[ent_id] = { action.GiveGold:\n          { action.Target: env.obs[ent_id].entities.index(cond['tgt_id']),\n           action.Price: action.Price.index(cond['gold']) } }\n\n      elif atn == action.Buy:\n        mkt_idx = ent_obs.market.index(cond['item_id'])\n        actions[ent_id] = { action.Buy: { action.MarketItem: mkt_idx } }\n\n    return actions\n\n# pylint: disable=unnecessary-lambda,bad-builtin\ndef profile_env_step(action_target=True, tasks=None, condition=None):\n  config = nmmo.config.Default()\n  config.set(\"PLAYERS\", [baselines.Sleeper])  # the scripted agents doing nothing\n  config.set(\"IMMORTAL\", True)  # otherwise the agents will die\n  config.set(\"PROVIDE_ACTION_TARGETS\", action_target)\n  env = nmmo.Env(config, seed=0)\n  if tasks is None:\n    tasks = []\n  env.reset(seed=0, make_task_fn=lambda: tasks)\n  for _ in range(3):\n    env.step({})\n\n  env._compute_observations()\n  obs = deepcopy(env.obs)\n\n  test_func = [\n    ('env.step({}):', lambda: env.step({})),\n    ('env.realm.step():', lambda: env.realm.step({})),\n    ('env._compute_observations():', lambda: env._compute_observations()),\n    ('obs.to_gym(), ActionTarget:', lambda: {a: o.to_gym() for a,o in obs.items()}),\n    ('env._compute_rewards():', lambda: env._compute_rewards())\n  ]\n\n  if condition:\n    print('=== Test condition:', condition, '===')\n  for name, func in test_func:\n    print(' -', name, timeit(func, number=100, globals=globals()))\n"
  },
  {
    "path": "utils/git-pr.sh",
    "content": "#!/bin/bash\nMASTER_BRANCH=\"2.0\"\n\n# check if in master branch\ncurrent_branch=$(git rev-parse --abbrev-ref HEAD)\nif [ \"$current_branch\" == MASTER_BRANCH ]; then\n  echo \"Please run 'git pr' from a topic branch.\"\n  exit 1\nfi\n\n# check if there are any uncommitted changes\ngit_status=$(git status --porcelain)\n\nif [ -n \"$git_status\" ]; then\n  read -p \"Uncommitted changes found. Commit before running 'git pr'? (y/n) \" ans\n  if [ \"$ans\" = \"y\" ]; then\n    git commit -m -a \"Automatic commit for git-pr\"\n  else\n    echo \"Please commit or stash changes before running 'git pr'.\"\n    exit 1\n  fi\nfi\n\n# Merging master\necho \"Merging master...\"\ngit merge origin/$MASTER_BRANCH\n\n# Checking pylint, xcxc, pytest without touching git\nPRE_GIT_CHECK=$(find . -name pre-git-check.sh)\nif test -f \"$PRE_GIT_CHECK\"; then\n  $PRE_GIT_CHECK\n  if [ $? -ne 0 ]; then\n    echo \"pre-git-check.sh failed. Exiting.\"\n    exit 1\n  fi\nelse\n  echo \"Missing pre-git-check.sh. Exiting.\"\n  exit 1\nfi\n\n# create a new branch from current branch and reset to master\necho \"Creating and switching to new topic branch...\"\ngit_user=$(git config user.email | cut -d'@' -f1)\nbranch_name=\"${git_user}-git-pr-$RANDOM-$RANDOM\"\ngit checkout -b $branch_name\ngit reset --soft origin/$MASTER_BRANCH\n\n# Verify that a commit message was added\necho \"Verifying commit message...\"\nif ! git commit -a ; then\n    echo \"Commit message is empty. Exiting.\"\n    exit 1\nfi\n\n# Push the topic branch to origin\necho \"Pushing topic branch to origin...\"\ngit push -u origin $branch_name\n\n# Generate a Github pull request (just the url, not actually making a PR)\necho \"Generating Github pull request...\"\npull_request_url=\"https://github.com/CarperAI/nmmo-environment/compare/$MASTER_BRANCH...CarperAI:nmmo-environment:$branch_name?expand=1\"\n\necho \"Pull request URL: $pull_request_url\"\n"
  },
  {
    "path": "utils/pre-git-check.sh",
    "content": "#!/bin/bash\n\necho\necho \"Checking pylint, xcxc, pytest without touching git\"\necho\n\n# Check the number of physical cores only\nif command -v lscpu &> /dev/null\nthen\n    # lscpu is available\n    cores=$(lscpu -b -p=Core,Socket | grep -v '^#' | sort -u | wc -l)\nelse\n    # lscpu is not available, use sysctl instead\n    cores=$(sysctl -n hw.physicalcpu)\nfi\n\n# Run linter\necho \"--------------------------------------------------------------------\"\necho \"Running linter...\"\nfiles=$(git ls-files -m -o --exclude-standard '*.py')\nfor file in $files; do\n  if test -e $file; then\n    echo $file\n    if ! pylint --score=no --fail-under=10 $file; then\n      echo \"Lint failed. Exiting.\"\n      exit 1\n    fi\n  fi\ndone\n\nif ! pylint --recursive=y nmmo tests; then\n  echo \"Lint failed. Exiting.\"\n  exit 1\nfi\n\n# Check if there are any \"xcxc\" strings in the code\necho \"--------------------------------------------------------------------\"\necho \"Looking for xcxc...\"\nfiles=$(find . -name '*.py')\nfor file in $files; do\n    if grep -q 'xcxc' $file; then\n        echo \"Found xcxc in $file!\" >&2\n        read -p \"Do you like to stop here? (y/n) \" ans\n        if [ \"$ans\" = \"y\" ]; then\n            exit 1\n        fi\n    fi\ndone\n\n# Run unit tests\necho\necho \"--------------------------------------------------------------------\"\necho \"Running unit tests...\"\nif ! pytest; then\n    echo \"Unit tests failed. Exiting.\"\n    exit 1\nfi\n\necho\necho \"Pre-git checks look good!\"\necho\n"
  },
  {
    "path": "utils/run-perf-tests.sh",
    "content": "pytest --benchmark-columns=ops,rounds,median,mean,stddev,min,max,iterations --benchmark-max-time=5 --benchmark-min-rounds=500 \\\n       --benchmark-warmup=on --benchmark-warmup-iterations=300 tests/test_performance.py\n"
  }
]