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/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) # and seed them into agent vectors for j in range(P_n): P[6, j] = int(random_indices[j]) # x location P[7, j] = int(random_indices[j]) # 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.