2
0

Ghost.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Microsoft.Xna.Framework;
  5. using Microsoft.Xna.Framework.Graphics;
  6. using Microsoft.Xna.Framework.Input;
  7. namespace XNAPacMan {
  8. public enum GhostState { Home, Scatter, Attack, Blue, Dead }
  9. public enum Ghosts { Blinky, Pinky, Inky, Clyde }
  10. /// <summary>
  11. /// One of the ghosts that try to kill Pac Man.
  12. /// </summary>
  13. public class Ghost {
  14. /// <summary>
  15. /// Instantiates a ghost.
  16. /// </summary>
  17. /// <param name="game">A reference to the Game object, needed for access to services.</param>
  18. /// <param name="player">A reference to the Pac Man, needed for AI.</param>
  19. /// <param name="identity">Which ghost, needed for appearance and behavior.</param>
  20. public Ghost(Game game, Player player, Ghosts identity) {
  21. spriteBatch_ = (SpriteBatch)game.Services.GetService(typeof(SpriteBatch));
  22. ghostBase1_ = game.Content.Load<Texture2D>("sprites/GhostBase");
  23. ghostBase2_ = game.Content.Load<Texture2D>("sprites/GhostBase2");
  24. ghostChased_ = game.Content.Load<Texture2D>("sprites/GhostChased");
  25. eyesBase_ = game.Content.Load<Texture2D>("sprites/GhostEyes");
  26. eyesCenter_ = game.Content.Load<Texture2D>("sprites/GhostEyesCenter");
  27. colorBase_ = Constants.colors(identity);
  28. identity_ = identity;
  29. previousNumCrumps_ = 0;
  30. Reset(true, player);
  31. wiggle_ = true;
  32. direction_ = new Direction();
  33. lastJunction_ = new Point();
  34. scatterTiles_ = Constants.scatterTiles(identity);
  35. }
  36. /// <summary>
  37. /// Put the ghosts back in their home, ready to begin a new attack
  38. /// </summary>
  39. /// <param name="newLevel">True at level start, false otherwise</param>
  40. /// <param name="player">The pac man. Pac Man is often respawned with the ghosts, so they need to know about the new Pac Man.</param>
  41. public void Reset(bool newLevel, Player player) {
  42. State = GhostState.Home; // Ghosts start at home
  43. previousState_ = GhostState.Home; // Sounds are played when currentState != previousState.
  44. // So to get them playing at level start, we do this simple hack.
  45. updateCount_ = 0;
  46. initialJumps_ = Constants.InitialJumps(identity_, newLevel);
  47. position_ = Constants.startPosition(identity_);
  48. scheduleStateEval_ = true;
  49. lastJunction_ = new Point();
  50. player_ = player;
  51. scatterModesLeft_ = 4;
  52. UpdateSpeed();
  53. }
  54. /// <summary>
  55. /// When we need to lock the game, make sure the timers here are updated to reflect
  56. /// the "waste of time".
  57. /// </summary>
  58. public void LockTimer(GameTime gameTime) {
  59. timeInCurrentState += gameTime.ElapsedGameTime;
  60. }
  61. /// <summary>
  62. /// In case this ghost is Inky, he will need a reference to blinky in order
  63. /// to find his way around the maze. Otherwise, setting this has no effect.
  64. /// </summary>
  65. /// <param name="blinky">A reference to Blinky.</param>
  66. public void SetBlinky(Ghost blinky) {
  67. blinky_ = blinky;
  68. }
  69. /// <summary>
  70. /// Call this every game update to get those ghosts moving around.
  71. /// </summary>
  72. /// <param name="gameTime">Provides a snapshot of timing values.</param>
  73. public void Update(GameTime gameTime) {
  74. if (scheduleStateEval_) {
  75. StateEval();
  76. }
  77. // After the AI has had the occasion to run, update lastJuntion.
  78. if (position_.DeltaPixel == Point.Zero && IsAJunction(position_.Tile)) {
  79. lastJunction_ = position_.Tile;
  80. }
  81. Move();
  82. }
  83. void PlaySound() {
  84. if (State == GhostState.Attack || State == GhostState.Scatter || State == GhostState.Home) {
  85. if (Grid.NumCrumps < 50) {
  86. GhostSoundsManager.playLoopAttackVeryFast();
  87. } else if (Grid.NumCrumps < 120) {
  88. GhostSoundsManager.playLoopAttackFast();
  89. } else {
  90. GhostSoundsManager.playLoopAttack();
  91. }
  92. } else if (State == GhostState.Blue) {
  93. GhostSoundsManager.playLoopBlue();
  94. } else if (State == GhostState.Dead) {
  95. GhostSoundsManager.playLoopDead();
  96. }
  97. }
  98. void StateEval() {
  99. GhostState initialState = State;
  100. scheduleStateEval_ = false;
  101. switch (State) {
  102. case GhostState.Home:
  103. // Ghost exit the home state for the scatter state when they get to the row
  104. // above the home
  105. if (position_.Tile.Y == 11 && position_.DeltaPixel.Y == 0) {
  106. // Select inital direction based on scatter tiles
  107. if (Constants.scatterTiles(identity_)[0].X < 13) {
  108. direction_ = Direction.Left;
  109. } else {
  110. direction_ = Direction.Right;
  111. }
  112. if (scatterModesLeft_ > 0) {
  113. State = GhostState.Scatter;
  114. } else {
  115. State = GhostState.Attack;
  116. }
  117. return;
  118. }
  119. // Ghosts move up when they are aligned with the entrance
  120. else if (position_.Tile.X == 13 && position_.DeltaPixel.X == 8) {
  121. direction_ = Direction.Up;
  122. }
  123. // When on one side, move towards middle when on the bottom and time's up
  124. // If time's not up, keep bouncing up and down
  125. else if ((position_.DeltaPixel.Y == 8) &&
  126. ((position_.Tile.X == 11 && position_.DeltaPixel.X == 8) ||
  127. (position_.Tile.X == 15 && position_.DeltaPixel.X == 8))) {
  128. if (position_.Tile.Y == 14) {
  129. initialJumps_--;
  130. if (initialJumps_ == 0) {
  131. if (position_.Tile.X == 11) {
  132. direction_ = Direction.Right;
  133. } else {
  134. direction_ = Direction.Left;
  135. }
  136. } else {
  137. direction_ = Direction.Up;
  138. }
  139. } else if (position_.Tile.Y == 13) {
  140. direction_ = Direction.Down;
  141. }
  142. }
  143. break;
  144. case GhostState.Scatter:
  145. // Attempt to reverse direction upon entering this state
  146. if (previousState_ == GhostState.Attack) {
  147. scatterModesLeft_--;
  148. if (NextTile(OppositeDirection(direction_)).IsOpen) {
  149. direction_ = OppositeDirection(direction_);
  150. }
  151. }
  152. AIScatter();
  153. int timeInScatterMode = scatterModesLeft_ <= 2 ? 5 : 7;
  154. if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(timeInScatterMode)) {
  155. State = GhostState.Attack;
  156. }
  157. break;
  158. case GhostState.Dead:
  159. // Attempt to reverse direction upon entering this state
  160. if (previousState_ != GhostState.Dead && previousState_ != GhostState.Blue) {
  161. if (NextTile(OppositeDirection(direction_)).IsOpen) {
  162. direction_ = OppositeDirection(direction_);
  163. }
  164. } else {
  165. AIDead();
  166. }
  167. if (position_.DeltaPixel.X == 8 && position_.DeltaPixel.Y == 8) {
  168. if (position_.Tile.Y == 14) {
  169. State = GhostState.Home;
  170. }
  171. }
  172. break;
  173. case GhostState.Attack:
  174. // Attempt to reverse direction upon entering this state
  175. if (previousState_ != GhostState.Attack && previousState_ != GhostState.Blue) {
  176. if (NextTile(OppositeDirection(direction_)).IsOpen) {
  177. direction_ = OppositeDirection(direction_);
  178. }
  179. } else {
  180. AIAttack();
  181. }
  182. if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(20)) {
  183. State = GhostState.Scatter;
  184. }
  185. break;
  186. case GhostState.Blue:
  187. // Attempt to reverse direction upon entering this state
  188. if (previousState_ != GhostState.Blue) {
  189. if (NextTile(OppositeDirection(direction_)).IsOpen) {
  190. direction_ = OppositeDirection(direction_);
  191. }
  192. } else {
  193. // TODO : make special blue AI
  194. AIAttack();
  195. }
  196. // When blue time is over, revert to attack mode.
  197. if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(Constants.BlueTime())) {
  198. State = GhostState.Attack;
  199. }
  200. break;
  201. }
  202. // TODO : move all these magic numbers to the Constants class.
  203. // We select a new sound only upon some state change, or when
  204. // the number of crumps goes below certain thresholds.
  205. if ((initialState != previousState_) ||
  206. (Grid.NumCrumps == 199 && previousNumCrumps_ == 200) ||
  207. (Grid.NumCrumps == 19 && previousNumCrumps_ == 20)) {
  208. PlaySound();
  209. }
  210. previousState_ = initialState;
  211. }
  212. void Move() {
  213. updateCount_++;
  214. updateCount_ %= updatesPerPixel_;
  215. if (updateCount_ == 0) {
  216. // There is no point running the full AI each update, especially considering we
  217. // want this code to run at 1000 updates per second. We run it each time it moves by a pixel.
  218. scheduleStateEval_ = true;
  219. // Now is a nice time to evaluate whether our current speed is correct. This variable
  220. // may change under a wide array of circumstances, so by putting it here we probably generate
  221. // too many checks but we ensure correct behavior.
  222. UpdateSpeed();
  223. switch (direction_) {
  224. case Direction.Up:
  225. position_.DeltaPixel.Y--;
  226. if (position_.DeltaPixel.Y < 0) {
  227. position_.DeltaPixel.Y = 15;
  228. position_.Tile.Y--;
  229. }
  230. break;
  231. case Direction.Down:
  232. position_.DeltaPixel.Y++;
  233. if (position_.DeltaPixel.Y > 15) {
  234. position_.DeltaPixel.Y = 0;
  235. position_.Tile.Y++;
  236. }
  237. break;
  238. case Direction.Left:
  239. position_.DeltaPixel.X--;
  240. if (position_.DeltaPixel.X < 0) {
  241. position_.DeltaPixel.X = 15;
  242. position_.Tile.X--;
  243. // Special case : the tunnel
  244. if (position_.Tile.X < 0) {
  245. position_.Tile.X = Grid.Width - 1;
  246. }
  247. }
  248. break;
  249. case Direction.Right:
  250. position_.DeltaPixel.X++;
  251. if (position_.DeltaPixel.X > 15) {
  252. position_.DeltaPixel.X = 0;
  253. position_.Tile.X++;
  254. // Special case : the tunnel
  255. if (position_.Tile.X > Grid.Width - 1) {
  256. position_.Tile.X = 0;
  257. }
  258. }
  259. break;
  260. }
  261. }
  262. }
  263. void UpdateSpeed() {
  264. int baseSpeed = Constants.GhostSpeed();
  265. if (State == GhostState.Home) {
  266. updatesPerPixel_ = baseSpeed * 2;
  267. } else if (State == GhostState.Blue) {
  268. updatesPerPixel_ = (int)(baseSpeed * 1.5);
  269. } else if (identity_ == Ghosts.Blinky && Grid.NumCrumps <= Constants.CruiseElroyTimer()) {
  270. updatesPerPixel_ = baseSpeed - 1;
  271. }
  272. // If in the tunnel, reduce speed
  273. else if (position_.Tile.Y == 14 &&
  274. ((0 <= position_.Tile.X && position_.Tile.X <= 5) ||
  275. (22 <= position_.Tile.X && position_.Tile.X <= 27))) {
  276. updatesPerPixel_ = baseSpeed + 5;
  277. } else {
  278. updatesPerPixel_ = baseSpeed;
  279. }
  280. }
  281. /// <summary>
  282. /// Guides the ghost towards his "favored" area of the board, as defined by scatterTiles_.
  283. /// </summary>
  284. void AIScatter() {
  285. // As with AIAttack(), all the method does is change direction if necessary,
  286. // which only happens when exactly at a junction
  287. if (position_.DeltaPixel != Point.Zero || !IsAJunction(position_.Tile)) {
  288. return;
  289. }
  290. // Scatter AI may be overriden by certain tiles
  291. if (AIOverride()) {
  292. return;
  293. }
  294. // If we are on one of our favored tiles, go on to the next
  295. int favoredTile = scatterTiles_.FindIndex(i => i == position_.Tile);
  296. int nextFavoredTile = (favoredTile + 1) % (scatterTiles_.Count);
  297. if (favoredTile != -1) {
  298. direction_ = FindDirection(scatterTiles_[nextFavoredTile]);
  299. }
  300. // If our last junction was a favored tile and this one isn't, ignore it
  301. else if (!scatterTiles_.Contains(lastJunction_)) {
  302. // Otherwise, find scatter tile closest to our position and head for it.
  303. List<Point> orderedTiles = scatterTiles_.ToList();
  304. orderedTiles.Sort((a, b) => Vector2.Distance(new Vector2(position_.Tile.X, position_.Tile.Y),
  305. new Vector2(a.X, a.Y)).
  306. CompareTo(
  307. Vector2.Distance(new Vector2(position_.Tile.X, position_.Tile.Y),
  308. new Vector2(b.X, b.Y))));
  309. direction_ = FindDirection(orderedTiles[0]);
  310. }
  311. }
  312. /// <summary>
  313. /// Guides the ghost to try to reach the player
  314. /// </summary>
  315. void AIAttack() {
  316. // All this method does is change direction if necessary.
  317. // There is only one case in which we may potentially change direction :
  318. // when the ghost is exactly at a junction.
  319. if (position_.DeltaPixel != Point.Zero || !IsAJunction(position_.Tile)) {
  320. return;
  321. }
  322. // Attack AI may be overriden by certain tiles
  323. if (AIOverride()) {
  324. return;
  325. }
  326. switch (identity_) {
  327. case Ghosts.Blinky:
  328. AttackAIBlinky();
  329. break;
  330. case Ghosts.Pinky:
  331. AttackAIPinky();
  332. break;
  333. case Ghosts.Inky:
  334. AttackAIInky();
  335. break;
  336. case Ghosts.Clyde:
  337. AttackAIClyde();
  338. break;
  339. default:
  340. throw new ArgumentException();
  341. }
  342. }
  343. void AIDead() {
  344. // When aligned with the entrance, go down. When the ghost has fully entered the house,
  345. // stateEval will change state to Home.
  346. if (position_.Tile.Y == 11 && position_.Tile.X == 13 && position_.DeltaPixel.X == 8) {
  347. direction_ = Direction.Down;
  348. }
  349. // Otherwise, only change direction if the ghost is exactly on a tile
  350. else if (position_.DeltaPixel == Point.Zero) {
  351. // We direct the ghost towards one of the two squares above the home. When he reaches one,
  352. // he still has to move slightly right or left in order to align with the entrance.
  353. if (position_.Tile.X == 13 && position_.Tile.Y == 11) {
  354. direction_ = Direction.Right;
  355. } else if (position_.Tile.X == 14 && position_.Tile.Y == 11) {
  356. direction_ = Direction.Left;
  357. }
  358. // Otherwise, when at a junction, head for the square above home closest to us.
  359. else if (IsAJunction(position_.Tile)) {
  360. if (position_.Tile.X > 13) {
  361. direction_ = FindDirection(new Point(14, 11));
  362. } else {
  363. direction_ = FindDirection(new Point(13, 11));
  364. }
  365. }
  366. }
  367. }
  368. /// <summary>
  369. /// Blinky is extremely straightforward : head directly for the player
  370. /// </summary>
  371. void AttackAIBlinky() {
  372. direction_ = FindDirection(player_.Position.Tile);
  373. }
  374. /// <summary>
  375. /// Pinky tries to head for two tiles ahead of the player.
  376. /// </summary>
  377. void AttackAIPinky() {
  378. Tile nextTile = NextTile(player_.Direction, player_.Position);
  379. Tile nextNextTile = NextTile(player_.Direction, new Position(nextTile.ToPoint, Point.Zero));
  380. direction_ = FindDirection(nextNextTile.ToPoint);
  381. }
  382. /// <summary>
  383. /// Inky is a bit more random. He will try to head for a square situated across
  384. /// the pac man from blinky's location.
  385. /// </summary>
  386. void AttackAIInky() {
  387. Tile nextTile = NextTile(player_.Direction, player_.Position);
  388. Tile nextNextTile = NextTile(player_.Direction, new Position(nextTile.ToPoint, Point.Zero));
  389. Vector2 line = new Vector2(blinky_.Position.Tile.X - nextNextTile.ToPoint.X, blinky_.Position.Tile.Y - nextNextTile.ToPoint.Y);
  390. line *= 2;
  391. Point destination = new Point(position_.Tile.X + (int)line.X, position_.Tile.Y + (int)line.Y);
  392. // Prevent out-of-bounds exception
  393. destination.X = (int)MathHelper.Clamp(destination.X, 0, Grid.Width - 1);
  394. destination.Y = (int)MathHelper.Clamp(destination.Y, 0, Grid.Height - 1);
  395. direction_ = FindDirection(destination);
  396. }
  397. /// <summary>
  398. /// Clyde is the bizarre one. When within 8 tiles of the player,
  399. /// he will head for his favored corner (AIScatter). When farther away,
  400. /// he will near the player using Blinky's AI.
  401. /// </summary>
  402. void AttackAIClyde() {
  403. float distanceToPlayer = Vector2.Distance(
  404. new Vector2(player_.Position.Tile.X, player_.Position.Tile.Y),
  405. new Vector2(position_.Tile.X, position_.Tile.Y));
  406. if (distanceToPlayer >= 8) {
  407. AttackAIBlinky();
  408. } else {
  409. AIScatter();
  410. }
  411. }
  412. // On certain tiles, we force all the ghosts in a particular direction.
  413. // This is to prevent them from bunching up too much, and give attentive
  414. // players some sure-fire ways to escape.
  415. bool AIOverride() {
  416. if (position_.Tile.Y == 11 && (position_.Tile.X == 12 || position_.Tile.X == 15)) {
  417. return (direction_ == Direction.Right || direction_ == Direction.Left);
  418. } else if (position_.Tile.Y == 20 && (position_.Tile.X == 9 || position_.Tile.X == 18)) {
  419. return (direction_ == Direction.Right || direction_ == Direction.Left);
  420. } else {
  421. return false;
  422. }
  423. }
  424. /// <summary>
  425. /// Returns the opposite of the specified direction
  426. /// </summary>
  427. /// <param name="d">direction</param>
  428. /// <returns>opposite direction</returns>
  429. Direction OppositeDirection(Direction d) {
  430. switch (d) {
  431. case Direction.Up:
  432. return Direction.Down;
  433. case Direction.Down:
  434. return Direction.Up;
  435. case Direction.Left:
  436. return Direction.Right;
  437. case Direction.Right:
  438. return Direction.Left;
  439. default:
  440. throw new ArgumentException();
  441. }
  442. }
  443. /// <summary>
  444. /// Returns in what direction we should go next in order to reach the destination.
  445. /// </summary>
  446. /// <param name="destination">Where we want to go</param>
  447. /// <returns>Where to go next</returns>
  448. Direction FindDirection(Point destination) {
  449. // We use a stupid but very efficient algorithm : go in the direction that
  450. // closes most distance between position and destination. This works well given
  451. // there are no dead ends and multiple paths leading to the destination.
  452. // However, we can never turn 180, and will rather choose a less-than-optimal path.
  453. int xDistance = destination.X - position_.Tile.X;
  454. int yDistance = destination.Y - position_.Tile.Y;
  455. var directions = new List<Direction>(4);
  456. // Order directions by shortest path. Note: there is probably a better way to do this, probably if I hadn't used an
  457. // enum for directions in the first place.
  458. if (Math.Abs(xDistance) > Math.Abs(yDistance)) {
  459. if (xDistance > 0) {
  460. if (yDistance < 0) { // Direction is Up-Right, favor Right
  461. directions = new List<Direction> { Direction.Right, Direction.Up, Direction.Down, Direction.Left };
  462. } else { // Direction is Down-Right, favor Right
  463. directions = new List<Direction> { Direction.Right, Direction.Down, Direction.Up, Direction.Left };
  464. }
  465. } else {
  466. if (yDistance < 0) { // Direction is Up-Left, favor Left
  467. directions = new List<Direction> { Direction.Left, Direction.Up, Direction.Down, Direction.Right };
  468. } else { // Direction is Down-Left, favor Left
  469. directions = new List<Direction> { Direction.Left, Direction.Down, Direction.Up, Direction.Right };
  470. }
  471. }
  472. } else {
  473. if (xDistance > 0) {
  474. if (yDistance < 0) { // Direction is Up-Right, favor Up
  475. directions = new List<Direction> { Direction.Up, Direction.Right, Direction.Left, Direction.Down };
  476. } else { // Direction is Down-Right, favor Down
  477. directions = new List<Direction> { Direction.Down, Direction.Right, Direction.Left, Direction.Up };
  478. }
  479. } else {
  480. if (yDistance < 0) { // Direction is Up-Left, favor Up
  481. directions = new List<Direction> { Direction.Up, Direction.Left, Direction.Right, Direction.Down };
  482. } else { // Direction is Down-Left, favor Down
  483. directions = new List<Direction> { Direction.Down, Direction.Left, Direction.Right, Direction.Up };
  484. }
  485. }
  486. }
  487. // Select first item in the list to meet two essential conditions : the path ahead is open,
  488. // and it's not the opposite direction. If we can't meet both conditions, just return the first
  489. // direction that leads to an open square.
  490. int index = directions.FindIndex(i => i != OppositeDirection(direction_) && NextTile(i).IsOpen);
  491. if (index != -1) {
  492. return directions[index];
  493. } else {
  494. // Put a breakpoint here, this should never happen.
  495. return directions.Find(i => NextTile(i).IsOpen);
  496. }
  497. }
  498. /// <summary>
  499. /// Retrieves the next tile in the specified direction from the ghost's position.
  500. /// </summary>
  501. /// <param name="d">Direction in which to look</param>
  502. /// <returns>The tile</returns>
  503. Tile NextTile(Direction d) {
  504. return NextTile(d, position_);
  505. }
  506. /// <summary>
  507. /// Retrieves the next tile in the specified direction from the specified position.
  508. /// </summary>
  509. /// <param name="d">Direction in which to look</param>
  510. /// <param name="p">Position from which to look</param>
  511. /// <returns>The tile</returns>
  512. public static Tile NextTile(Direction d, Position p) {
  513. switch (d) {
  514. case Direction.Up:
  515. if (p.Tile.Y - 1 < 0) {
  516. return Grid.TileGrid[p.Tile.X, p.Tile.Y];
  517. } else {
  518. return Grid.TileGrid[p.Tile.X, p.Tile.Y - 1];
  519. }
  520. case Direction.Down:
  521. if (p.Tile.Y + 1 >= Grid.Height) {
  522. return Grid.TileGrid[p.Tile.X, p.Tile.Y];
  523. } else {
  524. return Grid.TileGrid[p.Tile.X, p.Tile.Y + 1];
  525. }
  526. case Direction.Left:
  527. // Special case : the tunnel
  528. if (p.Tile.X == 0) {
  529. return Grid.TileGrid[Grid.Width - 1, p.Tile.Y];
  530. } else {
  531. return Grid.TileGrid[p.Tile.X - 1, p.Tile.Y];
  532. }
  533. case Direction.Right:
  534. // Special case : the tunnel
  535. if (p.Tile.X + 1 >= Grid.Width) {
  536. return Grid.TileGrid[0, p.Tile.Y];
  537. } else {
  538. return Grid.TileGrid[p.Tile.X + 1, p.Tile.Y];
  539. }
  540. default:
  541. throw new ArgumentException();
  542. }
  543. }
  544. /// <summary>
  545. /// Returns whether the specified tile is a junction.
  546. /// </summary>
  547. /// <param name="tile">Tile to check</param>
  548. /// <returns>whether the specified tile is a junction</returns>
  549. bool IsAJunction(Point tile) {
  550. if (NextTile(direction_).Type == TileTypes.Open) {
  551. // If the path ahead is open, we're at a junction if it's also open
  552. // to one of our sides
  553. if (direction_ == Direction.Up || direction_ == Direction.Down) {
  554. return ((NextTile(Direction.Left).IsOpen) ||
  555. (NextTile(Direction.Right).IsOpen));
  556. } else {
  557. return ((NextTile(Direction.Up).IsOpen) ||
  558. (NextTile(Direction.Down).IsOpen));
  559. }
  560. }
  561. // If the path ahead is blocked, then we're definitely at a junction, because there are no dead-ends
  562. else {
  563. return true;
  564. }
  565. }
  566. /// <summary>
  567. /// Assumes spritebatch.begin() was called
  568. /// </summary>
  569. /// <param name="gameTime">Provides a snapshot of timing values.</param>
  570. public void Draw(GameTime gameTime, Vector2 boardPosition) {
  571. // Ghosts have a two-frame animation; we use a bool to toggle between
  572. // the two. We divide DateTime.Now.Milliseconds by the period.
  573. if (((DateTime.Now.Millisecond / 125) % 2) == 0 ^ wiggle_) {
  574. wiggle_ = !wiggle_;
  575. }
  576. Vector2 position;
  577. position.X = boardPosition.X + (position_.Tile.X * 16) + (position_.DeltaPixel.X) - 6;
  578. position.Y = boardPosition.Y + (position_.Tile.Y * 16) + (position_.DeltaPixel.Y) - 6;
  579. Vector2 eyesBasePosition;
  580. eyesBasePosition.X = position.X + 4;
  581. eyesBasePosition.Y = position.Y + 6;
  582. Vector2 eyesCenterPosition = new Vector2();
  583. switch (direction_) {
  584. case Direction.Up:
  585. eyesBasePosition.Y -= 2;
  586. eyesCenterPosition.X = eyesBasePosition.X + 2;
  587. eyesCenterPosition.Y = eyesBasePosition.Y;
  588. break;
  589. case Direction.Down:
  590. eyesBasePosition.Y += 2;
  591. eyesCenterPosition.X = eyesBasePosition.X + 2;
  592. eyesCenterPosition.Y = eyesBasePosition.Y + 6;
  593. break;
  594. case Direction.Left:
  595. eyesBasePosition.X -= 2;
  596. eyesCenterPosition.X = eyesBasePosition.X;
  597. eyesCenterPosition.Y = eyesBasePosition.Y + 3;
  598. break;
  599. case Direction.Right:
  600. eyesBasePosition.X += 2;
  601. eyesCenterPosition.X = eyesBasePosition.X + 4;
  602. eyesCenterPosition.Y = eyesBasePosition.Y + 3;
  603. break;
  604. }
  605. if (State == GhostState.Blue) {
  606. if (((DateTime.Now - timeInCurrentState).Seconds < 0.5 * Constants.BlueTime())) {
  607. RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, Color.Blue);
  608. RenderSprite(ghostChased_, null, boardPosition, position, Color.White);
  609. } else {
  610. bool flash = (DateTime.Now.Second + DateTime.Now.Millisecond / 200) % 2 == 0;
  611. RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, flash ? Color.Blue : Color.White);
  612. RenderSprite(ghostChased_, null, boardPosition, position, flash ? Color.White : Color.Blue);
  613. }
  614. } else if (State == GhostState.Dead) {
  615. RenderSprite(eyesBase_, null, boardPosition, eyesBasePosition, Color.White);
  616. RenderSprite(eyesCenter_, null, boardPosition, eyesCenterPosition, Color.White);
  617. } else {
  618. RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, colorBase_);
  619. RenderSprite(eyesBase_, null, boardPosition, eyesBasePosition, Color.White);
  620. RenderSprite(eyesCenter_, null, boardPosition, eyesCenterPosition, Color.White);
  621. }
  622. }
  623. /// <summary>
  624. /// Allows rendering across the tunnel, which is tricky.
  625. /// </summary>
  626. /// <param name="spriteSheet">Source texture</param>
  627. /// <param name="rectangle">Portion of the source to render. Pass null for rendering the whole texture.</param>
  628. /// <param name="boardPosition">Top-left pixel of the board in the screen</param>
  629. /// <param name="position">Where to render the texture.</param>
  630. void RenderSprite(Texture2D spriteSheet, Rectangle? rectangle, Vector2 boardPosition, Vector2 position, Color color) {
  631. Rectangle rect = rectangle == null ? new Rectangle(0, 0, spriteSheet.Width, spriteSheet.Height) :
  632. rectangle.Value;
  633. int textureWidth = rectangle == null ? spriteSheet.Width : rectangle.Value.Width;
  634. int textureHeight = rectangle == null ? spriteSheet.Height : rectangle.Value.Height;
  635. // What happens when we are crossing to the other end by the tunnel?
  636. // We detect if part of the sprite is rendered outside of the board.
  637. // First, to the left.
  638. if (position.X < boardPosition.X) {
  639. int deltaPixel = (int)(boardPosition.X - position.X); // Number of pixels off the board
  640. var leftPortion = new Rectangle(rect.X + deltaPixel, rect.Y, textureWidth - deltaPixel, textureHeight);
  641. var leftPortionPosition = new Vector2(boardPosition.X, position.Y);
  642. var rightPortion = new Rectangle(rect.X, rect.Y, deltaPixel, textureHeight);
  643. var rightPortionPosition = new Vector2(boardPosition.X + (16 * 28) - deltaPixel, position.Y);
  644. spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, color);
  645. spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, color);
  646. }
  647. // Next, to the right
  648. else if (position.X > (boardPosition.X + (16 * 28) - textureWidth)) {
  649. int deltaPixel = (int)((position.X + textureWidth) - (boardPosition.X + (16 * 28))); // Number of pixels off the board
  650. var leftPortion = new Rectangle(rect.X + textureWidth - deltaPixel, rect.Y, deltaPixel, textureHeight);
  651. var leftPortionPosition = new Vector2(boardPosition.X, position.Y);
  652. var rightPortion = new Rectangle(rect.X, rect.Y, textureWidth - deltaPixel, textureHeight);
  653. var rightPortionPosition = new Vector2(position.X, position.Y);
  654. spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, color);
  655. spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, color);
  656. }
  657. // Draw normally - in one piece
  658. else {
  659. spriteBatch_.Draw(spriteSheet, position, rect, color);
  660. }
  661. }
  662. // DRAWING
  663. SpriteBatch spriteBatch_;
  664. Texture2D ghostBase1_;
  665. Texture2D ghostBase2_;
  666. Texture2D ghostChased_;
  667. Texture2D eyesBase_;
  668. Texture2D eyesCenter_;
  669. Color colorBase_;
  670. bool wiggle_;
  671. // LOGIC
  672. Ghost blinky_; // Only Inky needs this for his AI
  673. Ghosts identity_;
  674. Direction direction_;
  675. Position position_;
  676. GhostState state_;
  677. GhostState previousState_;
  678. List<Point> scatterTiles_;
  679. Point lastJunction_;
  680. DateTime timeInCurrentState;
  681. Player player_;
  682. int updatesPerPixel_;
  683. bool scheduleStateEval_;
  684. int scatterModesLeft_;
  685. int initialJumps_;
  686. int previousNumCrumps_;
  687. int updateCount_;
  688. public GhostState State { get { return state_; } set { state_ = value; timeInCurrentState = DateTime.Now; } }
  689. public Position Position { get { return position_; } }
  690. public Ghosts Identity { get { return identity_; } }
  691. }
  692. }