├── .gitignore ├── Classes └── autofight │ ├── Abstracts │ └── Unit.php │ ├── Army.php │ ├── BattleResult.php │ ├── Infantry.php │ ├── Interfaces │ ├── BattleLogger.php │ └── Unit.php │ ├── Loggers │ ├── LoggerCli.php │ └── LoggerWeb.php │ ├── Tank.php │ └── War.php ├── LICENSE.md ├── README.md ├── apple-touch-icon-114x114-precomposed.png ├── apple-touch-icon-57x57-precomposed.png ├── apple-touch-icon-72x72-precomposed.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── autoload.php ├── crossdomain.xml ├── favicon.ico ├── favicon.png ├── index.php ├── robots.txt └── utility_methods.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Wildcard .gitignore file I use in every project. 2 | 3 | # Local configuration 4 | config_local.php 5 | 6 | # Composer 7 | vendor 8 | vendor/* 9 | !public/js/vendor 10 | !public/js/vendor/* 11 | composer.phar 12 | composer.lock 13 | 14 | # Project folders and files 15 | Storage/ 16 | Storage/* 17 | 18 | # IntelliJ - PhpStorm and PyCharm 19 | .idea 20 | .idea/ 21 | .idea/* 22 | *.iml 23 | *.ipr 24 | *.iws 25 | 26 | # Logs 27 | error.log 28 | access.log 29 | 30 | # Netbeans 31 | nbproject 32 | .nbproject 33 | .nbproject/* 34 | nbproject/* 35 | nbproject/private/ 36 | build/ 37 | nbbuild/ 38 | dist/ 39 | nbdist/ 40 | nbactions.xml 41 | nb-configuration.xml 42 | 43 | # Mac OSX 44 | .DS_Store 45 | # Thumbnails 46 | ._* 47 | # Files that might appear on external disk 48 | .Spotlight-V100 49 | .Trashes 50 | 51 | # SublimeText project files 52 | /*.sublime-project 53 | *.sublime-workspace 54 | -------------------------------------------------------------------------------- /Classes/autofight/Abstracts/Unit.php: -------------------------------------------------------------------------------- 1 | iAccuracy; 50 | } 51 | 52 | /** 53 | * Returns number of remaining hit points 54 | * @return int 55 | */ 56 | function getHealth() 57 | { 58 | return $this->iHealth; 59 | } 60 | 61 | /** 62 | * Increases number of hit points by given value, or by 1 if no value 63 | * given. 64 | * @param null|int $iIncrease 65 | * @return mixed 66 | */ 67 | function increaseHealth($iIncrease = null) 68 | { 69 | $this->iHealth += ($iIncrease === null) ? 1 : abs((int)$iIncrease); 70 | return $this; 71 | } 72 | 73 | /** 74 | * Decreases number of hit points by given value, or by 1 if no value 75 | * given. 76 | * @param null $iDecrease 77 | * @return mixed 78 | */ 79 | function decreaseHealth($iDecrease = null) 80 | { 81 | $this->iHealth -= ($iDecrease === null) ? 1 : abs((int)$iDecrease); 82 | return $this; 83 | } 84 | 85 | /** 86 | * Returns true if the Unit is still alive, false otherwise 87 | * @return bool 88 | */ 89 | function isAlive() 90 | { 91 | return !($this->iHealth <= 0); 92 | } 93 | 94 | /** 95 | * Returns maximum number of surrounding units that can be affected by 96 | * a single shot from this Unit. 97 | * For example, a tank would have a radius of 3, which means 3 from each 98 | * side of original target (total 6), whereas an infantry Unit's rifle would only 99 | * have a radius of 0 (single target). 100 | * @return int 101 | */ 102 | function getRadius() 103 | { 104 | return $this->iRadius; 105 | } 106 | 107 | /** 108 | * Returns type of unit, e.g. "infantry", "tank", etc. 109 | * @return string 110 | */ 111 | function getType() 112 | { 113 | return $this->sType; 114 | } 115 | 116 | /** 117 | * Returns the maximum amount of damage a hit from this Unit can cause 118 | * @return int 119 | */ 120 | function getDamage() 121 | { 122 | return $this->iDamage; 123 | } 124 | 125 | /** 126 | * Sets army for unit when unit is added to army. 127 | * Enables easier chaining. 128 | * 129 | * @param Army $oArmy 130 | * @return $this 131 | */ 132 | function setArmy(Army $oArmy) 133 | { 134 | $this->oArmy = $oArmy; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Returns the unit's army 140 | * @return Army 141 | */ 142 | function getArmy() 143 | { 144 | return $this->oArmy; 145 | } 146 | 147 | /** 148 | * Sets the Unit's position in the army. Automatic. Ranging from 1 - army size. 149 | * @param int $iIndex 150 | * @return $this 151 | */ 152 | function setIndex($iIndex) 153 | { 154 | $this->iIndex = (int)$iIndex; 155 | return $this; 156 | } 157 | 158 | /** 159 | * Returns the Unit's position in the army 160 | * @return int 161 | */ 162 | function getIndex() 163 | { 164 | return $this->iIndex; 165 | } 166 | 167 | /** 168 | * Returns a random message from the messages property, depending 169 | * on hit score. 170 | * @param $iHitScore 171 | * @return mixed 172 | */ 173 | function determineMessage($iHitScore) { 174 | foreach ($this->aMessages as $iScore => $aMessages) { 175 | if ($iHitScore > $iScore) { 176 | continue; 177 | } else { 178 | return $aMessages[rand(0, count($aMessages) - 1)]; 179 | } 180 | } 181 | return '-->'; 182 | } 183 | 184 | /** 185 | * Returns a random array element 186 | * @param array $aArray 187 | * @return mixed 188 | */ 189 | protected function getRandomElement(array $aArray) { 190 | if (!empty($aArray)) { 191 | return $aArray[rand(0, count($aArray) - 1)]; 192 | } 193 | return null; 194 | } 195 | 196 | /** 197 | * Returns the name of the unit, tagged with index and army name 198 | * @return string 199 | */ 200 | function __toString() { 201 | return ucfirst($this->getType()).' unit '.$this->getIndex().' ('.$this->getArmy()->getLabel().')'; 202 | } 203 | } -------------------------------------------------------------------------------- /Classes/autofight/Army.php: -------------------------------------------------------------------------------- 1 | getType()] = $oUnit; 41 | } 42 | 43 | /** 44 | * The constructor takes a size parameter which is used to auto generate 45 | * the roster of the army. 46 | * @see Army::buildArmy() 47 | * @param $iSize 48 | */ 49 | public function __construct($iSize) 50 | { 51 | $this->setSize((int)$iSize); 52 | $this->buildArmy(); 53 | } 54 | 55 | /** 56 | * Sets an army label, for legibility in battle output and personalization 57 | * @param $sLabel 58 | * @return $this|Army 59 | */ 60 | public function setLabel($sLabel) 61 | { 62 | if (!is_string($sLabel) && !is_numeric($sLabel)) { 63 | die('A label must be a string or a number. "' . $sLabel . '" given.'); 64 | } 65 | $this->sLabel = $sLabel; 66 | return $this; 67 | } 68 | 69 | /** 70 | * Returns the defined army label 71 | * @return string 72 | */ 73 | public function getLabel() 74 | { 75 | return $this->sLabel; 76 | } 77 | 78 | /** 79 | * Builds an army according to size and unit rarity. 80 | * @return $this 81 | */ 82 | protected function buildArmy() 83 | { 84 | if (empty(self::$aUnitTypes)) { 85 | die('No unit types have been registered in the Army class.'); 86 | } 87 | $iRarityTotal = 0; 88 | $aRandomnessArray = array(); 89 | /** @var Unit $oUnit */ 90 | foreach (self::$aUnitTypes as $k => $oUnit) { 91 | $iRarityTotal += $oUnit->getRarity(); 92 | $aRandomnessArray[$k] = $iRarityTotal; 93 | } 94 | for ($i = 1; $i <= $this->getSize(); $i++) { 95 | $iRand = rand(1, $iRarityTotal); 96 | foreach ($aRandomnessArray as $k => $iScore) { 97 | if ($iRand > $iScore) { 98 | continue; 99 | } else if ($iRand <= $iScore) { 100 | $oUnit = clone self::$aUnitTypes[$k]; 101 | $oUnit->setArmy($this); 102 | break; 103 | } 104 | } 105 | $iIndex = count($this->aUnits); 106 | $this->aUnits[$iIndex] = $oUnit->setIndex($iIndex); 107 | } 108 | return $this; 109 | } 110 | 111 | /** 112 | * Returns a random living unit from the army 113 | * @param \autofight\Interfaces\Unit|null $oNotUnit 114 | * @return Unit 115 | */ 116 | public function getRandomAliveUnit(Unit $oNotUnit = null) 117 | { 118 | $aLivingUnits = array(); 119 | /** @var Unit $oUnit */ 120 | foreach ($this->aUnits as $oUnit) { 121 | if ($oUnit->isAlive()) { 122 | if (!$oNotUnit || ($oNotUnit && $oNotUnit->getIndex() != $oUnit->getIndex())) { 123 | $aLivingUnits[] = $oUnit; 124 | } 125 | } 126 | } 127 | $i = rand(0, count($aLivingUnits) - 1); 128 | return (isset($aLivingUnits[$i])) ? $aLivingUnits[$i] : null; 129 | } 130 | 131 | /** 132 | * Returns neighbor units in given range (radius). 133 | * For example, given a Unit X and 1 as range, will return unit 134 | * to the left of X AND to the right of X, if they exist (even if dead). 135 | * If you provide a side argument, only that side is returned. 136 | * So for Unit X, range 1, side left, only the ONE unit to the left of X is returned. 137 | * 138 | * @param Unit $oUnit 139 | * @param int $iRange 140 | * @param string $sSide 141 | * @return array 142 | */ 143 | public function getAdjacentUnits(Unit $oUnit, $iRange = 1, $sSide = 'both') 144 | { 145 | $aAdjacent = array(); 146 | while ($iRange > 0) { 147 | if ($sSide == 'both' || $sSide == 'left') { 148 | if (isset($this->aUnits[$oUnit->getIndex() - $iRange])) { 149 | $aAdjacent[] = $this->aUnits[$oUnit->getIndex() - $iRange]; 150 | } 151 | } 152 | if ($sSide == 'both' || $sSide == 'right') { 153 | if (isset($this->aUnits[$oUnit->getIndex() + $iRange])) { 154 | $aAdjacent[] = $this->aUnits[$oUnit->getIndex() + $iRange]; 155 | } 156 | } 157 | $iRange--; 158 | } 159 | return array_reverse($aAdjacent); 160 | } 161 | 162 | /** 163 | * Returns units 164 | * @return array 165 | */ 166 | public function getUnits() 167 | { 168 | return $this->aUnits; 169 | } 170 | 171 | /** 172 | * The size is used to auto generate the army roster. 173 | * @see Army::buildArmy() 174 | * @param $iSize 175 | * @return $this 176 | */ 177 | protected function setSize($iSize) 178 | { 179 | if (!is_numeric($iSize) || $iSize < 1) { 180 | die('Army construct param needs to be numeric and positive. 181 | "' . $iSize . '" is not.'); 182 | } 183 | 184 | $this->iSize = $iSize; 185 | return $this; 186 | } 187 | 188 | /** 189 | * Returns defined army size. 190 | * @return int 191 | */ 192 | public function getSize() 193 | { 194 | return $this->iSize; 195 | } 196 | 197 | /** 198 | * Counts number of remaining alive troops 199 | * @return int 200 | */ 201 | public function countAlive() 202 | { 203 | $i = 0; 204 | /** @var Unit $oUnit */ 205 | foreach ($this->aUnits as &$oUnit) { 206 | $i += (int)$oUnit->isAlive(); 207 | } 208 | return $i; 209 | } 210 | 211 | /** 212 | * Generates a random army name. If there are no more random names to generate, 213 | * generates a random numeric ID in the range 1 - 1000 214 | * @return string 215 | */ 216 | public function generateRandomLabel() 217 | { 218 | if (empty(self::$aAdjectives) || empty(self::$aNouns)) { 219 | return (string)rand(0, 1000); 220 | } 221 | 222 | $iAdjective = rand(0, count(self::$aAdjectives) - 1); 223 | $iNoun = rand(0, count(self::$aNouns) - 1); 224 | $sLabel = self::$aAdjectives[$iAdjective] . ' ' . self::$aNouns[$iNoun]; 225 | 226 | /** Remove picked values and reset array keys */ 227 | unset(self::$aAdjectives[$iAdjective], self::$aNouns[$iNoun]); 228 | self::$aNouns = array_values(self::$aNouns); 229 | self::$aAdjectives = array_values(self::$aAdjectives); 230 | 231 | return $sLabel; 232 | } 233 | 234 | } -------------------------------------------------------------------------------- /Classes/autofight/BattleResult.php: -------------------------------------------------------------------------------- 1 | array( 21 | 'brings eternal shame to his family', 22 | 'critically misses', 23 | 'shoots at the sky', 24 | 'shoots himself in the foot', 25 | 'jams his rifle', 26 | 'has a bullet explode in his rifle', 27 | 'breaks his rifle in half', 28 | 'hits himself in the head with recoil' 29 | ), 30 | 20 => array( 31 | 'misses badly', 32 | 'fails miserably', 33 | 'shoots like a blind five year old' 34 | ), 35 | 40 => array( 36 | 'shoots clumsily', 37 | 'misses by a yardstick', 38 | 'appears to be seeing double' 39 | ), 40 | 50 => array( 41 | 'shoots too low', 42 | 'shoots too high', 43 | 'shoots the ground' 44 | ), 45 | 60 => array( 46 | 'slightly wounds', 47 | 'grazes', 48 | 'pokes' 49 | ), 50 | 80 => array( 51 | 'wounds', 52 | 'hits', 53 | 'pierces' 54 | ), 55 | 99 => array( 56 | 'hits well', 57 | 'hits hard', 58 | 'badly wounds' 59 | ), 60 | 100 => array( 61 | 'critically hits', 62 | 'pulverizes', 63 | 'destroys', 64 | 'obliterates', 65 | 'critically wounds' 66 | ) 67 | ); 68 | 69 | /** @var array */ 70 | protected $aSuicideMessages = array( 71 | 'grew tired of it all and decided to end it', 72 | 'couldn\'t handle the killing', 73 | 'didn\'t have the stomach for war', 74 | 'gave up on life', 75 | 'killed himself', 76 | 'swallowed his own bullet', 77 | 'sat on a grenade, pulled out the pin, and waited', 78 | 'stepped on a land mine. On purpose' 79 | ); 80 | 81 | /** @var array */ 82 | protected $aIdleMessages = array( 83 | 'didn\'t feel like participating', 84 | 'went to sleep', 85 | 'was too depressed to hold the rifle', 86 | 'sat down and looked the other way', 87 | 'started crying', 88 | 'decided to clean his rifle', 89 | 'went to call his wife', 90 | 'couldn\'t stop looking at his girlfriend\'s picture', 91 | 'went to grab something to eat' 92 | ); 93 | 94 | /** @var array */ 95 | protected $aFriendlyFireMessages = array( 96 | 'went insane and attacked his own', 97 | 'went crazy and aimed at his friend', 98 | 'couldn\'t handle it and decided to attack the platoon leader', 99 | 'became too depressed to aim at enemies, and chose friends instead', 100 | 'decided to switch sides, for the time being', 101 | 'went mad and switched sides temporarily' 102 | ); 103 | 104 | /** @var int */ 105 | protected static $rarity = 100; 106 | 107 | /** @var int */ 108 | protected $iHealth = 100; 109 | 110 | /** @var int */ 111 | protected $iMaxHealth = 100; 112 | 113 | /** @var int */ 114 | protected $iAccuracy = 50; 115 | 116 | /** @var int */ 117 | protected $iRadius = 0; 118 | 119 | /** @var string */ 120 | protected $sType = 'infantry'; 121 | 122 | /** @var int */ 123 | protected $iDamage = 10; 124 | 125 | /** 126 | * When a unit acts, he performs his default action. 127 | * A soldier will aim and fire, a tank might move and fire, a medic will heal, and so on. 128 | * @param Army $oAttackedArmy 129 | * @return array 130 | */ 131 | public function act(Army $oAttackedArmy) 132 | { 133 | $aResults = array(); 134 | $oResult = new BattleResult(); 135 | $oResult->attacker = $this; 136 | $oResult->amount = 0; 137 | 138 | // Insanity 139 | if (rand(1, 1000000) == 1) { 140 | $oResult->type = BattleLogger::TYPE_INSANE; 141 | switch (rand(0, 2)) { 142 | case 0: 143 | // Suicidal 144 | $this->iHealth = 0; 145 | $oResult->defender = $this; 146 | $oResult->message = $this->getRandomElement($this->aSuicideMessages); 147 | $oResult->amount = 1000; 148 | break; 149 | case 1: 150 | // Idle 151 | $oResult->defender = $this; 152 | $oResult->message = $this->getRandomElement($this->aIdleMessages); 153 | 154 | break; 155 | case 2: 156 | // Friendly fire 157 | $aResults[] = $this.' '.$this->getRandomElement($this->aFriendlyFireMessages).'!'; 158 | $oAttackedUnit = $this->getArmy()->getRandomAliveUnit($this); 159 | $aResults = array_merge($aResults, $this->shoot($oAttackedUnit)); 160 | break; 161 | default: 162 | break; 163 | } 164 | } else { 165 | // No insanity, continue as planned 166 | $oAttackedUnit = $oAttackedArmy->getRandomAliveUnit(); 167 | if ($oAttackedUnit) { 168 | return $this->shoot($oAttackedUnit); 169 | } else { 170 | return array(); 171 | } 172 | } 173 | 174 | $aResults[] = $oResult; 175 | return $aResults; 176 | } 177 | 178 | /** 179 | * Shoots at a given unit. Hit or miss depends on accuracy and other 180 | * factors. Can shoot at self and commit suicide with a 100% success rate. 181 | * @param \autofight\Interfaces\Unit $oUnit 182 | * @return mixed 183 | */ 184 | public function shoot(iUnit $oUnit) 185 | { 186 | $aResults = array(); 187 | $oResult = new BattleResult(); 188 | $oResult->attacker = $this; 189 | $oResult->defender = $oUnit; 190 | 191 | // Calculate hit or miss. 192 | $iHitScore = rand(1, 100); 193 | $bHit = $iHitScore >= $this->iAccuracy; 194 | 195 | $oResult->type = ($bHit) ? BattleLogger::TYPE_HIT : BattleLogger::TYPE_MISS; 196 | $oResult->message = $this->determineMessage($iHitScore); 197 | 198 | $fPercentageOfAccuracy = $iHitScore / $this->iAccuracy * 100; 199 | if (!$bHit) { 200 | $iAmount = 0; 201 | // MISS 202 | if ($fPercentageOfAccuracy > 50 && $fPercentageOfAccuracy < 60) { 203 | /* 204 | If the hit score was between 50% and 60% of accuracy 205 | there's a chance the adjacent trooper was hit. 206 | */ 207 | $aAdjacent = $this->getArmy()->getAdjacentUnits($this, 1); 208 | if (!empty($aAdjacent)) { 209 | $oUnitToShootAt = $this->getRandomElement($aAdjacent); 210 | if ($oUnitToShootAt) { 211 | $aResults[] = $this.' aims at '.$oUnit.' but bullet strays towards '.$oUnitToShootAt.'!'; 212 | $aPostMerge = $this->shoot($oUnitToShootAt); 213 | } 214 | } 215 | } else if ($iHitScore == 1) { 216 | // CRITICAL MISS 217 | switch (rand(0, 1)) { 218 | case 0: 219 | // Reduce accuracy by 10 220 | $this->iAccuracy = ($this->iAccuracy < 11) ? 1 : ($this->iAccuracy - 10); 221 | $sAddedMessage = $this.' has suffered a permanent reduction of accuracy!'; 222 | break; 223 | case 1: 224 | // Reduce health by 10 225 | $this->iHealth = ($this->iHealth < 11) ? 1 : ($this->iHealth - 10); 226 | $sAddedMessage = $this.' has suffered a permanent reduction of health!'; 227 | break; 228 | default: 229 | break; 230 | } 231 | } 232 | } else { 233 | // HIT 234 | if ($iHitScore == 100) { 235 | // CRITICAL HIT 236 | $iAmount = $this->iDamage * 5; 237 | $aResults[] = $this.' scored a critical hit!!'; 238 | } else { 239 | $iAmount = $this->iDamage * $iHitScore / 100; 240 | } 241 | } 242 | $oResult->amount = $iAmount; 243 | 244 | $oUnit->decreaseHealth($iAmount); 245 | if (!$oUnit->isAlive()) { 246 | $oResult->message = 'kills'; 247 | $oResult->type = BattleLogger::TYPE_DEATH; 248 | } 249 | 250 | $aResults[] = $oResult; 251 | if (isset($sAddedMessage)) { 252 | $aResults[] = $sAddedMessage; 253 | } 254 | $aPostMerge = (isset($aPostMerge)) ? $aPostMerge : array(); 255 | $aResults = array_merge($aResults, $aPostMerge); 256 | return $aResults; 257 | } 258 | 259 | /** 260 | * Rarity is the chance of getting this unit in a random draw of units. 261 | * A bigger number means more chance to appear. 262 | * @return int 263 | */ 264 | static function getRarity() 265 | { 266 | return self::$rarity; 267 | } 268 | 269 | } -------------------------------------------------------------------------------- /Classes/autofight/Interfaces/BattleLogger.php: -------------------------------------------------------------------------------- 1 | 'Hit', 22 | BattleLogger::TYPE_MISS => 'Miss', 23 | BattleLogger::TYPE_DEATH => 'Death', 24 | BattleLogger::TYPE_MOVE => 'Move', 25 | BattleLogger::TYPE_INSANE => 'Insanity', 26 | ); 27 | 28 | /** @var array The colors are color code expressions for the terminal */ 29 | protected $aColors = array( 30 | BattleLogger::TYPE_HIT => '0;32m', 31 | BattleLogger::TYPE_MISS => '0;31m', 32 | BattleLogger::TYPE_DEATH => '0;33m', 33 | BattleLogger::TYPE_MOVE => '0;37m', 34 | BattleLogger::TYPE_INSANE => '0;36m' 35 | ); 36 | 37 | /** 38 | * Logs a battle result 39 | * @param BattleResult $oResult 40 | * @return $this 41 | */ 42 | function logResult(BattleResult $oResult) 43 | { 44 | 45 | $sMessage = "\033[".$this->aColors[$oResult->type].' '.$this->aPrefixes[$oResult->type].'! '; 46 | $sMessage .= $oResult->attacker.' '; 47 | switch ($oResult->type) { 48 | case (BattleLogger::TYPE_HIT) : 49 | $sMessage .= $oResult->message.'. '.ucfirst($oResult->defender); 50 | $sMessage .= ' takes '.$oResult->amount.' damage.'; 51 | break; 52 | case (BattleLogger::TYPE_MISS) : 53 | $sMessage .= $oResult->message.'. '.ucfirst($oResult->defender); 54 | $sMessage .= ' is safe.'; 55 | break; 56 | case (BattleLogger::TYPE_DEATH) : 57 | $sMessage .= 'causes '.$oResult->amount.' damage and '.$oResult->message.' '.ucfirst($oResult->defender).'!!'; 58 | break; 59 | default: 60 | break; 61 | } 62 | 63 | print $sMessage."\033[".$this->aColors[$oResult->type]." \033[1;37m".PHP_EOL; 64 | usleep(500000); 65 | return $this; 66 | } 67 | 68 | /** 69 | * Logs a misc message 70 | * @param string $sMessage 71 | * @return BattleLogger 72 | */ 73 | function logOther($sMessage) { 74 | print $sMessage.PHP_EOL; 75 | usleep(500000); 76 | return $this; 77 | } 78 | 79 | /** 80 | * Logs an array of passed results, automatically picking the proper method 81 | * @param $aResults 82 | * @return $this 83 | */ 84 | function logMultiple(array $aResults) { 85 | foreach ($aResults as $oResult) { 86 | if (is_string($oResult)) { 87 | $this->logOther($oResult); 88 | } else if ($oResult instanceof BattleResult) { 89 | $this->logResult($oResult); 90 | } 91 | } 92 | return $this; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /Classes/autofight/Loggers/LoggerWeb.php: -------------------------------------------------------------------------------- 1 | 'Hit', 21 | BattleLogger::TYPE_MISS => 'Miss', 22 | BattleLogger::TYPE_DEATH => 'Death', 23 | BattleLogger::TYPE_MOVE => 'Move', 24 | BattleLogger::TYPE_INSANE => 'Insanity' 25 | ); 26 | 27 | /** @var array */ 28 | protected $aStyles = array( 29 | BattleLogger::TYPE_HIT => 'color: green', 30 | BattleLogger::TYPE_MISS => 'color: red', 31 | BattleLogger::TYPE_DEATH => 'color: black; text-decoration: underline', 32 | BattleLogger::TYPE_MOVE => 'color: grey', 33 | BattleLogger::TYPE_INSANE => 'color: orange; font-weight:bold; text-decoration: underline' 34 | ); 35 | 36 | /** 37 | * Logs a misc message 38 | * @param string $sMessage 39 | * @return BattleLogger 40 | */ 41 | function logOther($sMessage) 42 | { 43 | echo '

' . $sMessage . '

'; 44 | return $this; 45 | } 46 | 47 | /** 48 | * Logs a battle result 49 | * @param BattleResult $oResult 50 | * @return $this 51 | */ 52 | function logResult(BattleResult $oResult) 53 | { 54 | $sMessage = '

'.$this->aPrefixes[$oResult->type].'!

'; 55 | $sMessage .= $oResult->attacker.' '; 56 | switch ($oResult->type) { 57 | case (BattleLogger::TYPE_HIT) : 58 | $sMessage .= $oResult->message.'. '.ucfirst($oResult->defender); 59 | $sMessage .= ' takes '.$oResult->amount.' damage.'; 60 | break; 61 | case (BattleLogger::TYPE_MISS) : 62 | $sMessage .= $oResult->message.'. '.ucfirst($oResult->defender); 63 | $sMessage .= ' is safe.'; 64 | break; 65 | case (BattleLogger::TYPE_DEATH) : 66 | $sMessage .= 'causes '.$oResult->amount.' damage and '.$oResult->message.' '.ucfirst($oResult->defender).'!!'; 67 | break; 68 | default: 69 | break; 70 | } 71 | echo $sMessage.'

'; 72 | return $this; 73 | } 74 | 75 | /** 76 | * Logs an array of passed results, automatically picking the proper method 77 | * @param $aResults 78 | * @return $this 79 | */ 80 | function logMultiple(array $aResults) { 81 | foreach ($aResults as $oResult) { 82 | if (is_string($oResult)) { 83 | $this->logOther($oResult); 84 | } else if ($oResult instanceof BattleResult) { 85 | $this->logResult($oResult); 86 | } 87 | } 88 | return $this; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /Classes/autofight/Tank.php: -------------------------------------------------------------------------------- 1 | array( 21 | 'brings eternal shame to his family', 22 | 'critically misses', 23 | 'shoots at the sky', 24 | 'has a projectile explode in the pipe', 25 | 'breaks the tank tracks', 26 | 'drops the projectile on the driver\'s head while reloading', 27 | 'dents the turret', 28 | 'cracks the turret on a tree' 29 | ), 30 | 20 => array( 31 | 'misses badly', 32 | 'fails miserably', 33 | 'shoots like a blind five year old' 34 | ), 35 | 40 => array( 36 | 'shoots clumsily', 37 | 'misses by a yardstick', 38 | 'appears to be seeing double' 39 | ), 40 | 50 => array( 41 | 'shoots too low', 42 | 'shoots too high', 43 | 'shoots the ground' 44 | ), 45 | 60 => array( 46 | 'slightly wounds', 47 | 'grazes', 48 | 'pokes' 49 | ), 50 | 80 => array( 51 | 'wounds', 52 | 'hits', 53 | 'pierces' 54 | ), 55 | 99 => array( 56 | 'hits well', 57 | 'hits hard', 58 | 'badly wounds' 59 | ), 60 | 100 => array( 61 | 'critically hits', 62 | 'pulverizes', 63 | 'destroys', 64 | 'obliterates', 65 | 'critically wounds' 66 | ) 67 | ); 68 | 69 | /** @var int */ 70 | protected static $rarity = 10; 71 | 72 | /** @var int */ 73 | protected $iHealth = 500; 74 | 75 | /** @var int */ 76 | protected $iMaxHealth = 500; 77 | 78 | /** @var int */ 79 | protected $iAccuracy = 35; 80 | 81 | /** @var int */ 82 | protected $iRadius = 3; 83 | 84 | /** @var int */ 85 | protected $iDamage = 50; 86 | 87 | /** @var string */ 88 | protected $sType = 'tank'; 89 | 90 | /** 91 | * @param Army $oAttackedArmy 92 | * @return array 93 | */ 94 | public function act(Army $oAttackedArmy) 95 | { 96 | if ($oAttackedArmy->countAlive()) { 97 | return $this->shoot($oAttackedArmy->getRandomAliveUnit()); 98 | } 99 | return array(); 100 | } 101 | 102 | 103 | /** 104 | * Shoots at a given unit. Hit or miss depends on accuracy and other 105 | * factors. Can shoot at self and commit suicide with a 100% success rate. 106 | * @param \autofight\Interfaces\Unit $oUnit 107 | * @return mixed 108 | */ 109 | public function shoot(iUnit $oUnit) 110 | { 111 | $aResults = array(); 112 | $oResult = new BattleResult(); 113 | $oResult->attacker = $this; 114 | $oResult->defender = $oUnit; 115 | 116 | $aPostMerge = array(); 117 | 118 | // Calculate hit or miss. 119 | $iHitScore = rand(1, 100); 120 | $bHit = $iHitScore >= $this->iAccuracy; 121 | 122 | $oResult->type = ($bHit) ? BattleLogger::TYPE_HIT : BattleLogger::TYPE_MISS; 123 | $oResult->message = $this->determineMessage($iHitScore); 124 | 125 | $fPercentageOfAccuracy = $iHitScore / $this->iAccuracy * 100; 126 | if (!$bHit) { 127 | $iAmount = 0; 128 | // MISS 129 | if ($fPercentageOfAccuracy > 50 && $fPercentageOfAccuracy < 60) { 130 | /* 131 | If the hit score was between 50% and 60% of accuracy 132 | there's a chance the adjacent trooper was hit. 133 | */ 134 | $aAdjacent = $this->getArmy()->getAdjacentUnits($this, 2); 135 | if (!empty($aAdjacent)) { 136 | $oUnitToShootAt = $this->getRandomElement($aAdjacent); 137 | if ($oUnitToShootAt) { 138 | $aResults[] = $this.' aims at '.$oUnit.' but projectile strays towards '.$oUnitToShootAt.'!'; 139 | $aPostMerge = $this->shoot($oUnitToShootAt); 140 | } 141 | } 142 | } else if ($iHitScore == 1) { 143 | // CRITICAL MISS 144 | switch (rand(0, 1)) { 145 | case 0: 146 | // Reduce accuracy by 10 147 | $this->iAccuracy = ($this->iAccuracy < 11) ? 1 : ($this->iAccuracy - 10); 148 | $sAddedMessage = $this . ' has suffered a permanent reduction of accuracy!'; 149 | break; 150 | case 1: 151 | // Reduce health by 10 152 | $this->iHealth = ($this->iHealth < 11) ? 1 : ($this->iHealth - 10); 153 | $sAddedMessage = $this . ' has suffered a permanent reduction of health!'; 154 | break; 155 | default: 156 | break; 157 | } 158 | } 159 | } else { 160 | // HIT 161 | if ($iHitScore == 100) { 162 | // CRITICAL HIT 163 | $iAmount = $this->iDamage * 5; 164 | $aResults[] = $this . ' scored a critical hit!!'; 165 | } else { 166 | $iAmount = $this->iDamage * $iHitScore / 100; 167 | } 168 | 169 | /** 170 | * SHRAPNEL implementation 171 | */ 172 | $aAdjacent = $oUnit->getArmy()->getAdjacentUnits($oUnit, $this->getRadius()); 173 | $aPostMerge[] = 'Splash Damage!'; 174 | /** @var \autofight\Interfaces\Unit $oAdjacentUnit */ 175 | foreach ($aAdjacent as $oAdjacentUnit) { 176 | if ($oAdjacentUnit->isAlive()) { 177 | $iAmountToReduce = round($iAmount * ($this->getRadius() - abs($oUnit->getIndex()-$oAdjacentUnit->getIndex())) / ($this->getRadius()*2) + 1, 2); 178 | $oAdjacentUnit->decreaseHealth($iAmountToReduce); 179 | if ($oAdjacentUnit->isAlive()) { 180 | $aPostMerge[] = $oAdjacentUnit.' was hit by shrapnel for '.$iAmountToReduce.' damage.'; 181 | } else { 182 | $aPostMerge[] = $oAdjacentUnit.' was hit by shrapnel for '.$iAmountToReduce.' damage and perished.'; 183 | } 184 | } else { 185 | $aPostMerge[] = 'The corpse of '.$oAdjacentUnit.' is mutilated by shrapnel.'; 186 | } 187 | } 188 | 189 | } 190 | 191 | $oUnit->decreaseHealth($iAmount); 192 | if (!$oUnit->isAlive()) { 193 | $oResult->message = 'kills'; 194 | $oResult->type = BattleLogger::TYPE_DEATH; 195 | } 196 | $oResult->amount = $iAmount; 197 | 198 | $aResults[] = $oResult; 199 | if (isset($sAddedMessage)) { 200 | $aResults[] = $sAddedMessage; 201 | } 202 | $aResults = array_merge($aResults, $aPostMerge); 203 | return $aResults; 204 | } 205 | 206 | /** 207 | * Rarity is the chance of getting this unit in a random draw of units. 208 | * A bigger number means more chance to appear. 209 | * @return int 210 | */ 211 | static function getRarity() 212 | { 213 | return self::$rarity; 214 | } 215 | 216 | } -------------------------------------------------------------------------------- /Classes/autofight/War.php: -------------------------------------------------------------------------------- 1 | oLogger = $oLogger; 33 | return $this; 34 | } 35 | 36 | /** 37 | * Adds an Army into the War. 38 | * A War needs at least two armies, but can have more (chaos!!) 39 | * An Army can also have a label for more personalized battle text output. 40 | * If the label is omitted, the numeric index of the array is used. 41 | * 42 | * @param Army $oArmy 43 | * @internal param null|string $sLabel 44 | * @return $this 45 | */ 46 | public function addArmy(Army $oArmy) 47 | { 48 | if ($oArmy->getLabel() === null) { 49 | $oArmy->setLabel($oArmy->generateRandomLabel()); 50 | } 51 | $this->aArmies[] = $oArmy; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Starts the battle. 57 | * The War object will call doTurn until there is only one living army left. 58 | */ 59 | public function fight() 60 | { 61 | if (count($this->aArmies) < 2) { 62 | die("War. War never changes. ... And as such it needs at least 2 armies!"); 63 | } 64 | 65 | while ($this->moreThanOneAliveArmy()) { 66 | $this->doTurn(); 67 | } 68 | 69 | $this->oLogger->logOther($this->getSurvivingArmy()->getLabel() . ' wins!'); 70 | } 71 | 72 | /** 73 | * Returns true if there's more than one army that can still fight. 74 | * @return bool 75 | */ 76 | protected function moreThanOneAliveArmy() 77 | { 78 | $iAlive = 0; 79 | /** @var Army $oArmy */ 80 | foreach ($this->aArmies as $oArmy) { 81 | $iAlive += (int)(bool)$oArmy->countAlive(); 82 | } 83 | return $iAlive > 1; 84 | } 85 | 86 | /** 87 | * Returns the surviving army. 88 | * @return Army|null 89 | */ 90 | protected function getSurvivingArmy() 91 | { 92 | /** @var Army $oArmy */ 93 | foreach ($this->aArmies as $oArmy) { 94 | if ($oArmy->countAlive()) { 95 | return $oArmy; 96 | } 97 | } 98 | return null; 99 | } 100 | 101 | /** 102 | * The main driver of the application.. 103 | */ 104 | protected function doTurn() 105 | { 106 | /** @var Army $oArmy */ 107 | /** @var Unit $oUnit */ 108 | 109 | $this->iTurns++; 110 | 111 | /** 112 | * Randomize order of armies, 113 | * each turn a different one has the chance of going first 114 | */ 115 | shuffle($this->aArmies); 116 | 117 | $this->oLogger->logOther('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 118 | $this->oLogger->logOther('Turn ' . $this->iTurns . ' begins.'); 119 | 120 | foreach ($this->aArmies as $i => $oArmy) { 121 | $this->oLogger->logOther('Army ' . $oArmy->getLabel() . ' goes ' . ordinal($i + 1)); 122 | } 123 | 124 | foreach ($this->aArmies as $oArmy) { 125 | // Units currently execute moves from first to last. 126 | // @todo implement unit initiative 127 | 128 | if ($this->moreThanOneAliveArmy()) { 129 | 130 | /** @var Army $oAttackedArmy */ 131 | $oAttackedArmy = $this->findAttackableArmy($oArmy); 132 | $this->oLogger->logOther( 133 | 'Army "' . $oArmy->getLabel() . '" attacks "' . $oAttackedArmy->getLabel() . '".' 134 | ); 135 | 136 | foreach ($oArmy->getUnits() as $oUnit) { 137 | if ($oUnit->isAlive()) { 138 | $aResults = $oUnit->act($oAttackedArmy); 139 | if (!empty($aResults)) { 140 | $aResults[] = '~'; 141 | } 142 | $this->oLogger->logMultiple($aResults); 143 | } 144 | } 145 | } 146 | } 147 | 148 | $this->oLogger->logOther('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 149 | 150 | } 151 | 152 | /** 153 | * Returns a random army that is NOT the army in the $oArmy argument. 154 | * Only armies with still living units can be returned 155 | * @param Army $oArmy 156 | * @return mixed 157 | */ 158 | protected function findAttackableArmy(Army $oArmy) 159 | { 160 | $aAttackable = array(); 161 | /** @var Army $oAvailableArmy */ 162 | foreach ($this->aArmies as $oAvailableArmy) { 163 | if ($oAvailableArmy->getLabel() != $oArmy->getLabel() 164 | && $oAvailableArmy->countAlive() 165 | ) { 166 | $aAttackable[] = $oAvailableArmy; 167 | } 168 | } 169 | if (isset($aAttackable[rand(0, count($aAttackable) - 1)])) { 170 | return $aAttackable[rand(0, count($aAttackable) - 1)]; 171 | } else { 172 | die('Could not find any attackable army. Looks like ' . $oArmy->getLabel() . ' wins.'); 173 | } 174 | } 175 | 176 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### License 2 | 3 | You cannot use this software for any purpose that causes a monetary transaction without prior approval from the author - including but not limited to online gambling sites, revenue from ads on the website hosting the project, microtransactions, tutoring people on the logic of the project for money, and so on. 4 | 5 | If you'd like to have some financial use of this project, let me know and we can arrange something. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Autofight 2 | 3 | This is a small task I was given by a prospective employer. Originally, I was supposed to only make two armies fight each other automatically with a little element of randomness, but I guess I got carried away and played around with it more than I had to. 4 | 5 | This is a completely useless no-GUI highly extensible text output "app". 6 | Use it only for educational purposes on OOP and how to write clean and neatly commented code. 7 | 8 | A detailed explanation on how and why this was built is available via [this tutorial](http://bit.ly/15lFkbM). 9 | 10 | ##What's going on here? 11 | First, two armies are generated with the number of units you give them. The armies have a random number of random types of units - one might be 100% infantry, the other might have 10 tanks alongside 40 infantry, and so on. There is support for more than 2 armies, but you need to manually add the army to the War object in index.php. Then, each army is given a randomly generated label, unless you hard code one, again in index.php. On every turn, the army order is randomized. This simulates initiative. An army picks the army to attack if there's more than two in the war (if not, it just picks the one that's not them), and all soldiers from the attacking army then make a move against the defending army, one by one, targeting a random alive opponent. Once they've all done their moves, the next army does the same - until there are no more armies left to move that turn. 12 | 13 | The "game" ends when all armies but one have zero survivors. There can be only one :) 14 | 15 | This is what the fight looks like: 16 | 17 | Sample 10 vs 10 fight 18 | 19 | ###Randomness 20 | There's an element of randomness in the game. Some of these random aspects are as follows: 21 | 22 | - a tank has 10% as much chance of appearing in an army as an infantry unit does. In other words, infantry units are 10 times more common. 23 | - a unit can miss, and if they miss in a specific miss range, can accidentally hit someone else 24 | - there's chance of critical hits, and critical misses. Critical hits do 5x damage, and critical misses permanently reduce either health or accuracy of the unit. A critical miss is akin to someone shooting themselves in the foot, or a projectile getting stuck in the tank's turret. 25 | - an infantry unit has 0.0001% chance of going insane. When insane, the unit may choose skip his turn, attack a fellow unit, or even commit suicide. 26 | - when no labels are given, armies get randomly generated names from hard coded adjectives and nouns. This makes for some interesting combinations like "Black Death" or "Lonely Marauders". 27 | - tanks have splash damage. If they score a hit, neighboring units suffer shrapnel damage. 28 | 29 | ##Usage 30 | You can test it out in CLI mode or via a web interface. 31 | In CLI, just run index.php X Y where X and Y are sizes of the first and second army respectively (e.g. php index.php 50 50). The army is auto-generated with the available units, and you should automatically start seeing output in your terminal, turn by turn. In the browser, the same applies, only the output is calculated and printed out at once, and you need to use a link like index.php?army1=50&army2=50. 32 | 33 | ##Requirements 34 | You need at least PHP 5.4 because the short array syntax is used and some other 5.4ish stuff. 35 | 36 | ##Fooling around and extending 37 | You can add new unit types, just implement the Unit interface or extend the Unit abstract. You can also modify the index.php file to accept more than 2 armies, the code already supports it. If you do mess with it, let me know what you end up with. I'm not really happy with how the logging turned out, I'd like something more flexible but didn't have time to fix it. PRs appreciated. 38 | 39 | For example, to implement a Medic class unit, you would extend the Unit abstract, and write an "act" method such that a fellow army unit is selected, and his Health increased depending on rolled score. 40 | To implement a different kind of logger - for example one that logs the result into a file, just implement the "BattleLogger" interface and plug it into the War object in index.php instead of the current one. 41 | -------------------------------------------------------------------------------- /apple-touch-icon-114x114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/apple-touch-icon-114x114-precomposed.png -------------------------------------------------------------------------------- /apple-touch-icon-57x57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/apple-touch-icon-57x57-precomposed.png -------------------------------------------------------------------------------- /apple-touch-icon-72x72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/apple-touch-icon-72x72-precomposed.png -------------------------------------------------------------------------------- /apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/apple-touch-icon.png -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/autofight/965cb058bd5cac8c8ffd305de63c28ef5ddfcf36/favicon.png -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Autofight - A PHP job interview task 8 | 9 | 10 | 11 | 56 | 57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 68 | 71 |
72 |
73 |
74 | 95 | 96 |
97 | 98 |
99 |

Full tutorial on how this was built and why available here.

100 |
101 | 102 | 103 | 104 | Maybe try this link: 134 | Army 1 = Army 2 = 50'; 135 | break; 136 | } 137 | echo $sMsg; 138 | } else { 139 | 140 | /** 141 | * Register available unit types 142 | */ 143 | Army::addUnitType(new \autofight\Infantry()); 144 | Army::addUnitType(new \autofight\Tank()); 145 | 146 | /** 147 | * Build armies 148 | */ 149 | $oArmy1 = new Army($iArmy1); 150 | $oArmy2 = new Army($iArmy2); 151 | 152 | $oWar = new \autofight\War(); 153 | 154 | /** 155 | * Register appropriate logger, depending on context 156 | */ 157 | $oWar->setLogger( 158 | PHP_SAPI == 'cli' 159 | ? new \autofight\Loggers\LoggerCli() 160 | : new \autofight\Loggers\LoggerWeb() 161 | ); 162 | 163 | /** 164 | * Start the war 165 | */ 166 | //$oWar->addArmy($oArmy1->setLabel('Blue'))->addArmy($oArmy2->setLabel('Red')); 167 | $oWar->addArmy($oArmy1)->addArmy($oArmy2); 168 | $oWar->fight(); 169 | } 170 | ?> 171 | 172 |
173 |
174 | 175 | 178 | 179 | 180 | 181 |
182 |
183 |
184 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: Mediapartners-Google 2 | Disallow: -------------------------------------------------------------------------------- /utility_methods.php: -------------------------------------------------------------------------------- 1 | = 11 && ($i % 100) <= 13) { 12 | $n = $i . 'th'; 13 | } else { 14 | $n = $i . $aEndings[$i % 10]; 15 | } 16 | return $n; 17 | } --------------------------------------------------------------------------------