expdef: name:Deathmatch info:~ This experiment simulates a team deathmatch. It means that framstick creatures are grouped in teams and the basic rule is: "My team vs WORLD!". The basic battle rules and collectibles are implemented. Statistics for the battle are saved to text files in CSV format: creatures.txt, teams.txt. For more information, see http://www.framsticks.com/deathmatch ~ code:~ //Author: Mateusz Cicheński @ Poznan University of Technology, 2012 @include "deathmatch-utils.inc" @include "deathmatch-levels.inc" global idleSimulationSteps; global battleNumber; global teams; function onExpDefLoad() { idleSimulationSteps = 0; battleNumber = 0; initLevels(); initPopulations(); Shapes.set("1p_weaponbox","""//0 p:-0.5, -0.5, -0.5 p: 0.5, -0.5, -0.5 p: 0.5, 0.5, -0.5 p: -0.5, 0.5, -0.5 p:-0.5, -0.5, 0.5 p:0.5, -0.5, 0.5 p:0.5, 0.5, 0.5 p:-0.5, 0.5, 0.5 j: 0,1 j: 1,2 j: 2,3 j: 3,0 j: 4,5 j: 5,6 j: 6,7 j: 7,4 j: 0,4 j: 1,5 j: 2,6 j: 3,7""", 0x0FAA0F); Shapes.set("1p_hpbox","""//0 p:-0.5, -0.5, -0.5 p: 0.5, -0.5, -0.5 p: 0.5, 0.5, -0.5 p: -0.5, 0.5, -0.5 p:-0.5, -0.5, 0.5 p:0.5, -0.5, 0.5 p:0.5, 0.5, 0.5 p:-0.5, 0.5, 0.5 j: 0,1 j: 1,2 j: 2,3 j: 3,0 j: 4,5 j: 5,6 j: 6,7 j: 7,4 j: 0,4 j: 1,5 j: 2,6 j: 3,7""", 0xAA0F0F); } function initPopulations() { //clear anything in populations Populations.clear(); //add Food population (group 0) var p=Populations[0]; p.name = "Food"; p.nnsim = 0; p.enableperf = 0; p.death = 1; p.energy = 1; p.selfmask = 0x20002; p.othermask = 0x10002; //add Weapon Upgrade population (group 1) p=Populations.addGroup("Upgrades"); p.nnsim = 0; p.enableperf = 0; p.death = 1; p.energy = 1; p.selfmask = 0x20002; p.othermask = 0x10002; //add Teams populations (group above 2) var i = 0; teams = ExpParams.teamCount; if (teams == 1) teams = GenePools[0].size; for (i = 0; i < teams; i++) { p=Populations.addGroup("Team "+i); p.nnsim = 1; p.enableperf = 1; p.death = 0; p.energy = 1; p.selfmask = 0x50001; p.othermask = 0x60001; } //Collisions on 32 bit mask: // standard - rebound creatures => if lower bits 0xFFFF // custom - custom function handler => if higher bits 0xFFFF0000 //Collisions map: // Food vs Food = 0x20002 & 0x10002 = 2 - standard // Upgrades vs Upgrades = 0x20002 & 0x10002 = 2 - standard // Team X vs Team X = 0x50001 & 0x60001 = 0x40001 - standard + custom // Team X vs Team Y = 0x50001 & 0x60001 = 0x40001 - standard + custom // Food vs Upgrades = 0x20002 & 0x10002 = 2 - standard // Food vs Team X = 0x20002 & 0x60001 = 0x20000 || 0x50001 & 0x10002 = 0x10000 - custom // Upgrades vs Team X = 0x20002 & 0x60001 = 0x20000 || 0x50001 & 0x10002 = 0x10000 - custom } function initCreatures() { for (var i = 0; i < Populations.size - 2; i++) { for (var j = 0; j < ExpParams.teamSize; j++) { var g; //Genotype object if (ExpParams.teamCount != 1) g = GenePools[0].random(); else g = GenePools[0][i]; Populations[i+2].add(g).name = "Member " + j; } } } function onExpInit() { battleNumber = 0; initAll(); } function initAll() { idleSimulationSteps = 0; battleNumber += 1; initLevel(); initPopulations(); initCreatures(); initFiles(); writeTeamStatistics(); } function initFiles() { var f=File.createDirect("creatures"+battleNumber+".txt","Creature statistics"); f.writeString("#Creature name; Team; Kills; Assists; Total damage dealt; Total damage received; HP boxes collected; Upgrade boxes collected; Lifespan\n"); f.close(); var s = "#Time interval; "; for (var i = 0; i < Populations.size - 2; i++) { s += "Team " + i + "; "; s += "Team members; "; } s += "Total energy\n"; var f=File.createDirect("teams"+battleNumber+".txt","Teams statistics"); f.writeString(s); f.close(); } function initLevel() { var levelNumber; if (ExpParams.level == -1) levelNumber = Math.random(levels.size); else levelNumber = ExpParams.level; var level = levels.get(levelNumber); Simulator.print("Level #" + (levelNumber + 1) + ": " + level[0]); World.wrldbnd = level[2]; World.wrldsiz = level[1]; World.wrldwat = -1; World.wrldtyp = level[3]; World.wrldmap = level[4]; World.wrldchg(); } function tryCreateHPBox() { if (Populations[0].size >= ExpParams.hpBoxTotalCount) return; if (Math.rnd01 > ExpParams.hpBoxProbability) return; var food = Populations[0].add("//0\nm:Vstyle=hpbox\np:"); food.name = "HP Box"; food.idleen = ExpParams.hpBoxIdleEnergy; food.energ0 = ExpParams.hpBoxStartingEnergy; food.energy = food.energ0; food.nnenabled = 0; } function tryCreateUpgradeBox() { if (Populations[1].size >= ExpParams.upgradeBoxTotalCount) return; if (Math.rnd01 > ExpParams.upgradeBoxProbability) return; var weapon = Populations[1].add("//0\nm:Vstyle=weaponbox\np:"); weapon.name = "Weapon Box"; weapon.idleen = ExpParams.upgradeBoxIdleEnergy; weapon.energ0 = ExpParams.upgradeBoxStartingEnergy; weapon.energy = weapon.energ0; weapon.nnenabled = 0; } //handle creature placement in the world function onBorn(cr) { // place newly born creature var retry = 20; //try 20 times var placed_ok = 0; while (retry-- && !placed_ok) { switch (cr.population.index) { case 0: //food placement place_centerhead(cr); break; case 1: //upgrade placement place_centerhead(cr); break; default: cr.user1 = [0.0, 0.0]; //attack cooldown (0), bonus damage (1) cr.user2 = [0,0,0,0,0,0]; //kills (0), assists (1), total damage dealt (2), total damage received (3), HP boxes collected (4), Upgrade boxes collected (5) cr.user3 = []; //who dealt damage to this creature cr.energ0 = ExpParams.creatureStartingEnergy; cr.energy = cr.energ0; place_inTeamSpot(cr); break; } if (!cr.boundingBoxCollisions(0)) {placed_ok=1; break;} } if (!placed_ok) print("onBorn() could not avoid collisions."); } //place creature in random spot and make it head center of the map function place_centerhead(cr) { var x, y, z; x = (World.wrldsiz - cr.size_x) * Math.rnd01 - cr.size_x / 2; y = (World.wrldsiz - cr.size_y) * Math.rnd01 - cr.size_y / 2; z = WorldMap.getHeight(x, y); var hx = 0.0 + (World.wrldsiz / 2) - x; var hy = 0.0 + (World.wrldsiz / 2) - y; var alpha_rad = vectorsAngle([1.0, 0.0], [hx, hy]); cr.rotate(0, 0, alpha_rad); cr.moveAbs(x, y, z - 0.999); print("Creature placed in [" + x +" " + y + " " + z); return ; } //Place creature in correct team spot (valid for Team creatures only) //Creatures are placed in circle with random rotation (commented out: heading center of the map) in their respective team groups function place_inTeamSpot(cr) { var radius = 360.00 * (cr.population.index - 1) / teams; var x, y, z; x = (ExpParams.teamSpawningAreaSize) * Math.rnd01 - cr.size_x / 2 - ExpParams.teamSpawningAreaSize/2; y = (ExpParams.teamSpawningAreaSize) * Math.rnd01 - cr.size_y / 2 - ExpParams.teamSpawningAreaSize/2; //vector of length half the world size minus spawning area size var vect = [0.0 + World.wrldsiz/2 - ExpParams.teamSpawningAreaSize/2 - 1.0, 0.0]; vect = rotateVector(vect, radius); //translate creature by given vector //then translate by halfworld (as it is the center of the world //and the positions were calculated for point [0,0] x = x + vect[0] + World.wrldsiz/2; y = y + vect[1] + World.wrldsiz/2; z = WorldMap.getHeight(x, y); var alpha_rad = 360.0 * Math.rnd01; //random rotation //this would rotate creatures so that they would face the world center: //var hx = 0.0 + (World.wrldsiz / 2) - x; //var hy = 0.0 + (World.wrldsiz / 2) - y; //var alpha_rad = vectorsAngle([1.0, 0.0], [hx, hy]); cr.rotate(0, 0, alpha_rad); //place it mid-air cr.moveAbs(x, y, z); return ; } // //////////////////////////////// // Simulation steps function onStep() { idleSimulationSteps += 1; tryCreateHPBox(); tryCreateUpgradeBox(); lowerCooldownCounters(); applySuddenDeathModifier(); clearDeadCreatures(); checkGameState(); } function writeTeamStatistics() { var s = "" + Simulator.time + "; "; var total = 0.0; for (var i = 2; i < Populations.size; i++) { var pop=Populations[i]; var sum = 0.0; for (var cr in pop) sum += cr.energy; s += "" + sum + "; "; s += "" + pop.size + "; "; total += sum; } if (total < 0) total == 0; s += "" + total + "\n"; var f=File.appendDirect("teams"+battleNumber+".txt","Teams statistics"); f.writeString(s); f.close(); } function clearDeadCreatures() { for (var i = 2; i < Populations.size; i++) { var pop=Populations[i]; for (var j = 0; j < pop.size; j++) { var cr=pop[j]; if (cr.energy <= 0) {pop.kill(cr); j--;} } } } function applySuddenDeathModifier() { if (idleSimulationSteps < ExpParams.stagnationInterval) return; for (var i = 2; i < Populations.size; i++) for (var cr in Populations[i]) cr.energy -= ExpParams.stagnationHealthReduce; } function lowerCooldownCounters() { for (var i = 2; i < Populations.size; i++) { var pop=Populations[i]; for (var cr in pop) { if (cr.user1 != null) { if (cr.user1[0] > 0) cr.user1[0]=cr.user1[0] - 1; if (cr.user1[0] < 0) cr.user1[0]=0.0; if (cr.user1[1] > 0) cr.user1[1]=cr.user1[1] - ExpParams.upgradeBoxDecay; if (cr.user1[1] < 0) cr.user1[1]=0.0; } } } } function checkGameState() { var alivePop = 0; for (var i = 2; i < Populations.size; i++) if (Populations[i].size > 0) alivePop += 1; if (alivePop == 0 && Simulator.time <= 1) { onExpInit(); //do initialization for user, because he forgot to do so return; } if (alivePop <= 1) { for (var i = 2; i < Populations.size; i++) { var pop=Populations[i]; if (pop.size > 0) { Simulator.print("Battle is OVER! The winner is: " + pop.name); //write stats of last living creatures var f=File.appendDirect("creatures"+battleNumber+".txt","Creature statistics"); for (var cr in pop) { //f.writeString("Creature name; Team; Kills; Assists; Total damage dealt; Total damage received; HP boxes collected; Upgrade boxes collected; Lifespan"); if (cr.user2 != null) f.writeString(cr.name + "; " + pop.name + "; " + cr.user2[0] + "; " + cr.user2[1] + "; " + cr.user2[2] + "; " + cr.user2[3] + "; " + cr.user2[4] + "; " + cr.user2[5] + "; " + cr.lifespan + "\n"); } f.close(); } } if (alivePop == 0) Simulator.print("Battle is OVER! Total annihilation!"); Simulator.stop(); } } function print(msg) { if (ExpParams.debug) Simulator.print(msg); } function stop(msg) { Simulator.print(msg); Simulator.stop(); } // //////////////////////////// // Collisions //function executed on any Creature collission function onTeam0CrCollision, onTeam1CrCollision, onTeam2CrCollision, onTeam3CrCollision, onTeam4CrCollision, onTeam5CrCollision, onTeam6CrCollision, onTeam7CrCollision, onTeam8CrCollision, onTeam9CrCollision() { var c1 = CrCollision.Creature1; var c2 = CrCollision.Creature2; if (c1 == null) stop("2"); if (c2 == null) stop("2"); if (c1.energy <= 0 || c2.energy <= 0) { //collision with already killed creature (chain resolution) print("onCrCollision called with less than 2 Creatures"); return ; } if (c1.population == null || c2.population == null) { //collision of creature without a group print("onCrCollision called for creature without group, ignoring"); return ; } //first condition should never occur, but who knows? if (c1.population.index < 2 || c2.population.index < 2) { //collision with HP Box and Upgrades are handled separately print("onCrCollision called for population 0 or 1, ignoring"); return ; } idleSimulationSteps = 0; var kill_c1 = 0; var kill_c2 = 0; var dice1 = 0; var dice2 = 0; var changed = 0; for (var i = 0; i < ExpParams.diceCount; i++) { dice1 = dice1 + Math.random(ExpParams.diceSides) + 1; dice2 = dice2 + Math.random(ExpParams.diceSides) + 1; } var energy1 = 0.0 + ExpParams.creatureDamage * dice1; var energy2 = 0.0 + ExpParams.creatureDamage * dice2; energy1 += ExpParams.upgradeMultiplier * c1.user1.get(1); energy2 += ExpParams.upgradeMultiplier * c2.user1.get(1); if (c1.population == c2.population) { energy1 = ExpParams.friendlyFireDamageMultiplier * energy1; energy2 = ExpParams.friendlyFireDamageMultiplier * energy2; } if (c1.user1 == null) stop("3"); if (c1.user2 == null) stop("3"); if (c1.user3 == null) stop("3"); if (c2.user1 == null) stop("3"); if (c2.user2 == null) stop("3"); if (c2.user2 == null) stop("3"); if (c2.user1[0] <= 0 && energy2 > 0) { changed = 1; c1.energy = c1.energy - energy2; if (c1.energy < 0) c1.energy = 0; print(c2.name + " [" + c2.population.name + "] rolled " + dice2 + " and dealt " + energy2 + " [+" + (ExpParams.upgradeMultiplier * c2.user1[1]) +" bonus] damage to " + c1.name + " [" + c1.population.name + "]"); c2.user1.set(0, ExpParams.attackCooldown); c2.user2.set(2, c2.user2[2] + energy1); c1.user2.set(3, c1.user2[3] + energy1); var vect = [c2.uid,c2.name]; var arrindex = arrayContains(c1.user3, 0, c2.uid); if (arrindex != -1) { vect.add(energy2 + c1.user3[arrindex][2]); c1.user3[arrindex]=vect; } else { vect.add(energy2); if (c1.user3 == null) c1.user3 = Vector.new(); c1.user3.add(vect); if (c1.energy > 0) c2.user2[1] = c2.user2[1] + 1; //increase assists statistic } if (c1.energy <= 0) { c2.user2[0] = c2.user2[0] + 1; //increase kills statistic kill_c1 = 1; } } if (c1.user1[0] <= 0 && energy1 > 0) { changed = 1; c2.energy = c2.energy - energy1; if (c2.energy < 0) c2.energy = 0; print(c1.name + " [" + c1.population.name + "] rolled " + dice1 + " and dealt " + energy1 + " [+" + (ExpParams.upgradeMultiplier * c1.user1[1]) +" bonus] damage to " + c2.name + " [" + c2.population.name + "]"); c1.user1[0] = ExpParams.attackCooldown; c1.user2[2] = c1.user2[2] + energy1; c2.user2[3] = c2.user2[3] + energy1; var vect = Vector.new(); vect.add(c1.uid); vect.add(c1.name); var arrindex = arrayContains(c2.user3, 0, c1.uid); if (arrindex != -1) { vect.add(energy1 + c2.user3[arrindex][2]); c2.user3[arrindex] = vect; } else { vect.add(energy1); if (c2.user3 == null) c2.user3 = Vector.new(); c2.user3.add(vect); if (c2.energy > 0) { c1.user2[1] = c1.user2[1] + 1; //increase assists statistic } } if (c2.energy <= 0) { c1.user2[0] = c1.user2[0] + 1; //increase kills statistic kill_c2 = 1; } } if (changed == 1) { writeTeamStatistics(); } //resolve kills if (kill_c1 == 1) c1.population.kill(c1); if (kill_c2 == 1) c2.population.kill(c2); } //function executed on collision with Food population group function onFoodCollision() { if (ExpParams.pickupNotStagnation == 1) idleSimulationSteps = 0; //collect HP Box (by eating it :D) print(Collision.Creature2.name + " [" + Collision.Creature2.population.name + "] picked a HP box"); Collision.Creature2.user2[4] = Collision.Creature2.user2[4] + 1; Collision.Creature1.energy_m += Collision.Creature1.energy; Collision.Creature2.energy_p += Collision.Creature1.energy; //kill HP Box Collision.Creature1.population.kill(Collision.Creature1); writeTeamStatistics(); } //function executed on collision with Upgrade population group function onUpgradesCollision() { if (ExpParams.pickupNotStagnation == 1) idleSimulationSteps = 0; //collect Upgrade Box (by eating it :D) print(Collision.Creature2.name + " [" + Collision.Creature2.population.name + "] picked an upgrade box"); Collision.Creature2.user2[5] = Collision.Creature2.user2[5] + 1; Collision.Creature1.energy_m += Collision.Creature1.energy; Collision.Creature2.user1[1] = Collision.Creature2.user1[1] + Collision.Creature1.energy; //kill Upgrade Box Collision.Creature1.population.kill(Collision.Creature1); writeTeamStatistics(); } //function executed on Creature death //TODO: implement some statistics for dieing creature function onDied(cr) { //ignore death of hp boxes and upgrades if (cr.population.index < 2) return ; print(cr.name + " was killed"); for (var dam in cr.user3) print(dam[1] + " dealt " + dam[2] + " damage"); var f=File.appendDirect("creatures"+battleNumber+".txt","Creature statistics"); //f.writeString("Creature name; Team; Kills; Assists; Total damage dealt; Total damage received; HP boxes collected; Upgrade boxes collected; Lifespan"); f.writeString(cr.name + "; " + cr.population.name + "; " + cr.user2[0] + "; " + cr.user2[1] + "; " + cr.user2[2] + "; " + cr.user2[3] + "; " + cr.user2[4] + "; " + cr.user2[5] + "; " + cr.lifespan + "\n"); f.close(); writeTeamStatistics(); } // //////////////////////////// // ExpParams setters //reinit populations on change function ExpParams_teamCount_set() { initPopulations(); } function ExpParams_level_set() { initLevel(); } ~ # ################################ # Team prop: id:teamCount name:Number of teams in Deathmatch type:d 1 10 1 group:Team help:If set to 1, the number of teams will be equal to the number of genotypes in gene pool. prop: id:teamSize name:Number of creatures per team type:d 1 10 5 group:Team prop: id:teamSpawningAreaSize name:Spawning area size for team type:f 10 30 20 group:Team prop: id:creatureStartingEnergy name:Starting energy of creature type:f 0 1000 300 group:Team help:Base starting energy level for each creature in teams # ################################ # Attack parameters prop: id:diceSides name:Number of sides of dices type:f 1 10 1 group:Attack rules prop: id:diceCount name:Number of dices to roll per attack type:f 0 10 5 group:Attack rules prop: id:creatureDamage name:Basic damage multiplied by result of dice roll type:f 0 100 10 group:Attack rules prop: id:friendlyFireDamageMultiplier name:Multiplier of energy taken when Creatures of the same team collide type:f 0 1 0.0 group:Attack rules help:Set to 0 for no friendly fire, set to 1 if full damage should be dealt when interacting with team member prop: id:attackCooldown name:Number of simulation steps between two consecutive attacks of a creature type:f 100 10000 1000 group:Attack rules help:Set this to 100 to nearly instantly resolve battles # ################################ # Collectibles - HP Boxes & Upgrades prop: id:pickupNotStagnation name:Collectible pick-up resets stagnation counter? type:d 0 1 1 help:If set to true picking up an item will restart counting towards stagnation, effects in longer battles. prop: id:hpBoxStartingEnergy name:Starting energy of HP Box type:f 0 1000 100 group:Collectibles help:Base starting energy level for HP Box prop: id:hpBoxIdleEnergy name:Amount of energy lost by HP Box type:f 0 10 0.00 group:Collectibles help:How much energy HP Box looses each step (0 - it will not disappear) prop: id:hpBoxProbability name:Probablity that new HP Box will spawn type:f 0 1 0.0005 group:Collectibles help:Probability of HP Box appearing each step of simulation prop: id:hpBoxTotalCount name:Maximum number of HP Boxes on the field type:d 0 20 10 group:Collectibles help:The total number of HP Boxes on the field will never exceed this number prop: id:upgradeBoxStartingEnergy name:Starting energy of Upgrade Box type:f 0 1000 500 group:Collectibles help:Base starting energy level for Upgrade Box prop: id:upgradeBoxIdleEnergy name:Amount of energy lost by Upgrade Box type:f 0 10 0.00 group:Collectibles help:How much energy Upgrade Box looses each step (0 - it will not disappear) prop: id:upgradeBoxProbability name:Probablity that new Upgrade Box will spawn type:f 0 1 0.0005 group:Collectibles help:Probability of Upgrade Box appearing each step of simulation prop: id:upgradeBoxTotalCount name:Maximum number of Upgrade Boxes on the field type:d 0 20 10 group:Collectibles help:The total number of Upgrade Boxes on the field will never exceed this number prop: id:upgradeBoxDecay name:How fast upgrade boost fades from creature type:f 0 10 0.1 group:Collectibles help:Each step creature damage bonus will be decreased by given amount until it reaches 0 prop: id:upgradeMultiplier name:Multiplier of bonus damage type:f 0 1 0.1 group:Collectibles help:Value of collected bonus multiplied by this variable is taken as bonus damage # ################################ # Other prop: id:level name:Level type:d -1 2 0 help:Number of a level to battle (-1 is random) prop: id:stagnationInterval name:Number of idle simulation steps before Sudden Death type:d 0 10000 5000 help:If no combat occurs during given number of steps, the Sudden Death mode is turned on (0 = Sudden Death all the time) prop: id:stagnationHealthReduce name:Sudden Death health reduce type:f 0 100 0.1 help:If Suddent Death is turned on, creatures will lose given amount of HP each simulation step prop: id:debug name:Show additional debug messages type:d 0 1 0