Pac-Man Evolution
Loading...
Searching...
No Matches
Brains.cpp
1/*----------------------------------------------------------------------
2Pac-Man Evolution - Roberto Prieto
3Copyright (C) 2018-2024 MegaStorm Systems
4contact@megastormsystems.com - http://www.megastormsystems.com
5
6This software is provided 'as-is', without any express or implied
7warranty. In no event will the authors be held liable for any damages
8arising from the use of this software.
9
10Permission is granted to anyone to use this software for any purpose,
11including commercial applications, and to alter it and redistribute it
12freely, subject to the following restrictions:
13
141. The origin of this software must not be misrepresented; you must not
15claim that you wrote the original software. If you use this software
16in a product, an acknowledgment in the product documentation would be
17appreciated but is not required.
182. Altered source versions must be plainly marked as such, and must not be
19misrepresented as being the original software.
203. This notice may not be removed or altered from any source distribution.
21
22------------------------------------------------------------------------
23
24Brains classes
25
26------------------------------------------------------------------------ */
27
28#include "Brains.h"
29#include "BrainsFactory.h"
30#include "ObjectsGhost.h"
31#include "GameField.h"
32#include "Pac-Man_Evolution.h"
33#include "ArtificialNeuralNet.h"
34#include "EVNTrainer.h"
35
36// ---------- Base brain ----------
37// Constructor
38Brain::Brain()
39{
40 sName = "Random";
41 iTargetType = PME_BRAIN_HIGHLEVEL_TARGET;
42}
43
44// Destructor
45Brain::~Brain() {}
46
47// Default think method return a random target position
48Sint32 Brain::think(Actor* pActor, Sint32 &iTX, Sint32 &iTY, GameField* pGF)
49{
50 iTX = Main::Instance().ITool().randWELL() % MAZE_WIDTH;
51 iTY = Main::Instance().ITool().randWELL() % MAZE_HEIGHT;
52 return 0;
53}
54
55// Get brain name
56void Brain::getName(string& sN)
57{
58 sN = sName;
59}
60
61// Get Target type
62Sint32 Brain::getTargetType()
63{
64 return iTargetType;
65}
66
67// ---------- Evolved brain ----------
68BrainEvolved::BrainEvolved() : Brain()
69{
70 sName = "Evolved";
71 pNeuralNet = new(std::nothrow) ArtificialNeuralNet();
72 iNNInput = 0;
73 iNNOutput = 0;
74 dFitness = 0;
75}
76
77BrainEvolved::~BrainEvolved()
78{
79 if(pNeuralNet != nullptr) delete pNeuralNet;
80 pNeuralNet = nullptr;
81}
82
83Sint32 BrainEvolved::setFitness(double dF)
84{
85 if(dF < 0) dF = 0;
86 dFitness = dF;
87 return 0;
88}
89
90double BrainEvolved::getFitness()
91{
92 return dFitness;
93}
94
95ArtificialNeuralNet* BrainEvolved::getNeuralNet()
96{
97 if(pNeuralNet) return pNeuralNet;
98 else return nullptr;
99}
100
101// Load a Evolved neural net from the given file, it checks for the input/output
102Sint32 BrainEvolved::load(string sCDCFile, string sXMLName)
103{
104 // Load the neural network, we are using an extra weight for the bias parameter
105 if(EVNTrainer::Instance().load(sName, pNeuralNet, sCDCFile, sXMLName) != 0)
106 {
107 Main::Instance().ILogMgr().get()->msg(LML_NORMAL, " [BrainEvolved] Warning: neural network for ghost '%s' could not be loaded\n", sName.c_str());
108 delete pNeuralNet;
109 pNeuralNet = nullptr;
110 }
111
112 // Check input/output of NN loaded, manually fixed to a specific amount.
113 // On think() method we use this layout so can not break it.
114 else if((pNeuralNet->getNumInputs() != iNNInput) || (pNeuralNet->getNumOutputs() != iNNOutput))
115 {
116 Main::Instance().ILogMgr().get()->msg(LML_NORMAL, " [BrainEvolved] Warning: neural network loaded for ghost '%s' does not match with expected inputs('%d')/outputs('%d')\n", sName.c_str(), iNNInput, iNNOutput);
117 delete pNeuralNet;
118 pNeuralNet = nullptr;
119 }
120 #ifdef DEBUG_EVOLVED
121 if(pNeuralNet) Main::Instance().ILogMgr().get()->msg(LML_INFO, " [BrainEvolved] Info: neural network loaded for ghost '%s' with '%d' inputs, '%d' outputs and '%d' weights\n",
122 sName.c_str(), pNeuralNet->getNumInputs(), pNeuralNet->getNumOutputs(), pNeuralNet->getNumberOfWeights());
123 #endif
124
125 return 0;
126}
127
128// ---------- PacMan brains ----------
129// Human brain
130BrainPacManHuman::BrainPacManHuman() : Brain() { sName = "PacMan-Human"; iTargetType = PME_BRAIN_IMMEDIATE_TARGET; }
131Sint32 BrainPacManHuman::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF) // Get human keyboard action and keep moving while possible
132{
133 Main& mC64 = Main::Instance();
134 Sint32 iTmpX, iTmpY, iDirX, iDirY;
135
136 // Get current PacMan position
137 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
138 iTmpX = iTX;
139 iTmpY = iTY;
140
141 // Modify target following the keystrokes
142 if(mC64.getKeyState(SDLK_DOWN))
143 {
144 if(pGF->getState(iTX, iTY + 1) == PME_STATE_WALKABLE) ++iTY;
145 }
146 if(mC64.getKeyState(SDLK_UP))
147 {
148 if(pGF->getState(iTX, iTY - 1) == PME_STATE_WALKABLE) --iTY;
149 }
150 if(mC64.getKeyState(SDLK_RIGHT))
151 {
152 if((iTX + 1) > (MAZE_WIDTH - 1)) ++iTX;
153 else if(pGF->getState(iTX + 1, iTY) == PME_STATE_WALKABLE) ++iTX;
154 }
155 if(mC64.getKeyState(SDLK_LEFT))
156 {
157 if((iTX - 1) < 0) --iTX;
158 else if(pGF->getState(iTX - 1, iTY) == PME_STATE_WALKABLE) --iTX;
159 }
160
161 // With no human action, keep moving into the same direction
162 if(iTmpX == iTX && iTmpY == iTY)
163 {
164 // Get direction
165 pGF->getObjectDirection(PME_OBJECT_PACMAN, iDirX, iDirY);
166
167 // Apply direction
168 iTmpX = iTmpX + 1 * iDirX;
169 iTmpY = iTmpY + 1 * iDirY;
170 if((iTmpX) > (MAZE_WIDTH - 1)) iTX = iTmpX;
171 else if((iTmpX) < 0) iTX = iTmpX;
172 else if(pGF->getState(iTmpX, iTmpY) == PME_STATE_WALKABLE)
173 {
174 iTX = iTmpX;
175 iTY = iTmpY;
176 }
177 }
178
179 return 0;
180}
181// Fixed brain
182BrainPacMan::BrainPacMan() : Brain() { sName = "PacMan-Fixed"; }
183Sint32 BrainPacMan::think(Actor* pActor,Sint32& iTX, Sint32& iTY, GameField* pGF) // Fixed rules I
184{
185 vector<MazePoint> vGhosts;
186 MazePoint pointGhost;
187 string sStateName;
188 Sint32 i, iSelected = 0;
189 double dLowest = 1000.0f;
190
191 // Get current position
192 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
193
194 // Get all ghost positions (non-death and not at home), calculate the distance and get the closest one
195 if(pGF->getObjectPosition(PME_OBJECT_GHOST_RED, pointGhost.iX, pointGhost.iY) == 0)
196 {
197 pGF->getObjectStateName(PME_OBJECT_GHOST_RED, sStateName);
198 if(sStateName != "Death" && sStateName != "Init")
199 {
200 pointGhost.iReserved = PME_OBJECT_GHOST_RED;
201 vGhosts.push_back(pointGhost);
202 }
203 }
204 if(pGF->getObjectPosition(PME_OBJECT_GHOST_PINK, pointGhost.iX, pointGhost.iY) == 0)
205 {
206 pGF->getObjectStateName(PME_OBJECT_GHOST_PINK, sStateName);
207 if(sStateName != "Death" && sStateName != "Init")
208 {
209 pointGhost.iReserved = PME_OBJECT_GHOST_PINK;
210 vGhosts.push_back(pointGhost);
211 }
212 }
213 if(pGF->getObjectPosition(PME_OBJECT_GHOST_BLUE, pointGhost.iX, pointGhost.iY) == 0)
214 {
215 pGF->getObjectStateName(PME_OBJECT_GHOST_BLUE, sStateName);
216 if(sStateName != "Death" && sStateName != "Init")
217 {
218 pointGhost.iReserved = PME_OBJECT_GHOST_BLUE;
219 vGhosts.push_back(pointGhost);
220 }
221 }
222 if(pGF->getObjectPosition(PME_OBJECT_GHOST_ORANGE, pointGhost.iX, pointGhost.iY) == 0)
223 {
224 pGF->getObjectStateName(PME_OBJECT_GHOST_ORANGE, sStateName);
225 if(sStateName != "Death" && sStateName != "Init")
226 {
227 pointGhost.iReserved = PME_OBJECT_GHOST_ORANGE;
228 vGhosts.push_back(pointGhost);
229 }
230 }
231
232 // Calculate the distance of all found ghosts to PacMan
233 for(i = 0; i < vGhosts.size(); ++i) pActor->euclideanDistance(iTX, iTY, vGhosts[i]);
234
235 // Get the lowest distance
236 for(i = 0; i < vGhosts.size(); ++i)
237 {
238 if(dLowest > vGhosts[i].dDistance)
239 {
240 dLowest = vGhosts[i].dDistance;
241 iSelected = i;
242 }
243 }
244
245 // With no ghosts (debugging for example), go to nearest pellet
246 if(vGhosts.size() == 0)
247 {
248 pGF->getClosestPellet(PME_OBJECT_PACMAN, iTX, iTY);
249 }
250
251 // When Wave mode is evading and we are not very far, go to closest ghost
252 else if((pGF->getWaveMode() == PME_GLOBAL_WAVE_EVADING) && (vGhosts[iSelected].dDistance < 10))
253 {
254 iTX = vGhosts[iSelected].iX;
255 iTY = vGhosts[iSelected].iY;
256 }
257 // When ghosts are far from PacMan, go to nearest pellet
258 else if(vGhosts[iSelected].dDistance > 6.0)
259 {
260 pGF->getClosestPellet(PME_OBJECT_PACMAN, iTX, iTY);
261 }
262 // When ghosts are close to PacMan, try to evade
263 else
264 {
265 iTX = vGhosts[iSelected].iX + 2 * (iTX - vGhosts[iSelected].iX);
266 iTY = vGhosts[iSelected].iY + 2 * (iTY - vGhosts[iSelected].iY);
267 }
268
269 return 0;
270}
271
272// Red Ghost brains
273// Fixed brain
274BrainRedGhost::BrainRedGhost() : Brain() { sName = "Red-Fixed"; }
275Sint32 BrainRedGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF) // Target PacMan position
276{
277 return pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
278}
279// Evolved brain
280BrainEvolvedRedGhost::BrainEvolvedRedGhost(Sint32 iID) : BrainEvolved()
281{
282 string sNumber;
283
284 // Out of range
285 if(iID > PME_GA_POPULATION)
286 {
287 sName = "Red-Unknown";
288 return;
289 }
290 iNNInput = PME_ANN_GHOST_RED_INPUT;
291 iNNOutput = PME_ANN_GHOST_OUTPUT;
292
293 // Evolved ID
294 if(iID < 0)
295 {
296 sName = "Red-" + sName;
297 load();
298 }
299 // Training ID
300 else
301 {
302 Main::Instance().ITool().intToStrDec(iID, sNumber);
303 sName = "Red-Training";
304 sName = sName + "-" + sNumber;
305 }
306}
307Sint32 BrainEvolvedRedGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF)
308{
309 double dTmp;
310 vector<double> vInputs, vOutputs;
311
312 // In case of problems with the neural network, fallback to fixed target
313 if(pNeuralNet == nullptr) return 0;
314
315 // Get PacMan position
316 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
317
318 // Prepare inputs
319 // 2 input: PacMan (x,y) -> normalization to [0,1]
320 vInputs.clear();
321 dTmp = ((double)iTX / (double)MAZE_WIDTH);
322 vInputs.push_back(dTmp);
323 dTmp = ((double)iTY / (double)MAZE_HEIGHT);
324 vInputs.push_back(dTmp);
325
326 // Call neural network
327 vOutputs = pNeuralNet->update(vInputs);
328
329 // Prepare outputs
330 // 2 output from [0,1], convert it to target coordinates
331 if(!vOutputs.empty())
332 {
333 iTX = (Sint32)Main::Instance().ITool().round(vOutputs[0] * MAZE_WIDTH);
334 iTY = (Sint32)Main::Instance().ITool().round(vOutputs[1] * MAZE_HEIGHT);
335 }
336
337 return 0;
338}
339
340// Pink Ghost brains
341// Fixed brain
342BrainPinkGhost::BrainPinkGhost() : Brain() { sName = "PinkGhost-Fixed"; }
343Sint32 BrainPinkGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF) // Target PacMan position plus 4 maze points where he is facing to
344{
345 Sint32 iDirX, iDirY;
346
347 // Get current position and direction
348 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
349 pGF->getObjectDirection(PME_OBJECT_PACMAN, iDirX, iDirY);
350
351 // Calculate target
352 iTX = iTX + 4 * iDirX;
353 iTY = iTY + 4 * iDirY;
354
355 return 0;
356}
357// Evolved brain
358BrainEvolvedPinkGhost::BrainEvolvedPinkGhost(Sint32 iID) : BrainEvolved()
359{
360 string sNumber;
361
362 // Out of range
363 if(iID > PME_GA_POPULATION)
364 {
365 sName = "Pink-Unknown";
366 return;
367 }
368 iNNInput = PME_ANN_GHOST_PINK_INPUT;
369 iNNOutput = PME_ANN_GHOST_OUTPUT;
370
371 // Evolved ID
372 if(iID < 0)
373 {
374 sName = "Pink-" + sName;
375 load();
376 }
377 // Training ID
378 else
379 {
380 Main::Instance().ITool().intToStrDec(iID, sNumber);
381 sName = "Pink-Training";
382 sName = sName + "-" + sNumber;
383 }
384}
385Sint32 BrainEvolvedPinkGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF)
386{
387 double dTmp;
388 vector<double> vInputs, vOutputs;
389 Sint32 iDirX, iDirY;
390
391 // In case of problems with the neural network, fallback to fixed target
392 if(pNeuralNet == nullptr) return 0;
393
394 // Get PacMan position and direction
395 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
396 pGF->getObjectDirection(PME_OBJECT_PACMAN, iDirX, iDirY);
397
398 // Prepare inputs
399 // 4 input: PacMan (x,y), PacMan direction +4 -> normalization to [0,1]
400 vInputs.clear();
401 dTmp = ((double)iTX / (double)MAZE_WIDTH);
402 vInputs.push_back(dTmp);
403 dTmp = ((double)iTY / (double)MAZE_HEIGHT);
404 vInputs.push_back(dTmp);
405 dTmp = ((double)(iTX + 4 * iDirX) / (double)MAZE_HEIGHT);
406 vInputs.push_back(dTmp);
407 dTmp = ((double)(iTY + 4 * iDirY) / (double)MAZE_HEIGHT);
408 vInputs.push_back(dTmp);
409
410 // Call neural network
411 vOutputs = pNeuralNet->update(vInputs);
412
413 // Prepare outputs
414 // 2 output from [0,1], convert it to target coordinates
415 if(!vOutputs.empty())
416 {
417 iTX = (Sint32)Main::Instance().ITool().round(vOutputs[0] * MAZE_WIDTH);
418 iTY = (Sint32)Main::Instance().ITool().round(vOutputs[1] * MAZE_HEIGHT);
419 }
420
421 return 0;
422}
423
424// Blue Ghost brains
425// Fixed brain
426BrainBlueGhost::BrainBlueGhost() : Brain() { sName = "BlueGhost-Fixed"; }
427Sint32 BrainBlueGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF) // Target PacMan position plus 2 where he is facing to and Red Ghost vector to it multiply x2
428{
429 Sint32 iDirX, iDirY, iGRX, iGRY;
430
431 // Get current position and direction of PacMan
432 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
433 pGF->getObjectDirection(PME_OBJECT_PACMAN, iDirX, iDirY);
434
435 // Calculate target (PacMain position plus 2 where he is facing)
436 iTX = iTX + 2 * iDirX;
437 iTY = iTY + 2 * iDirY;
438
439 // Get current position of Red Ghost
440 pGF->getObjectPosition(PME_OBJECT_GHOST_RED, iGRX, iGRY);
441
442 // Set final target
443 iTX = iGRX + 2 * (iTX - iGRX);
444 iTY = iGRY + 2 * (iTY - iGRY);
445
446 return 0;
447}
448
449// Evolved brain
450BrainEvolvedBlueGhost::BrainEvolvedBlueGhost(Sint32 iID) : BrainEvolved()
451{
452 string sNumber;
453
454 // Out of range
455 if(iID > PME_GA_POPULATION)
456 {
457 sName = "Blue-Unknown";
458 return;
459 }
460 iNNInput = PME_ANN_GHOST_BLUE_INPUT;
461 iNNOutput = PME_ANN_GHOST_OUTPUT;
462
463 // Evolved ID
464 if(iID < 0)
465 {
466 sName = "Blue-" + sName;
467 load();
468 }
469 // Training ID
470 else
471 {
472 Main::Instance().ITool().intToStrDec(iID, sNumber);
473 sName = "Blue-Training";
474 sName = sName + "-" + sNumber;
475 }
476}
477Sint32 BrainEvolvedBlueGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF)
478{
479 double dTmp;
480 vector<double> vInputs, vOutputs;
481 MazePoint pointRed;
482
483 // In case of problems with the neural network, fallback to fixed target
484 if(pNeuralNet == nullptr) return 0;
485
486 // Get PacMan and Red Ghost positions and calculate Red-to-PacMan distance
487 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
488 pGF->getObjectPosition(PME_OBJECT_GHOST_RED, pointRed.iX, pointRed.iY);
489 pActor->euclideanDistance(iTX, iTY, pointRed);
490
491 // Prepare inputs
492 // 3 input: PacMan (x,y), Red Ghost distance to PacMan -> normalization to [0,1]
493 vInputs.clear();
494 dTmp = ((double)iTX / (double)MAZE_WIDTH);
495 vInputs.push_back(dTmp);
496 dTmp = ((double)iTY / (double)MAZE_HEIGHT);
497 vInputs.push_back(dTmp);
498 dTmp = pointRed.dDistance / 38.0; // ~38 is the maximum distance on the maze -> sqrt(MAZE_WIDTH - 2)2 + (MAZE_HEIGHT - 2)2)
499 vInputs.push_back(dTmp);
500
501 // Call neural network
502 vOutputs = pNeuralNet->update(vInputs);
503
504 // Prepare outputs
505 // 2 output from [0,1], convert it to target coordinates
506 if(!vOutputs.empty())
507 {
508 iTX = (Sint32)Main::Instance().ITool().round(vOutputs[0] * MAZE_WIDTH);
509 iTY = (Sint32)Main::Instance().ITool().round(vOutputs[1] * MAZE_HEIGHT);
510 }
511
512 return 0;
513}
514
515// Orange Ghost brains
516// Fixed brain
517BrainOrangeGhost::BrainOrangeGhost() : Brain() { sName = "OrangeGhost-Fixed"; }
518Sint32 BrainOrangeGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF) // Target PacMan when far from him 8 maze points or target scattering when close than 8
519{
520 Sint32 iPX, iPY;
521 MazePoint pointActor;
522
523 // Get PacMan position
524 pGF->getObjectPosition(PME_OBJECT_PACMAN, iPX, iPY);
525
526 // Get distance to PacMan
527 pActor->getPositionMaze(pointActor.iX, pointActor.iY);
528 pActor->euclideanDistance(iPX, iPY, pointActor);
529
530 // Distance greater than 8, target PacMan
531 if(pointActor.dDistance > 8)
532 {
533 iTX = iPX;
534 iTY = iPY;
535 }
536 // Distance smaller than 8, target scattering position
537 else reinterpret_cast<Ghost*>(pActor)->getScatteringTarget(iTX, iTY);
538
539 return 0;
540}
541
542// Evolved brain
543BrainEvolvedOrangeGhost::BrainEvolvedOrangeGhost(Sint32 iID) : BrainEvolved()
544{
545 string sNumber;
546
547 // Out of range
548 if(iID > PME_GA_POPULATION)
549 {
550 sName = "Orange-Unknown";
551 return;
552 }
553 iNNInput = PME_ANN_GHOST_ORANGE_INPUT;
554 iNNOutput = PME_ANN_GHOST_OUTPUT;
555
556 // Evolved ID
557 if(iID < 0)
558 {
559 sName = "Orange-" + sName;
560 load();
561 }
562 // Training ID
563 else
564 {
565 Main::Instance().ITool().intToStrDec(iID, sNumber);
566 sName = "Orange-Training";
567 sName = sName + "-" + sNumber;
568 }
569}
570Sint32 BrainEvolvedOrangeGhost::think(Actor* pActor, Sint32& iTX, Sint32& iTY, GameField* pGF)
571{
572 double dTmp;
573 vector<double> vInputs, vOutputs;
574 MazePoint pointActor;
575
576 // In case of problems with the neural network, fallback to fixed target
577 if(pNeuralNet == nullptr) return 0;
578
579 // Get PacMan and our positions and calculate distance
580 pGF->getObjectPosition(PME_OBJECT_PACMAN, iTX, iTY);
581 pActor->getPositionMaze(pointActor.iX, pointActor.iY);
582 pActor->euclideanDistance(iTX, iTY, pointActor);
583
584 // Get position of the closest pellet to PacMan
585 pGF->getClosestPellet(PME_OBJECT_PACMAN, iTX, iTY);
586
587 // Prepare inputs
588 // 3 input: Distance to PacMan and closest pellet to PacMan (x,y) -> normalization to [0,1]
589 vInputs.clear();
590 dTmp = ((double)iTX / (double)MAZE_WIDTH);
591 vInputs.push_back(dTmp);
592 dTmp = ((double)iTY / (double)MAZE_HEIGHT);
593 vInputs.push_back(dTmp);
594 dTmp = pointActor.dDistance / 38.0; // ~38 is the maximum distance on the maze -> sqrt(MAZE_WIDTH - 2)2 + (MAZE_HEIGHT - 2)2)
595 vInputs.push_back(dTmp);
596
597 // Call neural network
598 vOutputs = pNeuralNet->update(vInputs);
599
600 // Prepare outputs
601 // 2 output from [0,1], convert it to target coordinates
602 if(!vOutputs.empty())
603 {
604 iTX = (Sint32)Main::Instance().ITool().round(vOutputs[0] * MAZE_WIDTH);
605 iTY = (Sint32)Main::Instance().ITool().round(vOutputs[1] * MAZE_HEIGHT);
606 }
607
608 return 0;
609}