game_manager.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. function GameManager(size, InputManager, Actuator, StorageManager) {
  2. this.size = size; // Size of the grid
  3. this.inputManager = new InputManager;
  4. this.storageManager = new StorageManager;
  5. this.actuator = new Actuator;
  6. this.startTiles = 2;
  7. this.inputManager.on("move", this.move.bind(this));
  8. this.inputManager.on("restart", this.restart.bind(this));
  9. this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
  10. this.setup();
  11. }
  12. // Restart the game
  13. GameManager.prototype.restart = function () {
  14. this.storageManager.clearGameState();
  15. this.actuator.continueGame(); // Clear the game won/lost message
  16. this.setup();
  17. };
  18. // Keep playing after winning (allows going over 2048)
  19. GameManager.prototype.keepPlaying = function () {
  20. this.keepPlaying = true;
  21. this.actuator.continueGame(); // Clear the game won/lost message
  22. };
  23. // Return true if the game is lost, or has won and the user hasn't kept playing
  24. GameManager.prototype.isGameTerminated = function () {
  25. return this.over || (this.won && !this.keepPlaying);
  26. };
  27. // Set up the game
  28. GameManager.prototype.setup = function () {
  29. var previousState = this.storageManager.getGameState();
  30. // Reload the game from a previous game if present
  31. if (previousState) {
  32. this.grid = new Grid(previousState.grid.size,
  33. previousState.grid.cells); // Reload grid
  34. this.score = previousState.score;
  35. this.over = previousState.over;
  36. this.won = previousState.won;
  37. this.keepPlaying = previousState.keepPlaying;
  38. } else {
  39. this.grid = new Grid(this.size);
  40. this.score = 0;
  41. this.over = false;
  42. this.won = false;
  43. this.keepPlaying = false;
  44. // Add the initial tiles
  45. this.addStartTiles();
  46. }
  47. // Update the actuator
  48. this.actuate();
  49. };
  50. // Set up the initial tiles to start the game with
  51. GameManager.prototype.addStartTiles = function () {
  52. for (var i = 0; i < this.startTiles; i++) {
  53. this.addRandomTile();
  54. }
  55. };
  56. // Adds a tile in a random position
  57. GameManager.prototype.addRandomTile = function () {
  58. if (this.grid.cellsAvailable()) {
  59. var value = Math.random() < 0.9 ? 2 : 4;
  60. var tile = new Tile(this.grid.randomAvailableCell(), value);
  61. this.grid.insertTile(tile);
  62. }
  63. };
  64. // Sends the updated grid to the actuator
  65. GameManager.prototype.actuate = function () {
  66. if (this.storageManager.getBestScore() < this.score) {
  67. this.storageManager.setBestScore(this.score);
  68. }
  69. // Clear the state when the game is over (game over only, not win)
  70. if (this.over) {
  71. this.storageManager.clearGameState();
  72. } else {
  73. this.storageManager.setGameState(this.serialize());
  74. }
  75. this.actuator.actuate(this.grid, {
  76. score: this.score,
  77. over: this.over,
  78. won: this.won,
  79. bestScore: this.storageManager.getBestScore(),
  80. terminated: this.isGameTerminated()
  81. });
  82. };
  83. // Represent the current game as an object
  84. GameManager.prototype.serialize = function () {
  85. return {
  86. grid: this.grid.serialize(),
  87. score: this.score,
  88. over: this.over,
  89. won: this.won,
  90. keepPlaying: this.keepPlaying
  91. };
  92. };
  93. // Save all tile positions and remove merger info
  94. GameManager.prototype.prepareTiles = function () {
  95. this.grid.eachCell(function (x, y, tile) {
  96. if (tile) {
  97. tile.mergedFrom = null;
  98. tile.savePosition();
  99. }
  100. });
  101. };
  102. // Move a tile and its representation
  103. GameManager.prototype.moveTile = function (tile, cell) {
  104. this.grid.cells[tile.x][tile.y] = null;
  105. this.grid.cells[cell.x][cell.y] = tile;
  106. tile.updatePosition(cell);
  107. };
  108. // Move tiles on the grid in the specified direction
  109. GameManager.prototype.move = function (direction) {
  110. // 0: up, 1: right, 2: down, 3: left
  111. var self = this;
  112. if (this.isGameTerminated()) return; // Don't do anything if the game's over
  113. var cell, tile;
  114. var vector = this.getVector(direction);
  115. var traversals = this.buildTraversals(vector);
  116. var moved = false;
  117. // Save the current tile positions and remove merger information
  118. this.prepareTiles();
  119. // Traverse the grid in the right direction and move tiles
  120. traversals.x.forEach(function (x) {
  121. traversals.y.forEach(function (y) {
  122. cell = { x: x, y: y };
  123. tile = self.grid.cellContent(cell);
  124. if (tile) {
  125. var positions = self.findFarthestPosition(cell, vector);
  126. var next = self.grid.cellContent(positions.next);
  127. // Only one merger per row traversal?
  128. if (next && next.value === tile.value && !next.mergedFrom) {
  129. var merged = new Tile(positions.next, tile.value * 2);
  130. merged.mergedFrom = [tile, next];
  131. self.grid.insertTile(merged);
  132. self.grid.removeTile(tile);
  133. // Converge the two tiles' positions
  134. tile.updatePosition(positions.next);
  135. // Update the score
  136. self.score += merged.value;
  137. // The mighty 2048 tile
  138. if (merged.value === 2048) self.won = true;
  139. } else {
  140. self.moveTile(tile, positions.farthest);
  141. }
  142. if (!self.positionsEqual(cell, tile)) {
  143. moved = true; // The tile moved from its original cell!
  144. }
  145. }
  146. });
  147. });
  148. if (moved) {
  149. this.addRandomTile();
  150. if (!this.movesAvailable()) {
  151. this.over = true; // ÄãÊäÀ²!
  152. }
  153. this.actuate();
  154. }
  155. };
  156. // Get the vector representing the chosen direction
  157. GameManager.prototype.getVector = function (direction) {
  158. // Vectors representing tile movement
  159. var map = {
  160. 0: { x: 0, y: -1 }, // Up
  161. 1: { x: 1, y: 0 }, // Right
  162. 2: { x: 0, y: 1 }, // Down
  163. 3: { x: -1, y: 0 } // Left
  164. };
  165. return map[direction];
  166. };
  167. // Build a list of positions to traverse in the right order
  168. GameManager.prototype.buildTraversals = function (vector) {
  169. var traversals = { x: [], y: [] };
  170. for (var pos = 0; pos < this.size; pos++) {
  171. traversals.x.push(pos);
  172. traversals.y.push(pos);
  173. }
  174. // Always traverse from the farthest cell in the chosen direction
  175. if (vector.x === 1) traversals.x = traversals.x.reverse();
  176. if (vector.y === 1) traversals.y = traversals.y.reverse();
  177. return traversals;
  178. };
  179. GameManager.prototype.findFarthestPosition = function (cell, vector) {
  180. var previous;
  181. // Progress towards the vector direction until an obstacle is found
  182. do {
  183. previous = cell;
  184. cell = { x: previous.x + vector.x, y: previous.y + vector.y };
  185. } while (this.grid.withinBounds(cell) &&
  186. this.grid.cellAvailable(cell));
  187. return {
  188. farthest: previous,
  189. next: cell // Used to check if a merge is required
  190. };
  191. };
  192. GameManager.prototype.movesAvailable = function () {
  193. return this.grid.cellsAvailable() || this.tileMatchesAvailable();
  194. };
  195. // Check for available matches between tiles (more expensive check)
  196. GameManager.prototype.tileMatchesAvailable = function () {
  197. var self = this;
  198. var tile;
  199. for (var x = 0; x < this.size; x++) {
  200. for (var y = 0; y < this.size; y++) {
  201. tile = this.grid.cellContent({ x: x, y: y });
  202. if (tile) {
  203. for (var direction = 0; direction < 4; direction++) {
  204. var vector = self.getVector(direction);
  205. var cell = { x: x + vector.x, y: y + vector.y };
  206. var other = self.grid.cellContent(cell);
  207. if (other && other.value === tile.value) {
  208. return true; // These two tiles can be merged
  209. }
  210. }
  211. }
  212. }
  213. }
  214. return false;
  215. };
  216. GameManager.prototype.positionsEqual = function (first, second) {
  217. return first.x === second.x && first.y === second.y;
  218. };