4
\$\begingroup\$

I am trying to create a highly customizable "battle simulator". The default setup is a predator/prey relationship (no births); however, I am trying to create code such that it's easy to add and adjust various parameters in order to accomodate (relatively) distinct agent behaviour. I would like to build on the complexity of this customization once I've optimized my overall approach.

I think I've commented pretty thoroughly in the code, but the gist is this:

  • Decide on sizes of grid and your two populations
  • Create a suitable numpy "agent array", P. Each column of P is an individual agent, and each row is an attribute - speed, awareness, vigor, (x,y) grid locations, etc.
  • Seed individual agents with (weighted per group) starting attribute values, including randomized x and y grid locations for each agent.
  • Create an environment grid of zeros, and seed ones (prey vectors have this as their first attribute) and twos (predators have this) in each agent's starting location.

I've been using a colormap to plot and animate the grid, so white dots chase and eat orange dots on a black field. Nothing too sophisticated, kind of like Pacman on steroids. Plenty of random, stupid movement, but in the long run everyone does more or less what they should do, and doesn't go out of bounds. Here are two example gifs since I'm not including the full code:

https://i.imgur.com/OliUR1S.gif

https://i.imgur.com/r9HUTv0.gif (This one has an infectuous component - zombie!)

The entire code is ~900 lines, so for now I'm only posting the starting grid. Seems like that should illustrate my approach well enough but I can post more if needed.

To be more specific in my question(s): I'm wondering if this numpy "agent array"/enviro_grid of zeros is the best way to approach this. I doubt it is, but all the Python I know is from a Sci. Computing class, so I just rolled with the numpy I knew. The rest of the code (not included) is a LOT of nested loops. Each agent is an individual with distinct attributes and location, so checking surroundings, handling behaviour, changing grid ones/twos to zeros and back again, checking surroundings after movement and handling probabilty rolls for adjacent interactions, etc, gets pretty messy. So before I get too deep it seemed prudent to do a sanity check, maybe see if I should do a complete overhaul using some better, more Pythonic techniques.

Here is the code sample (200 lines with comments):

import numpy as np
import random
import pylab as plt

# set grid array size
grid_m = int(100)
grid_n = int(100)

# set relative pred/prey percentages here
# prey density in relation to grid size, pred. density in relation to prey size
prey_dens = 0.02
predator_dens = 0.25
prey_amnt = int(np.rint((prey_dens) * (grid_m) * (grid_n)))
predator_amnt = int(np.rint((predator_dens) * (prey_amnt)))
# ^^had to do int twice to avoid float error later, even though used np.rint??
total_agents = prey_amnt + predator_amnt

##alternatively, just input some hard population numbers here
# prey_amnt = 200
# predator_amnt = 5


# __________commonly used adjustable variables, in no particular order_________
agent_properties = int(8)  # total number of attributes, listed below
prey_max_life = int(100)  # prey max life in years; 100% death next iteration
predator_max_life = int(40)  # pred. max life in years; 100% death next iter.
pred_strength = 2  # we 'roll' against this number later
pred_infection_rate = 0# for "zombie", or infectuous disease modeling
prey_eta = 95  # beta, eta are for two-parameter weibull functions...
prey_beta = 10.6  # ...which describe 'hazard functions' for agents
predator_eta = 38
predator_beta = 10.6
prey_awares_low = 3  # = grid space over which one can detect other agents
prey_awares_midd = 4
prey_awares_high = 5
pred_awares_low = 30
pred_awares_midd = 60
pred_awares_high = 90
pred_awares_infirm = 10
prey_speed_low = 1  # = grid space over which one can move per iteration
prey_speed_midd = 2
prey_speed_high = 3
prey_speed_infirm = 0
pred_speed_low = 4
pred_speed_midd = 5
pred_speed_high = 6
# _________try to keep heavily-trafficked customization above this line__________


# _____________________________________
# Agent array P.  Each column is an independent agent, with associated
#  properties as follows:
# P[0, i] = DESIGNATION 1 or 2; Predator or prey; army A or army B; etc
# P[1, i] = AGE in years
# P[2, i] = VIGOR (affected by age; 0 < V < 1 and used to "roll"
# against for probabilities in pred/prey interactions)
# P[3, i] = AWARENESS (affected by VIGOR; how many grid spaces away one can 
# detect nearby agents)
# P[4, i] = SPEED (affected by VIGOR; how many grid spaces over which one can
#  move per iteration)
# P[5, i] = MORTALITY HAZARD FUNCTION (prob of dying "for no reason").
#  Time-dependent; higher with age unti1 = 1 at max_life
# P[6, i] = x_location on grid plane
# P[7, i] = y_location on grid plane
# ______________________________________

P_m = agent_properties  # size, rows
P_n = total_agents  # size, columns
P = np.zeros([P_m, P_n])  # create array and seed with zeros

# Start seeding agent array______________________________________________
# P[0, j]: DESIGNATION___________
P[0, 0:prey_amnt] = int(1)
P[0, prey_amnt:P_n] = int(2)

# P[1, j]: AGE____________________________________
# in years
# randomize starting ages - loosely based off US demography stats
# 20% 1-14 years old
# 70% 15-70 years old
# 10% 70-100 years old
# Similarly for predators, but scaled for predator_max_life
# breaking this up into several steps just because long lines, and flexibility

# Prey_______________________
# round the density numbers to nearest int; ceiling on last in case ~0
prey_young_round = int(0.2 * prey_amnt)
prey_midd_round = int(0.7 * prey_amnt)
prey_old_round = int(np.ceil(0.1 * prey_amnt))

# create weighted array of ages
starting_age_prey_young = \
    np.random.randint(1, 14, prey_young_round)
starting_age_prey_midd = \
    np.random.randint(15, 70, prey_midd_round)
starting_age_prey_old = \
    np.random.randint(71, prey_max_life, prey_old_round)

# finally, seed into agent-matrix
P[1, 0:prey_young_round] = starting_age_prey_young
P[1, (prey_young_round):(prey_young_round + prey_midd_round)] = \
    starting_age_prey_midd
P[1, (prey_amnt - prey_old_round):prey_amnt] = starting_age_prey_old

# Predators________________________
# round
predator_young_round = int(0.2 * predator_amnt)
predator_midd_round = int(0.7 * predator_amnt)
predator_old_round = int(np.ceil(0.1 * predator_amnt))

# generate age array
starting_age_predator_young = \
    np.random.randint(1, 6, predator_young_round)
starting_age_predator_midd = \
    np.random.randint(7, 28, predator_midd_round)
starting_age_predator_old = \
    np.random.randint(29, predator_max_life, predator_old_round)

# finally seed
P[1, prey_amnt:(prey_amnt + predator_young_round)] = \
    starting_age_predator_young
P[1, (prey_amnt + predator_young_round):(prey_amnt + predator_young_round +
    predator_midd_round)] = starting_age_predator_midd
P[1, (total_agents - predator_old_round):total_agents] = \
    starting_age_predator_old

# P[2, j]: VIGOR_________________________________________
## dependent on AGE; uses a two-parameter weibull (backwards s-shape)
# prey____________________________
for j in range(prey_amnt):
    P[2, j] = np.exp(-(P[1, j] / prey_eta) ** prey_beta)
# predator__________________________
for j in range(prey_amnt, P_n):
    P[2, j] = np.exp(-(P[1, j] / predator_eta) ** predator_beta)

# P[3, j]: AWARENESS_____________________________________
# ability to sense nearby stuff; dependent on VIGOR
# prey__________________
for j in range(prey_amnt):
    if 0 <= P[2, j] < 0.33:
        P[3, j] = prey_awares_low
    elif .33 <= P[2, j] < .66:
        P[3, j] = prey_awares_midd
    elif .66 <= P[2, j] <= 1:
        P[3, j] = prey_awares_high

# predator__________________
for j in range(prey_amnt, P_n):
    if 1 <= P[1, j] <= 2 or 38 <= P[1, j]:
        P[3, j] = pred_awares_infirm
    else:
        if 0 <= P[2, j] < 0.33:
            P[3, j] = pred_awares_low
        elif .33 <= P[2, j] < .66:
            P[3, j] = pred_awares_midd
        elif .66 <= P[2, j] <= 1:
            P[3, j] = pred_awares_high

# P[4, j]: SPEED_______________________________________
# dependent on VIGOR (but also on AGE: < 5 or > 90 cant move at all)
# prey _________________
for j in range(prey_amnt):
    if 1 <= P[1, j] <= 5 or 90 <= P[1, j]:
        P[4, j] = prey_speed_infirm
    else:
        if 0 <= P[2, j] < 0.33:
            P[4, j] = prey_speed_low
        elif .33 <= P[2, j] < .66:
            P[4, j] = prey_speed_midd
        elif .66 <= P[2, j] <= 1:
            P[4, j] = prey_speed_high

# predator___________________
# all of them can move for now
for j in range(prey_amnt, P_n):
    if 0 <= P[2, j] < 0.33:
        P[4, j] = pred_speed_low
    elif .33 <= P[2, j] < .66:
        P[4, j] = pred_speed_midd
    elif .66 <= P[2, j] <= 1:
        P[4, j] = pred_speed_high

# P[5, :]HAZARD FUNCTION____________________________________________
# instantaneous prob of failure (death) "for no reason"; dependent on AGE
# should climb sharply in old age and reach 1 at ~max_life
# prey_____________________
for j in range(prey_amnt):
    P[5, j] = (prey_beta * (P[1, j]) ** (prey_beta - 1)) \
              / (prey_eta ** prey_beta)
# predator
for j in range(prey_amnt, P_n):
    P[5, j] = (predator_beta * (P[1, j]) ** (predator_beta - 1)) \
              / (predator_eta ** predator_beta)


# P[6:7, :]:GRID LOCATIONS______________________________________________
# (x, y); randomize starting location indices for each agent
indices = [(m, n) for m in range(grid_m) for n in range(grid_n)]
random_indices = random.sample(indices, P.shape[1])

# and seed them into agent vectors
for j in range(P_n):
    P[6, j] = int(random_indices[j][0])  # x location
    P[7, j] = int(random_indices[j][1])  # y location


# ENVIRONMENT________________________________________________
# Create grid
enviro_grid = np.zeros([grid_m, grid_n])

# place players in their locations
for j in range(P_n):
    enviro_grid[int(P[6, j]), int(P[7, j])] = int(P[0, j])

# now we make em dance
# Here there be lots and lots and LOTS of loops describing agent checks
# and behaviour.  Displayed as an animation of white dots which chase and 
# eat orange dots.  As shown in the above gifs.  
# Also note the reason there are int()'s everywhere is because I kept getting
# errors/warnings about "expecting int but got float!" and I couldn't take
# it anymore.  Couldn't pinpoint the exact source(s), so one by one I just turned
# every integer into an explicit int() until the warnings went away.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.