Assignment 9: Capulets and Montegues & Visualizations

Capulets and Montegues is a game in which several rival families struggle to control a city. A wealth of additional options allows the game to be transformed into an interactive art piece, too, though – all within a few key presses! Oof though; this assignment turned to be much bigger than I expected, but I am very pleased with the result.

The basic game setup looks like this:

Settings 110112113 (more about those later)

There are 10 groups of people (class Person in the code) in the city, identified by the color of their clothes, starting out from random positions. They have a few desires in life – they do not want to be too close to the edge of the map, they do not want to be packed too closely and they do not want to be near to their enemies, but they do want to get as close to their friends as they can while working their way towards the center of the city. The game makes them move in accordance with these motivations according to a set of weighting variables.

The region each Person controls is identified by a correspondingly-colored Voronoi cell. The most important district of the city is the central one: the citadel. Holding it gives points to the closest Person around (first number in the top-left stats rectangle); that is why the color of the central region is more saturated and why it is encircled by a wall.

The colored lines between people show interactions: enemies hit each other while friends give each other a small health boost (he health of each Person is shown by the little number above their head). That is why people tend to hang out in groups in this game! For it to be easier to defend, the citadel gives a small extra health boost to current owner.

To prevent any one group from snowballing out of control, the game has maximum and minimum limits for the number of group members (the second number in the stats rectangle). A balancing algorithm reduces the allowed maximum for the group that is the current leader in score (“Leading” in the fourth column), and reduces it even further for the current holder of the center. On the other side, the current score loser is allowed to have more members. When a group has more members than its allowed quota (“Over” in the fifth column), all members of the offending group suffer severe health decreases.

As people die (from crossing the population quota, succumbing to enemy attacks, being squeezed too close to the edge of the screen or to other people), the game respawns them at random position, potentially as members of another group. If there is a group with fewer members than what its minimum limit dictates (“Under”), it is selected as the recipient of this reborn Person. If all groups are at or above their minimum limits, the algorithm selects the group with the lowest number of points that is not yet maxed out; ties are broken in favor of the group with the fewest members (“Next”).

There are a few ways to interact with the game, although it can evolve on its own in pleasant ways. The most straightforward one first: pressing the spacebar respawns all people on the map.

The most fun way to interact with the game, though, is to play favorites – when a key is pressed that corresponds to a group (first column in stats), a member of that group is spawned at the mouse cursor’s position (replacing a randomly chosen existing person). Spamming group members at the center can quickly propel that group to score leader status! (Even though the members may quickly despawn if their numbers are over the group’s maximum limit.)

In addition, several keys are used to adjust parameters of the simulation. The -/+ keys change the speed of the simulation (lower right corner of the screen). </> keys change the number of people in the simulation; meanwhile, the [/] keys adjust the number of group/colors in the simulation (between 1 and 10).

I am proud that I was able to figure out a way to change those last two variables at runtime – almost everything in my program depends on them staying constant during each simulation step. This is not the case between simulation steps, however. So, I can afford to replace the Swarm class’s personArray with a completely new one with a new number of people or group colors in that time.

Changing the number of colors turned out to be easy. Removing a color respawns any members of that group as a different color. Adding a color did not even require any intervention on my part – the balancing algorithm notices that the added color is underrepresented in the sample and spawns new members automatically.

Adjusting the number of people was more difficult but also ultimately solvable. All people from the old array get copied over during the process, except the ones for whom there is no more room – those are discarded. Conversely, if additional people are necessary, they are spawned as new randomized instances of Person. There need to be at least 10 people in the simulation. There is no explicit upper limit on the number of people, apart from available computer resources (the Voronoi and Delaunay calculations slow the simulation a lot) and screen space (from some point, the people start overlapping, despite the built-in repulsion value).


I was not satisfied with these interactions, however. I wanted more. Having put so much work into the game’s core algorithm, I saw that it was held back by the visuals. I started moving away from the original concept by using different links between people. Instead of showing the health effects, I connected neighboring people of the same color with their corresponding Delaunay link:

Settings 1100221113

The people came next. Even though I love the simple humanlike visuals (with random hair color, height, and width!), in visualization, simpler is better. Enter dots:

Settings 1100211113

Let’s face it, though, the numbers are quite ugly. Also the stats rectangles. And while we are making this into an interactive media visualization, why not remove the dots as well? Stunning:

Settings 1100200003

Or keep the hitpoints instead of the lines? I feel like that makes it look like some sort of military map:

Settings 1100001003

That effect is much more pronounced, though, if we also remove the region colors (I still like to highlight the center region though):

Settings 0100001003

Okay, that might have been too conceptual – let’s roll back. What if we used the Delaunay links on their own?

Settings 0100200003

Okay, let’s do one more step back and add the dots back in. We get a map of the night sky! (I love this one.)

Settings 0100210003

All of these visualizations were made possible by the last kind of interactivity: visualization options. Pressing the keys 1234567890 changes whether and how different elements of the visualization are displayed. It is possible to:

  • (1) Switch colored regions on/off.
  • (2) Switch colored central region on/off (on top of coloring from option 1).
  • (3) Switch region borders on/off (not shown in above visualizations).
  • (4) Switch central region borders on/off.
  • (5) Links: off/health effects/friendly neighbors’ Delaunay.
  • (6) People: off/dots/figures.
  • (7) Switch hitpoints on/off.
  • (8) Switch stats on/off.
  • (9) Switch settings (speed, number of people, number of colors) on/off.
  • (0) Object trails: slowly disappearing/disappearing/quickly disappearing/off.

The last setting was the one that truly revolutionized what is possible to do with the visualization – object trails. It became possible to use the different visual components to construct beautiful interactive art. There is definitely some desktop wallpaper potential!

Metro

Settings 0000010000 (30 people, 1 color, 1/32 speed)

Black and White

Settings 0000010000 (200 people, 2 colors, 1/8 speed)

Proteins

Settings 0000010000 (20 people, 6 colors, 1/8 speed)

Yarn

Settings 0000010000 (10 people, 10 colors, 1/4 speed)

Pointilism

Settings 0000010002 (70 people, 10 colors, 1/2 speed)

Ornaments

Settings 0000200002 (70 people, 10 colors, 1/16 speed)

Graffiti 

Settings 0000200002 (200 people, 3 colors, 1/32 speed)

Psychedelic Bubbles

Settings 0010000002 (200 people, 10 colors, 1/32 speed)

Mesh

Settings 0010000000 (200 people, 1 colors, 1/32 speed)

Bubbles

Settings 0010000002 (200 people, 1 colors, 1/32 speed)

Tunnel

Settings 0001000001 (30 people, 10 colors, 1/4 speed)

 

Ribbon

Settings 0100000002 (10 people, 3 colors, 1 speed, spawning ‘c’ in center)

Rose

Settings 0100000000 (10 people, 3 colors, 8 speed)

The code is presented below. There are two classes: Person (data and methods for individual people in the simulation), and Swarm (data and methods for the collection of people). Also notice the wealth of customizable variables – adjusting everything from default settings and options, through min/max member limits, distance from boundary/others, health effects, score effects, to movement weights:

import megamu.mesh.*;

// ONLY CHANGE RIGHT BEFORE SWARM REGENERATION!
int NUM_PEOPLE = 200; //200
int NUM_COLORS = 10; //10

// reassign with NUM_PEOPLE and NUM_COLORS:
float VAR_MAX_NUMBER = 1.25 * (NUM_PEOPLE/NUM_COLORS);
float VAR_MIN_NUMBER = 0.75 * (NUM_PEOPLE/NUM_COLORS);

float VAR_LOSER_EXTRA = 0.75 * (NUM_PEOPLE/NUM_COLORS);
float VAR_LEADER_EXTRA = -0.75 * (NUM_PEOPLE/NUM_COLORS);
float VAR_CLOSEST_TO_CENTER_EXTRA = -0.25 * (NUM_PEOPLE/NUM_COLORS);

float SPEED_MODIFIER = 1 / 32.0;

// reassign with SPEED_MODIFIER:
float VAR_HITPOINTS = 100 / (SPEED_MODIFIER);

int ADD_PEOPLE_STEP = 10;
int ADD_COLORS_STEP = 1;

int OPT_REGIONS = 1; //1
int OPT_CENTER_REGION = 1; //1
int OPT_OUTLINES = 0; //0
int OPT_CENTER_OUTLINE = 1; //1
int OPT_LINKS = 1; //1
int OPT_MODE = 2; //2
int OPT_HITPOINTS = 1; //1
int OPT_STATS = 1; //1
int OPT_SPEED = 1; //1
int OPT_GHOSTS = 3; //3

float VAR_MAX_SPEED = 25; //25

int VAR_BOUNDARY = 100; //100
int VAR_TOO_CLOSE = 50; //50

int VAR_HIT_BOUNDARY = 50; //50
int VAR_HIT_TOO_CLOSE = 25; //25
int VAR_HIT_FRIENDLY = 75; //75
int VAR_HIT_HOSTILE = 80; //80

float H_OVER_MAX = -10; //-10
float H_UNDER_MIN = 1; //1
float H_BOUNDARY = -25; //-25
float H_TOO_CLOSE = -10; //-10
float H_FRIENDLY = 1; //1
float H_HOSTILE = -1; //-1
float H_CLOSEST_TO_CENTER = 1; //1

float S_CLOSEST_TO_CENTER = 0.1; //0.1

float W_RANDOM = 1; //1
float W_BOUNDARY = -5; //-5
float W_CENTER = 3.5; //3.5
float W_TOO_CLOSE = -25; //-10
float W_FRIENDLY = 1; //1
float W_HOSTILE = -3.5; //-3.5

boolean regenerateSwarm = false;
int newNumPeople = NUM_PEOPLE;
int newNumColors = NUM_COLORS;
boolean ghostsChanged = false;

color[] linkColors = {color(0,0,0),color(127,127,127),color(255,0,0),color(0,255,0),color(255,191,0),color(0,0,255),color(165,42,42),color(255,165,0),color(255,0,255),color(0,255,255)};
color[] regionColors = {color(0,0,0,63),color(191,191,191,63),color(255,0,0,63),color(0,255,0,63),color(255,255,0,63),color(0,0,255,63),color(165,42,42,63),color(255,165,0,63),color(255,0,255,63),color(0,255,255,63)};
color[] clothesColors = {color(0,0,0),color(255,255,255),color(255,0,0),color(0,255,0),color(255,255,0),color(0,0,255),color(165,42,42),color(255,165,0),color(255,0,255),color(0,255,255)};

color[] hairColors = {color(8,8,6),color(80,69,69),color(184,151,120),color(233,206,168),color(83,61,53),color(255,242,255)};
color skinColor = color(222,168,153);

Swarm peopleSwarm;

void setup() {
 //size(960, 960);
 fullScreen();
 background(255);
 rectMode(CENTER);
 fill(0);
 rect(width/2,height/2,2,2);
 peopleSwarm = new Swarm();
}

void draw() {
 rectMode(CENTER);
 noStroke();
 if (OPT_GHOSTS == 0) fill(255,1);
 else if (OPT_GHOSTS == 1) fill(255,5);
 else if (OPT_GHOSTS == 2) fill(255,10);
 else if (OPT_GHOSTS == 3) fill(255,255);
 rect(width/2,height/2,width,height);
 
 if (ghostsChanged) {
  rectMode(CENTER);
  noStroke();
  fill(255,255);
  rect(width/2,height/2,width,height);
  
  ghostsChanged = false;
 }
 
 rectMode(CENTER);
 stroke(0);
 fill(0);
 rect(width/2,height/2,2,2);
 
 if (regenerateSwarm) peopleSwarm = new Swarm(newNumPeople, newNumColors, peopleSwarm);
 
 peopleSwarm.simulate();
 
 if (OPT_REGIONS == 1) peopleSwarm.displayRegions();
 
 if (OPT_CENTER_REGION == 1) peopleSwarm.displayCenterRegion();
 
 if (OPT_OUTLINES == 1) peopleSwarm.displayRegionOutlines();
 
 if (OPT_CENTER_OUTLINE == 1) peopleSwarm.displayCenterRegionOutline();
 
 if (OPT_LINKS == 1) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayEffects();
 if (OPT_LINKS == 2) peopleSwarm.displayColorLinks();
 
 if (OPT_MODE == 1) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayDot();
 else if (OPT_MODE == 2) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayPerson();
 
 if (OPT_MODE == 0 && OPT_HITPOINTS == 1) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayStandaloneHitpoints();
 else if (OPT_MODE == 1 && OPT_HITPOINTS == 1) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayDotHitpoints();
 else if (OPT_MODE == 2 && OPT_HITPOINTS == 1) for (int i = 0; i < NUM_PEOPLE; i += 1) peopleSwarm.personArray[i].displayPersonHitpoints();
 
 if (OPT_STATS == 1) peopleSwarm.displayStats();
 
 if (OPT_SPEED == 1) peopleSwarm.displaySettings();
 
 peopleSwarm.lifeCheck();
}

class Swarm {
 Person[] personArray;
 
 Voronoi voronoi;
 Delaunay delaunay;
 
 int[] colorNumberArray;
 int[] colorMaxNumberArray;
 int[] colorMinNumberArray;
 
 float[] colorScoreArray;
 
 Person closestToCenter = null;
 
 Swarm() {
  personArray = new Person[NUM_PEOPLE];
  
  colorNumberArray = new int[NUM_COLORS];
  colorMaxNumberArray = new int[NUM_COLORS];
  colorMinNumberArray = new int[NUM_COLORS];
  
  colorScoreArray = new float[NUM_COLORS];
  
  for (int i = 0; i < NUM_COLORS; i += 1) colorNumberArray[i] = 0;
  
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   int clothesColor = int(random(NUM_COLORS));
   personArray[i] = new Person(i, random(width), random(height), int(random(10,20)), int(random(30,40)), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), int(random(hairColors.length)), clothesColor);
   colorNumberArray[clothesColor] += 1;
  }
  
  for (int i = 0; i < NUM_COLORS; i += 1) colorScoreArray[i] = 0;
  
  setClosestToCenter();
  
  for (int i = 0; i < NUM_COLORS; i += 1) {
   colorMaxNumberArray[i] = 0;
   colorMinNumberArray[i] = 0;
  }
  
  setColorNumberLimits();
 }
 
 Swarm(int newNumPeople, int newNumColors, Swarm previousSwarm) {
  NUM_PEOPLE = newNumPeople;
  NUM_COLORS = newNumColors;
  
  VAR_MAX_NUMBER = 1.25 * (NUM_PEOPLE/NUM_COLORS);
  VAR_MIN_NUMBER = 0.75 * (NUM_PEOPLE/NUM_COLORS);
  
  VAR_LOSER_EXTRA = 0.75 * (NUM_PEOPLE/NUM_COLORS);
  VAR_LEADER_EXTRA = -0.75 * (NUM_PEOPLE/NUM_COLORS);
  VAR_CLOSEST_TO_CENTER_EXTRA = -0.25 * (NUM_PEOPLE/NUM_COLORS);
  
  regenerateSwarm = false;
  
  personArray = new Person[NUM_PEOPLE];
  
  colorNumberArray = new int[NUM_COLORS];
  colorMaxNumberArray = new int[NUM_COLORS];
  colorMinNumberArray = new int[NUM_COLORS];
  
  colorScoreArray = new float[NUM_COLORS];
  
  for (int i = 0; i < NUM_COLORS; i += 1) colorNumberArray[i] = 0;
  
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   if (i < previousSwarm.personArray.length) {
    if (previousSwarm.personArray[i].clothesColor >= NUM_COLORS) {
     // changed number of colors
     int clothesColor = int(random(NUM_COLORS));
     previousSwarm.personArray[i] = new Person(i, random(width), random(height), int(random(10,20)), int(random(30,40)), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), int(random(hairColors.length)), clothesColor);
    }
   }
  }
  
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   if (i < previousSwarm.personArray.length) personArray[i] = previousSwarm.personArray[i];
   else {
    // changed number of people
    int clothesColor = int(random(NUM_COLORS));
    personArray[i] = new Person(i, random(width), random(height), int(random(10,20)), int(random(30,40)), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), int(random(hairColors.length)), clothesColor);
   }
   colorNumberArray[personArray[i].clothesColor] += 1;
  }
  
  for (int i = 0; i < NUM_COLORS; i += 1) colorScoreArray[i] = 0;
  
  setClosestToCenter();
  
  for (int i = 0; i < NUM_COLORS; i += 1) {
   colorMaxNumberArray[i] = 0;
   colorMinNumberArray[i] = 0;
  }
  
  setColorNumberLimits();
 }
 
 void changeSpeeds() {
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   int hCountClose = 0;
   int vCountClose = 0;
   int hCountFriendly = 0;
   int vCountFriendly = 0;
   int hCountHostile = 0;
   int vCountHostile = 0;
   
   for (int j = 0; j < NUM_PEOPLE; j+= 1) {
    if ((personArray[j].x-personArray[i].x > -VAR_TOO_CLOSE && personArray[j].x-personArray[i].x < VAR_TOO_CLOSE) && (personArray[j].y-personArray[i].y > -VAR_TOO_CLOSE && personArray[j].y-personArray[i].y < VAR_TOO_CLOSE)) { // too close
     if (personArray[j].x > personArray[i].x) hCountClose += 1;
     if (personArray[j].x < personArray[i].x) hCountClose -= 1;
     if (personArray[j].y > personArray[i].y) vCountClose += 1;
     if (personArray[j].y < personArray[i].y) vCountClose -= 1;
    }
    
    if (personArray[j].clothesColor == personArray[i].clothesColor) { // friendly
     if (personArray[j].x > personArray[i].x) hCountFriendly += 1;
     if (personArray[j].x < personArray[i].x) hCountFriendly -= 1;
     if (personArray[j].y > personArray[i].y) vCountFriendly += 1;
     if (personArray[j].y < personArray[i].y) vCountFriendly -= 1;
    }
    
    if (personArray[j].clothesColor != personArray[i].clothesColor) { // hostile
     if (personArray[j].x > personArray[i].x) hCountHostile += 1;
     if (personArray[j].x < personArray[i].x) hCountHostile -= 1;
     if (personArray[j].y > personArray[i].y) vCountHostile += 1;
     if (personArray[j].y < personArray[i].y) vCountHostile -= 1;
    }
    
    float hSpeedChange = 0;
    if (random(2) >= 1) hSpeedChange += W_RANDOM;
    if (random(2) < 1) hSpeedChange -= W_RANDOM;
    if (personArray[i].x > width-VAR_BOUNDARY) hSpeedChange += W_BOUNDARY;
    if (personArray[i].x < 0+VAR_BOUNDARY) hSpeedChange -= W_BOUNDARY;
    if (width/2 > personArray[i].x) hSpeedChange += W_CENTER;
    if (width/2 < personArray[i].x) hSpeedChange -= W_CENTER;
    if (hCountClose > 0) hSpeedChange += W_TOO_CLOSE;
    if (hCountClose < 0) hSpeedChange -= W_TOO_CLOSE;
    if (hCountFriendly > 0) hSpeedChange += W_FRIENDLY;
    if (hCountFriendly < 0) hSpeedChange -= W_FRIENDLY;
    if (hCountHostile > 0) hSpeedChange += W_HOSTILE;
    if (hCountHostile < 0) hSpeedChange -= W_HOSTILE;
    hSpeedChange *= SPEED_MODIFIER;
    
    float vSpeedChange = 0;
    if (random(2) >= 1) vSpeedChange += W_RANDOM;
    if (random(2) < 1) vSpeedChange -= W_RANDOM;
    if (personArray[i].y > height-VAR_BOUNDARY) vSpeedChange += W_BOUNDARY;
    if (personArray[i].y < 0+VAR_BOUNDARY) vSpeedChange -= W_BOUNDARY;
    if (height/2 > personArray[i].y) vSpeedChange += W_CENTER;
    if (height/2 < personArray[i].y) vSpeedChange -= W_CENTER;
    if (vCountClose > 0) vSpeedChange += W_TOO_CLOSE;
    if (vCountClose < 0) vSpeedChange -= W_TOO_CLOSE;
    if (vCountFriendly > 0) vSpeedChange += W_FRIENDLY;
    if (vCountFriendly < 0) vSpeedChange -= W_FRIENDLY;
    if (vCountHostile > 0) vSpeedChange += W_HOSTILE;
    if (vCountHostile < 0) vSpeedChange -= W_HOSTILE;
    vSpeedChange *= SPEED_MODIFIER;
    
    personArray[i].changeSpeed(hSpeedChange, vSpeedChange);
   }
  }
 }
 
 int getLeaderColor() {
  int leaderColor = 0;
  int highestNumber = 0;
  float highestScore = Float.NEGATIVE_INFINITY;
  for (int i = 0; i < NUM_COLORS; i += 1) {
   if (colorScoreArray[i] > highestScore) {
    highestNumber = colorNumberArray[i];
    highestScore = colorScoreArray[i];
    leaderColor = i;
   }
   else if (colorScoreArray[i] == highestScore && colorNumberArray[i] > highestNumber) {
    highestNumber = colorNumberArray[i];
    highestScore = colorScoreArray[i];
    leaderColor = i;
   }
  }
  return leaderColor;
 }
 
 int getLoserColor() {
  int loserColor = 0;
  int lowestNumber = NUM_PEOPLE;
  float lowestScore = Float.POSITIVE_INFINITY;
  for (int i = 0; i < NUM_COLORS; i += 1) { 
   if (colorScoreArray[i] < lowestScore) {
    lowestNumber = colorNumberArray[i];
    lowestScore = colorScoreArray[i];
    loserColor = i;
   }
   else if (colorScoreArray[i] == lowestScore && colorNumberArray[i] < lowestNumber) {
    lowestNumber = colorNumberArray[i];
    lowestScore = colorScoreArray[i];
    loserColor = i;
   }
  }
  return loserColor;
 }
 
 int getNextColor() { // loser version
  int nextColor = 0;
  int lowestNumber = NUM_PEOPLE;
  float lowestScore = Float.POSITIVE_INFINITY;
  for (int i = 0; i < NUM_COLORS; i += 1) {
   if (colorNumberArray[i] < colorMinNumberArray[i]) {
    // if someone has fewer than they should have, they are always going to be next
    nextColor = i;
    break;
   }
   
   if (colorNumberArray[i] >= colorMaxNumberArray[i]) {
    // if someone has more that they should have, they are never going to be next (to prevent runaway colors)
    continue;
   }
   
   if (colorScoreArray[i] < lowestScore) {
    lowestNumber = colorNumberArray[i];
    lowestScore = colorScoreArray[i];
    nextColor = i;
   }
   else if (colorScoreArray[i] == lowestScore && colorNumberArray[i] < lowestNumber) {
    lowestNumber = colorNumberArray[i];
    lowestScore = colorScoreArray[i];
    nextColor = i;
   }
  }
  return nextColor;
 }
 
 /*int getNextColor() { // leader version
  int nextColor = 0;
  int highestNumber = 0;
  float highestScore = Float.NEGATIVE_INFINITY;
  for (int i = 0; i < NUM_COLORS; i += 1) {
   if (colorNumberArray[i] < colorMinNumberArray[i]) {
    // if someone has fewer than they should have, they are always going to be next
    nextColor = i;
    break;
   }
   
   if (colorNumberArray[i] >= colorMaxNumberArrat[i]) {
    // if someone has more that they should have, they are never going to be next (to prevent runaway colors)
    continue;
   }
   
   if (colorScoreArray[i] > highestScore) {
    highestNumber = colorNumberArray[i];
    highestScore = colorScoreArray[i];
    nextColor = i;
   }
   else if (colorScoreArray[i] == highestScore && colorNumberArray[i] > highestNumber) {
    highestNumber = colorNumberArray[i];
    highestScore = colorScoreArray[i];
    nextColor = i;
   }
  }
  return nextColor;
 }*/
 
 /*int getNextColor() { // random version
  int nextColor = -1;
  while (nextColor == -1) { // THIS CAN LEAD TO INFINITE LOOP IF EVERYONE IS MAXED OUT!!
   for (int i = 0; i < NUM_COLORS; i += 1) {
    if (colorNumberArray[i] < colorMinNumberArray[i]) {
     // if someone has fewer than they should have, they are always going to be next
     nextColor = i;
     break;
    }
    
    if (colorNumberArray[i] >= colorMaxNumberArray[i]) {
     // if someone has more that they should have, they are never going to be next (to prevent runaway colors)
     continue;
    }
    
    if (int(random(NUM_COLORS)) == 0) {
     nextColor = i;
    }
   }
  }
  return nextColor;
 }*/
 
 void respawnPerson(int personIndex, float x, float y, int newPersonClothesColor) {
  colorNumberArray[personArray[personIndex].clothesColor] -= 1;
  personArray[personIndex] = new Person(personIndex, x, y, int(random(10,20)), int(random(30,40)), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), random(-9*SPEED_MODIFIER,9*SPEED_MODIFIER), int(random(hairColors.length)), newPersonClothesColor);
  //System.out.println("Respawned: "+personIndex);
  colorNumberArray[newPersonClothesColor] += 1;
 }
 
 void modifyHitpoints() {
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   if (colorNumberArray[personArray[i].clothesColor] > colorMaxNumberArray[personArray[i].clothesColor]) personArray[i].affect(H_OVER_MAX,null);
   else if (colorNumberArray[personArray[i].clothesColor] < colorMaxNumberArray[personArray[i].clothesColor]) personArray[i].affect(H_UNDER_MIN,null);
 
   if (personArray[i].x > width-VAR_HIT_BOUNDARY || personArray[i].x < 0+VAR_HIT_BOUNDARY) personArray[i].affect(H_BOUNDARY,null);
   if (personArray[i].y > height-VAR_HIT_BOUNDARY || personArray[i].y < 0+VAR_HIT_BOUNDARY) personArray[i].affect(H_BOUNDARY,null);
 
   for (int j = 0; j < NUM_PEOPLE; j+= 1) {
    if (i == j) continue;
    if (personArray[j].clothesColor != personArray[i].clothesColor) {
     if ((personArray[j].x-personArray[i].x > -VAR_HIT_HOSTILE && personArray[j].x-personArray[i].x < VAR_HIT_HOSTILE) && (personArray[j].y-personArray[i].y > -VAR_HIT_HOSTILE && personArray[j].y-personArray[i].y < VAR_HIT_HOSTILE)) {
      personArray[i].affect(H_HOSTILE,personArray[j]);
     }
    }
    else {
     if ((personArray[j].x-personArray[i].x > -VAR_HIT_FRIENDLY && personArray[j].x-personArray[i].x < VAR_HIT_FRIENDLY) && (personArray[j].y-personArray[i].y > -VAR_HIT_FRIENDLY && personArray[j].y-personArray[i].y < VAR_HIT_FRIENDLY)) {
      personArray[i].affect(H_FRIENDLY,personArray[j]);
     }
    }
    if ((personArray[j].x-personArray[i].x > -VAR_HIT_TOO_CLOSE && personArray[j].x-personArray[i].x < VAR_HIT_TOO_CLOSE) && (personArray[j].y-personArray[i].y > -VAR_HIT_TOO_CLOSE && personArray[j].y-personArray[i].y < VAR_HIT_TOO_CLOSE)) { // too close
     personArray[i].affect(H_TOO_CLOSE,personArray[j]);
    }
   }
  }
 }
 
 void lifeCheck() {
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   if (personArray[i].hitpoints <= 0) { 
    int newPersonClothesColor = getNextColor();
    float x = random(width);
    float y = random(height);
    
    respawnPerson(i, x, y, newPersonClothesColor);
   }
   
   if (personArray[i].hitpoints > VAR_HITPOINTS) {
    personArray[i].hitpoints = VAR_HITPOINTS;
   }
  }
 }
 
 void setColorNumberLimits() {
  int leaderColor = getLeaderColor();
  int loserColor = getLoserColor();
  for (int i = 0; i < NUM_COLORS; i += 1) {
   colorMaxNumberArray[i] = round(VAR_MAX_NUMBER);
   colorMinNumberArray[i] = round(VAR_MIN_NUMBER);
   
   if (i == leaderColor) {
    colorMaxNumberArray[i] += round(VAR_LEADER_EXTRA);
    colorMinNumberArray[i] += round(VAR_LEADER_EXTRA);
   }
   else if (i == loserColor) {
    colorMaxNumberArray[i] += round(VAR_LOSER_EXTRA);
    colorMinNumberArray[i] += round(VAR_LOSER_EXTRA);
   }
   
   if (i == closestToCenter.clothesColor) {
    colorMaxNumberArray[i] += round(VAR_CLOSEST_TO_CENTER_EXTRA);
    colorMaxNumberArray[i] += round(VAR_CLOSEST_TO_CENTER_EXTRA);
   }
  }
 }
 
 void setClosestToCenter() {
  Person closestToCenter = null;
  float closestDistance = Float.POSITIVE_INFINITY;
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   if (personArray[i].distanceToCenter < closestDistance) {
    closestDistance = personArray[i].distanceToCenter;
    closestToCenter = personArray[i];
   }
  }
  this.closestToCenter = closestToCenter;
 }
 
 void awardScorePoints() { 
  colorScoreArray[closestToCenter.clothesColor] += S_CLOSEST_TO_CENTER;
  closestToCenter.closestToCenter(H_CLOSEST_TO_CENTER);
 }
 
 void makeVoronoiAndDelaunay() {
  float[][] positions = new float[NUM_PEOPLE][2];
  for (int i = 0; i < NUM_PEOPLE; i +=1 ) {
   positions[i][0] = personArray[i].x;
   positions[i][1] = personArray[i].y;
  }
  voronoi = new Voronoi(positions);
  delaunay = new Delaunay(positions);
 }
 
 void simulate() {
  //for (int i = 0; i < NUM_COLORS; i += 1) System.out.println(i+": "+colorMaxNumberArray[i]+" > "+colorNumberArray[i]+" > "+colorMinNumberArray[i]);
 
  changeSpeeds();
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   personArray[i].move();
  }
  makeVoronoiAndDelaunay();
  
  modifyHitpoints();
  
  for (int i = 0; i < NUM_PEOPLE; i += 1) {
   personArray[i].setDistanceToCenter();
  }
  
  setClosestToCenter();
  awardScorePoints();
  
  setColorNumberLimits();
 }
 
 void displayRegions() {
  MPolygon[] regions = voronoi.getRegions();
  for(int i = 0; i < regions.length; i += 1) {
   // an array of points
   float[][] coords = regions[i].getCoords();
   
   color regionColor = regionColors[personArray[i].clothesColor]; // the regions are in the same order as people
   
   noStroke();
   fill(regionColor);
   beginShape();
   for(int j = 0; j < regions[i].count(); j++) {
    vertex(coords[j][0], coords[j][1]);
   }
   endShape();
  }
 }
 
 void displayRegionOutlines() {
  MPolygon[] regions = voronoi.getRegions();
  for(int i = 0; i < regions.length; i += 1) {
   // an array of points
   float[][] coords = regions[i].getCoords();
   
   color regionColor = linkColors[personArray[i].clothesColor]; // the regions are in the same order as people
   
   strokeWeight(1);
   stroke(regionColor);
   noFill();
   beginShape();
   for(int j = 0; j < regions[i].count(); j++) {
    vertex(coords[j][0], coords[j][1]);
   }
   endShape();
  }
 }
 
 void displayCenterRegion() {
  MPolygon[] regions = voronoi.getRegions();
  
  noStroke();
  fill(regionColors[personArray[closestToCenter.index].clothesColor]);
  beginShape();
  float[][] coords = regions[closestToCenter.index].getCoords();
  for(int i = 0; i < regions[closestToCenter.index].count(); i++) {
   vertex(coords[i][0], coords[i][1]);
  }
  endShape();
 }
 
 void displayCenterRegionOutline() {
  MPolygon[] regions = voronoi.getRegions();
  
  strokeWeight(2);
  stroke(0);
  noFill();
  beginShape();
  float[][] coords = regions[closestToCenter.index].getCoords();
  for(int i = 0; i < regions[closestToCenter.index].count(); i++) {
   vertex(coords[i][0], coords[i][1]);
  }
  endShape();
 }
 
 void displayColorLinks() {
  int[][] links = delaunay.getLinks();
  for (int i = 0; i < links.length; i += 1) {
   int startIndex = links[i][0];
   int endIndex = links[i][1];
   
   if (personArray[startIndex].clothesColor == personArray[endIndex].clothesColor) {
    strokeWeight(1);
    stroke(linkColors[personArray[startIndex].clothesColor]);
    line(personArray[startIndex].x,personArray[startIndex].y,personArray[endIndex].x,personArray[endIndex].y);
   }
  }
 }
 
 void displayStats() {
  rectMode(CORNER);
  noStroke();
  fill(0,0,0,63);
  rect(0,0,350,(NUM_COLORS*25));
  
  for (int i = 0; i < NUM_COLORS; i += 1) {
   rectMode(CORNER);
   textSize(20);
   fill(clothesColors[i]);
   text(char(i+97)+": ",0,i*25,50,i*25+25);
   
   rectMode(CORNER);
   textSize(20);
   fill(clothesColors[i]);
   String closestToCenterMark = "";
   if (closestToCenter.clothesColor == i) closestToCenterMark = "*";
   text(int(colorScoreArray[i])+""+closestToCenterMark,50,i*25,100,i*25+25);
   
   rectMode(CORNER);
   textSize(20);
   fill(clothesColors[i]);
   text("("+int(colorNumberArray[i])+")",100,i*25,150,i*25+25);
  }
  
  int leaderColor = getLeaderColor();
  rectMode(CORNER);
  textSize(20);
  fill(clothesColors[leaderColor]);
  text("Leading",150,leaderColor*25,250,leaderColor*25+25);
  
  int loserColor = getLoserColor();
  if (loserColor != leaderColor) {
   rectMode(CORNER);
   textSize(20);
   fill(clothesColors[loserColor]);
   text("Losing",150,getLoserColor()*25,250,loserColor*25+25);
  }
  
  int nextColor = getNextColor();
  for (int i = 0; i < NUM_COLORS; i += 1) {
   if (i == nextColor) {
    rectMode(CORNER);
    textSize(20);
    fill(clothesColors[i]);
    text("Next",250,i*25,350,i*25+25);
   }
   else if (colorNumberArray[i] == colorMinNumberArray[i] || colorNumberArray[i] == 0) {
    rectMode(CORNER);
    textSize(20);
    fill(clothesColors[i]);
    text("Min",250,i*25,350,i*25+25);
   }
   else if (colorNumberArray[i] < colorMinNumberArray[i]) {
    rectMode(CORNER);
    textSize(20);
    fill(clothesColors[i]);
    text("Under",250,i*25,350,i*25+25);
   }
   else if (colorNumberArray[i] == colorMaxNumberArray[i] || colorNumberArray[i] == NUM_PEOPLE) {
    rectMode(CORNER);
    textSize(20);
    fill(clothesColors[i]);
    text("Max",250,i*25,350,i*25+25);
   }
   else if (colorNumberArray[i] > colorMaxNumberArray[i]) {
    rectMode(CORNER);
    textSize(20);
    fill(clothesColors[i]);
    text("Over",250,i*25,350,i*25+25);
   }
  }
 }
 
 void displaySettings() {
  rectMode(CORNER);
  noStroke();
  fill(0,0,0,63);
  rect(width-250,height-3*25,width,height);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("< >",width-250,height-3*25,width-200,height-2*25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("[ ]",width-250,height-2*25,width-200,height-25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("- +",width-250,height-25,width-200,height);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("People: ",width-200,height-3*25,width-100,height-2*25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("Colors: ",width-200,height-2*25,width-100,height-25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text("Speed: ",width-200,height-25,width-100,height);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text(NUM_PEOPLE+"",width-100,height-3*25,width,height-2*25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  text(NUM_COLORS+"",width-100,height-2*25,width,height-25);
  
  rectMode(CORNER);
  textSize(20);
  fill(255);
  String speedText = int(SPEED_MODIFIER)+"";
  if (int(SPEED_MODIFIER) < 1) speedText = "1/"+int(1/(SPEED_MODIFIER));
  text(speedText+"",width-100,height-25,width,height);
 }
}

class Person {
 int index;
 
 float x;
 float y;
 int w;
 int h;
 
 int hairColor;
 int clothesColor;
 
 float hSpeed;
 float vSpeed;
 
 float distanceToCenter = Float.POSITIVE_INFINITY;
 
 float hitpoints = VAR_HITPOINTS;
 boolean isHit = false;
 Person hitBy = null;
 boolean isHealed = false;
 Person healedBy = null;
 boolean isClosestToCenter = false;
 
 Person(int index, float x, float y, int w, int h, float hSpeed, float vSpeed, int hairColor, int clothesColor) {
  this.index = index;
  this.x = x;
  this.y = y;
  this.w = w;
  this.h = h;
  this.hSpeed = hSpeed;
  this.vSpeed = vSpeed;
  this.hairColor = hairColor;
  this.clothesColor = clothesColor;
  
  setDistanceToCenter();
 }
 
 void setDistanceToCenter() {
  distanceToCenter = (new PVector(x,y,0)).dist(new PVector(width/2,height/2));
 }
 
 void affect(float hitpointEffect, Person affectedBy) {
  hitpoints += hitpointEffect;
  if (hitpointEffect < 0) {
   isHit = true;
   hitBy = affectedBy;
  }
  if (hitpointEffect > 0) {
   isHealed = true;
   healedBy = affectedBy;
  }
 }
 
 void closestToCenter(float hitpointEffect) {
  hitpoints += hitpointEffect;
  isClosestToCenter = true;
 }
 
 void limitSpeed() {
  if (hSpeed < -VAR_MAX_SPEED*SPEED_MODIFIER) hSpeed = -VAR_MAX_SPEED*SPEED_MODIFIER;
  if (hSpeed > VAR_MAX_SPEED*SPEED_MODIFIER) hSpeed = VAR_MAX_SPEED*SPEED_MODIFIER;
  
  if (vSpeed < -VAR_MAX_SPEED*SPEED_MODIFIER) vSpeed = -VAR_MAX_SPEED*SPEED_MODIFIER;
  if (vSpeed > VAR_MAX_SPEED*SPEED_MODIFIER) vSpeed = VAR_MAX_SPEED*SPEED_MODIFIER;
 }
 
 void changeSpeed(float hSpeedChange, float vSpeedChange) {
  hSpeed += hSpeedChange;
  vSpeed += vSpeedChange;
  
  limitSpeed();
 }
 
 void move() {
  x += hSpeed;
  y += vSpeed;
 }
 
 void displayEffects() {
  if (isHit) { 
   if (hitBy != null) {
    strokeWeight(1);
    stroke(255,0,0);
    line(hitBy.x,hitBy.y,x,y);
    hitBy = null;
   }
   
   isHit = false;
  }
  
  if (isHealed) {
   if (healedBy != null) {
    strokeWeight(1);
    stroke(0,255,0);
    line(healedBy.x,healedBy.y,x,y);
    healedBy = null;
   }
   
   isHealed = false;
  }
  
  if (isClosestToCenter) {
   strokeWeight(1);
   stroke(0,0,255);
   line(x,y,width/2,height/2);
   
   isClosestToCenter = false;
  }
 }
 
 void displayStandaloneHitpoints() {
  rectMode(CENTER);
  textSize(10);
  fill(0);
  text(int(hitpoints*SPEED_MODIFIER)+"",x*2-10,y*2-5,x*2+10, y*2+5);
 }
 
 void displayDot() {
  rectMode(CENTER);
  strokeWeight(1);
  stroke(0);
  fill(clothesColors[clothesColor]);
  ellipse(x,y,10,10);
 }
 
 void displayDotHitpoints() {
  rectMode(CENTER);
  textSize(10);
  fill(0);
  text(int(hitpoints*SPEED_MODIFIER)+"",x*2-10,y*2-30,x*2+10, y*2-25);
 }
 
 void displayPerson() {
  rectMode(CENTER);
  strokeWeight(1);
  stroke(0);
  fill(clothesColors[clothesColor]);
  rect(x, y, w, h);
  fill(skinColor);
  rect(x, y-h/2, w, 10);
  fill(hairColors[hairColor]);
  rect(x, y-h/2-7, w, 4);
 }
 
 void displayPersonHitpoints() {
  rectMode(CENTER);
  textSize(10);
  fill(0);
  text(int(hitpoints*SPEED_MODIFIER)+"",x*2-10,y*2-75,x*2+10, y*2-70);
 }
}

void keyPressed() {
 if (key == '+' || key == '-') {
  if (key == '+') SPEED_MODIFIER *= 2;
  if (key == '-') SPEED_MODIFIER /= 2;
  VAR_HITPOINTS = 100 / (SPEED_MODIFIER); // for new people
  // old people's hitpoints get truncated down because of hitpoint max limits
  // the shown hitpoints get adjusted up or down due to multiplication by changed SPEED_MODIFIER
 }
 
 if (key == '>' || key == '<') {
  if (key == '>') {
   newNumPeople += ADD_PEOPLE_STEP;
   regenerateSwarm = true;
  }
  if (key == '<' && NUM_PEOPLE-ADD_PEOPLE_STEP > 0) {
   newNumPeople -= ADD_PEOPLE_STEP;
   regenerateSwarm = true;
  }
 }
 
 if (key == ']' || key == '[') {
  if (key == ']' && NUM_COLORS+ADD_COLORS_STEP <= clothesColors.length) {
   newNumColors += 1;
   regenerateSwarm = true;
  }
  if (key == '[' && NUM_COLORS-ADD_COLORS_STEP > 0) {
   newNumColors -= 1;
   regenerateSwarm = true;
  }
 }
 
 if (key == ' ') peopleSwarm = new Swarm();
 
 if (key == '1') OPT_REGIONS = (OPT_REGIONS+1) % 2;
 if (key == '2') OPT_CENTER_REGION = (OPT_CENTER_REGION+1) % 2;
 if (key == '3') OPT_OUTLINES = (OPT_OUTLINES+1) % 2;
 if (key == '4') OPT_CENTER_OUTLINE = (OPT_CENTER_OUTLINE+1) % 2;
 if (key == '5') OPT_LINKS = (OPT_LINKS+1) % 3;
 if (key == '6') OPT_MODE = (OPT_MODE+1) % 3;
 if (key == '7') OPT_HITPOINTS = (OPT_HITPOINTS+1) % 2;
 if (key == '8') OPT_STATS = (OPT_STATS+1) % 2;
 if (key == '9') OPT_SPEED = (OPT_SPEED+1) % 2;
 if (key == '0') {
  OPT_GHOSTS = (OPT_GHOSTS+1) % 4;
  ghostsChanged = true;
 }
 
 int keyValue = int(key);
 int colorToAdd = keyValue-97;
 if (colorToAdd >= 0 && colorToAdd < NUM_COLORS) {
  peopleSwarm.respawnPerson(int(random(NUM_PEOPLE)), mouseX+random(-10,10), mouseY+random(-10,10), colorToAdd);
 }
}