├── .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 |
75 |
76 |
77 |
81 |
84 |
85 |
86 |
87 |
91 |
94 |
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 | }
--------------------------------------------------------------------------------