Skip to content

Commit

Permalink
added destroy, give-gold, and tests to check actions/masks correctness
Browse files Browse the repository at this point in the history
  • Loading branch information
kywch committed Mar 3, 2023
1 parent 39098e6 commit bf27407
Show file tree
Hide file tree
Showing 14 changed files with 1,049 additions and 224 deletions.
3 changes: 3 additions & 0 deletions nmmo/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ class Item:
ITEM_INVENTORY_CAPACITY = 12
'''Number of inventory spaces'''

ITEM_GIVE_TO_FRIENDLY = True
'''Whether agents with the same population index can give gold/item to each other'''

@property
def INVENTORY_N_OBS(self):
'''Number of distinct item observations'''
Expand Down
6 changes: 2 additions & 4 deletions nmmo/core/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ def __init__(self,
self.obs = None

self.possible_agents = list(range(1, config.PLAYER_N + 1))
self._dead_agents = set()
self._dead_agents = OrderedSet()
self.scripted_agents = OrderedSet()

# pylint: disable=method-cache-max-size-none
@functools.lru_cache(maxsize=None)
# CHECK ME: Do we need the agent parameter here?
def observation_space(self, agent: int):
'''Neural MMO Observation Space
Expand Down Expand Up @@ -79,7 +78,6 @@ def _init_random(self, seed):
random.seed(seed)

@functools.lru_cache(maxsize=None)
# CHECK ME: Do we need the agent parameter here?
def action_space(self, agent):
'''Neural MMO Action Space
Expand Down Expand Up @@ -133,7 +131,7 @@ def reset(self, map_id=None, seed=None, options=None):

self._init_random(seed)
self.realm.reset(map_id)
self._dead_agents = set()
self._dead_agents = OrderedSet()

# check if there are scripted agents
for eid, ent in self.realm.players.items():
Expand Down
72 changes: 59 additions & 13 deletions nmmo/core/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ def __init__(self, values, id_col):
self.inv_type = self.values[:,ItemState.State.attr_name_to_col["type_id"]]
self.inv_level = self.values[:,ItemState.State.attr_name_to_col["level"]]

def sig(self, itm_type, level):
if (itm_type in self.inv_type) and (level in self.inv_level):
return np.nonzero((self.inv_type == itm_type) & (self.inv_level == level))[0][0]

return None
def sig(self, item: item_system.Item, level: int):
idx = np.nonzero((self.inv_type == item.ITEM_TYPE_ID) & (self.inv_level == level))[0]
return idx[0] if len(idx) else None


class Observation:
Expand Down Expand Up @@ -148,15 +146,26 @@ def _make_action_targets(self):
masks[action.Use] = {
action.InventoryItem: self._make_use_mask()
}
masks[action.Give] = {
action.InventoryItem: self._make_sell_mask(),
action.Target: self._make_give_target_mask()
}
masks[action.Destroy] = {
action.InventoryItem: self._make_destroy_item_mask()
}

if self.config.EXCHANGE_SYSTEM_ENABLED:
masks[action.Sell] = {
action.InventoryItem: self._make_sell_mask(),
action.Price: None # allow any integer
action.Price: None # should allow any integer > 0
}
masks[action.Buy] = {
action.MarketItem: self._make_buy_mask()
}
masks[action.GiveGold] = {
action.Target: self._make_give_target_mask(),
action.Price: None # reusing Price, allow any integer > 0
}

if self.config.COMMUNICATION_SYSTEM_ENABLED:
masks[action.Comm] = {
Expand Down Expand Up @@ -189,20 +198,28 @@ def _make_attack_mask(self):
EntityState.State.attr_name_to_col["col"]]]
within_range = utils.linf(entities_pos, (agent.row, agent.col)) <= attack_range

immunity = self.config.COMBAT_SPAWN_IMMUNITY
if 0 < immunity < agent.time_alive:
# ids > 0 equals entity.is_player
spawn_immunity = (self.entities.ids > 0) & \
(self.entities.values[:,EntityState.State.attr_name_to_col["time_alive"]] < immunity)
else:
spawn_immunity = np.ones(self.entities.len, dtype=np.int8)

if not self.config.COMBAT_FRIENDLY_FIRE:
population = self.entities.values[:,EntityState.State.attr_name_to_col["population_id"]]
no_friendly_fire = population != agent.population_id # this automatically masks self
else:
# allow friendly fire but self
# allow friendly fire but no self shooting
no_friendly_fire = np.ones(self.entities.len, dtype=np.int8)
no_friendly_fire[self.entities.index(agent.id)] = 0 # mask self

return np.concatenate([within_range & no_friendly_fire,
return np.concatenate([within_range & no_friendly_fire & spawn_immunity,
np.zeros(self.config.PLAYER_N_OBS - self.entities.len, dtype=np.int8)])

def _make_use_mask(self):
# empty inventory -- nothing to use
if self.inventory.len == 0:
if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0):
return np.zeros(self.config.INVENTORY_N_OBS, dtype=np.int8)

item_skill = self._item_skill()
Expand Down Expand Up @@ -247,9 +264,35 @@ def _item_skill(self):
item_system.Poultice.ITEM_TYPE_ID: level
}

def _make_destroy_item_mask(self):
# empty inventory -- nothing to destroy
if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0):
return np.zeros(self.config.INVENTORY_N_OBS, dtype=np.int8)

not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col["equipped"]] == 0

# not equipped items in the inventory can be destroyed
return np.concatenate([not_equipped,
np.zeros(self.config.INVENTORY_N_OBS - self.inventory.len, dtype=np.int8)])

def _make_give_target_mask(self):
# empty inventory -- nothing to give
if not (self.config.ITEM_SYSTEM_ENABLED and self.inventory.len > 0):
return np.zeros(self.config.PLAYER_N_OBS, dtype=np.int8)

agent = self.agent()
entities_pos = self.entities.values[:, [EntityState.State.attr_name_to_col["row"],
EntityState.State.attr_name_to_col["col"]]]
same_tile = utils.linf(entities_pos, (agent.row, agent.col)) == 0
same_team_not_me = (self.entities.ids != agent.id) & (agent.population_id == \
self.entities.values[:, EntityState.State.attr_name_to_col["population_id"]])

return np.concatenate([same_tile & same_team_not_me,
np.zeros(self.config.PLAYER_N_OBS - self.entities.len, dtype=np.int8)])

def _make_sell_mask(self):
# empty inventory -- nothing to sell
if self.inventory.len == 0:
if not (self.config.EXCHANGE_SYSTEM_ENABLED and self.inventory.len > 0):
return np.zeros(self.config.INVENTORY_N_OBS, dtype=np.int8)

not_equipped = self.inventory.values[:,ItemState.State.attr_name_to_col["equipped"]] == 0
Expand All @@ -259,10 +302,14 @@ def _make_sell_mask(self):
np.zeros(self.config.INVENTORY_N_OBS - self.inventory.len, dtype=np.int8)])

def _make_buy_mask(self):
if not self.config.EXCHANGE_SYSTEM_ENABLED:
return np.zeros(self.config.MARKET_N_OBS, dtype=np.int8)

market_flt = np.ones(self.market.len, dtype=np.int8)
full_inventory = self.inventory.len >= self.config.ITEM_INVENTORY_CAPACITY

# if the inventory is full, one can only buy existing ammo stack
# otherwise, one can buy anything owned by other, having enough money
if full_inventory:
exist_ammo_listings = self._existing_ammo_listings()
if not np.any(exist_ammo_listings):
Expand All @@ -273,9 +320,8 @@ def _make_buy_mask(self):
market_items = self.market.values
enough_gold = market_items[:,ItemState.State.attr_name_to_col["listed_price"]] <= agent.gold
not_mine = market_items[:,ItemState.State.attr_name_to_col["owner_id"]] != self.agent_id
not_equipped = market_items[:,ItemState.State.attr_name_to_col["equipped"]] == 0

return np.concatenate([market_flt & enough_gold & not_mine & not_equipped,
return np.concatenate([market_flt & enough_gold & not_mine,
np.zeros(self.config.MARKET_N_OBS - self.market.len, dtype=np.int8)])

def _existing_ammo_listings(self):
Expand All @@ -295,7 +341,7 @@ def _existing_ammo_listings(self):
if exist_ammo.shape[0] == 0:
return np.zeros(self.market.len, dtype=np.int8)

# search the existing ammo stack from the market
# search the existing ammo stack from the market that's not mine
type_flt = np.tile( np.array(exist_ammo[:,sig_col[0]]), (self.market.len,1))
level_flt = np.tile( np.array(exist_ammo[:,sig_col[1]]), (self.market.len,1))
item_type = np.tile( np.transpose(np.atleast_2d(self.market.values[:,sig_col[0]])),
Expand Down
23 changes: 18 additions & 5 deletions nmmo/core/realm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from nmmo.core.tile import TileState
from nmmo.entity.entity import EntityState
from nmmo.entity.entity_manager import NPCManager, PlayerManager
from nmmo.io.action import Action
from nmmo.io.action import Action, Buy
from nmmo.lib.datastore.numpy_datastore import NumpyDatastore
from nmmo.systems.exchange import Exchange
from nmmo.systems.item import Item, ItemState
Expand Down Expand Up @@ -150,14 +150,27 @@ def step(self, actions):
self.players.update(actions)
self.npcs.update(npc_actions)

# Execute actions
# Execute actions -- CHECK ME the below priority
# - 10: Use - equip ammo, restore HP, etc.
# - 20: Buy - exchange while sellers, items, buyers are all intact
# - 30: Give, GiveGold - transfer while both are alive and at the same tile
# - 40: Destroy - use with SELL/GIVE, if not gone, destroy and recover space
# - 50: Attack
# - 60: Move
# - 70: Sell - to guarantee the listed items are available to buy
# - 99: Comm
for priority in sorted(merged):
# TODO: we should be randomizing these, otherwise the lower ID agents
# will always go first.
ent_id, (atn, args) = merged[priority][0]
# will always go first. --> ONLY SHUFFLE BUY
if priority == Buy.priority:
np.random.shuffle(merged[priority])

# CHECK ME: do we need this line?
# ent_id, (atn, args) = merged[priority][0]
for ent_id, (atn, args) in merged[priority]:
ent = self.entity(ent_id)
atn.call(self, ent, *args)
if ent.alive:
atn.call(self, ent, *args)

dead = self.players.cull()
self.npcs.cull()
Expand Down
8 changes: 6 additions & 2 deletions nmmo/entity/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,16 @@ def receive_damage(self, source, dmg):
if self.alive:
return True

# if the entity is dead, unlist its items regardless of looting
# at this point, self is dead
if source:
source.history.player_kills += 1

# if self is dead, unlist its items from the market regardless of looting
if self.config.EXCHANGE_SYSTEM_ENABLED:
for item in list(self.inventory.items):
self.realm.exchange.unlist_item(item)

# if the entity is dead but no one can loot, destroy its items
# if self is dead but no one can loot, destroy its items
if source is None or not source.is_player: # nobody or npcs cannot loot
if self.config.ITEM_SYSTEM_ENABLED:
for item in list(self.inventory.items):
Expand Down
20 changes: 8 additions & 12 deletions nmmo/entity/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,28 @@ def receive_damage(self, source, dmg):
if self.immortal:
return False

# super().receive_damage returns True if self is alive after taking dmg
if super().receive_damage(source, dmg):
return True

if not self.config.ITEM_SYSTEM_ENABLED:
return False

# if self is killed, source receive gold & inventory items
source.gold.increment(self.gold.val)
self.gold.update(0)
# starting from here, source receive gold & inventory items
if self.config.EXCHANGE_SYSTEM_ENABLED:
source.gold.increment(self.gold.val)
self.gold.update(0)

# TODO(kywch): make source receive the highest-level items first
# because source cannot take it if the inventory is full
# Also, destroy the remaining items if the source cannot take those
for item in list(self.inventory.items):
if not item.quantity.val:
item.datastore_record.delete()
continue

self.inventory.remove(item)
source.inventory.receive(item)

if not super().receive_damage(source, dmg):
if source:
source.history.player_kills += 1
return False
# if source doesn't have space, inventory.receive() destroys the item
source.inventory.receive(item)

# CHECK ME: this is an empty function. do we still need this?
self.skills.receive_damage(dmg)
return False

Expand Down
Loading

0 comments on commit bf27407

Please sign in to comment.