main.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. console.log('JavaScript Beginner Projects: Tic Tac Toe!');
  2. window.addEventListener('DOMContentLoaded', () => {
  3. _Setup();
  4. });
  5. const BOARD_STATE = {
  6. player: 1,
  7. ai: 2,
  8. blank: 3,
  9. draw: 4,
  10. };
  11. let _GAMESTATE = null;
  12. function _Setup() {
  13. _CreateBoard();
  14. _InitializeState();
  15. }
  16. function _CreateBoard() {
  17. const rows = document.getElementById('rows');
  18. for (let x = 0; x < 3; x++) {
  19. const curRow = document.createElement('div');
  20. curRow.id = 'row' + x;
  21. curRow.className = 'row';
  22. rows.appendChild(curRow);
  23. for (let y = 0; y < 3; y++) {
  24. const node = document.createElement('img');
  25. node.className = 'square';
  26. node.id = x + '.' + y;
  27. node.onclick = _HandlePlayerClick;
  28. curRow.appendChild(node);
  29. }
  30. }
  31. }
  32. function _InitializeState() {
  33. _GAMESTATE = {
  34. turn: 'player',
  35. active: true,
  36. };
  37. }
  38. function _HandlePlayerClick(evt) {
  39. const isBlank = !evt.target.src.length;
  40. if (isBlank &&
  41. _GAMESTATE.active &&
  42. _GAMESTATE.turn == 'player') {
  43. evt.target.src = 'x.png';
  44. _CheckGameOver();
  45. _AISelectMove();
  46. }
  47. }
  48. function _CheckGameOver() {
  49. const winner = _EvaluateBoard(_GetBoardStates());
  50. if (winner == null) {
  51. return;
  52. }
  53. _GAMESTATE.active = false;
  54. let desc = '';
  55. if (winner == BOARD_STATE.ai) {
  56. desc = 'You lose!';
  57. } else if (winner == BOARD_STATE.player) {
  58. desc = 'You win!';
  59. } else {
  60. desc = 'Tie game, try again.'
  61. }
  62. document.getElementById('description').innerText = desc;
  63. }
  64. function _GetBoardStates() {
  65. const boardStates = [];
  66. for (let x = 0; x < 3; x++) {
  67. const row = [];
  68. for (let y = 0; y < 3; y++) {
  69. const node = document.getElementById(x + '.' + y);
  70. if (node.src.includes('x.png')) {
  71. row.push(BOARD_STATE.player);
  72. } else if (node.src.includes('o.png')) {
  73. row.push(BOARD_STATE.ai);
  74. } else {
  75. row.push(BOARD_STATE.blank);
  76. }
  77. }
  78. boardStates.push(row);
  79. }
  80. return boardStates;
  81. }
  82. function _GetSquareElementNodes() {
  83. const nodes = [];
  84. for (let x = 0; x < 3; x++) {
  85. for (let y = 0; y < 3; y++) {
  86. nodes.push(document.getElementById(x + '.' + y))
  87. }
  88. }
  89. return nodes;
  90. }
  91. function _HighlightSquares(blinks) {
  92. if (blinks === undefined) {
  93. blinks = 10;
  94. }
  95. const nodes = _GetSquareElementNodes();
  96. for (const n of nodes) {
  97. n.className = 'square';
  98. }
  99. if (blinks >= 0) {
  100. setTimeout(() => {
  101. _AISelectMove(blinks - 1);
  102. }, 100);
  103. const x = Math.floor(Math.random() * 3);
  104. const y = Math.floor(Math.random() * 3);
  105. const node = document.getElementById(x + '.' + y);
  106. node.className = 'square highlight';
  107. return true;
  108. }
  109. return false;
  110. }
  111. function _AISelectMove(blinks) {
  112. _GAMESTATE.turn = 'ai';
  113. if (_HighlightSquares(blinks)) {
  114. return;
  115. }
  116. const boardStates = _GetBoardStates();
  117. const [_, choice] = _Minimax(boardStates, BOARD_STATE.ai);
  118. if (choice != null) {
  119. const [x, y] = choice;
  120. document.getElementById(x + '.' + y).src = 'o.png';
  121. }
  122. _CheckGameOver();
  123. _GAMESTATE.turn = 'player';
  124. }
  125. function _EvaluateBoard(boardStates) {
  126. const winningStates = [
  127. // Horizontals
  128. [[0, 0], [0, 1], [0, 2]],
  129. [[1, 0], [1, 1], [1, 2]],
  130. [[2, 0], [2, 1], [2, 2]],
  131. [[0, 0], [1, 0], [2, 0]],
  132. [[0, 1], [1, 1], [2, 1]],
  133. [[0, 2], [1, 2], [2, 2]],
  134. // Diagonals
  135. [[0, 0], [1, 1], [2, 2]],
  136. [[2, 0], [1, 1], [0, 2]],
  137. ];
  138. for (const possibleState of winningStates) {
  139. let curPlayer = null;
  140. let isWinner = true;
  141. for (const [x, y] of possibleState) {
  142. const occupant = boardStates[x][y];
  143. if (curPlayer == null && occupant != BOARD_STATE.blank) {
  144. curPlayer = occupant;
  145. } else if (curPlayer != occupant) {
  146. isWinner = false;
  147. }
  148. }
  149. if (isWinner) {
  150. return curPlayer;
  151. }
  152. }
  153. let hasMoves = false;
  154. for (let x = 0; x < 3; x++) {
  155. for (let y = 0; y < 3; y++) {
  156. if (boardStates[x][y] == BOARD_STATE.blank) {
  157. hasMoves = true;
  158. }
  159. }
  160. }
  161. if (!hasMoves) {
  162. return BOARD_STATE.draw;
  163. }
  164. return null;
  165. }
  166. function _Minimax(boardStates, player) {
  167. // First check if the game has already been won.
  168. const winner = _EvaluateBoard(boardStates);
  169. if (winner == BOARD_STATE.ai) {
  170. return [1, null];
  171. } else if (winner == BOARD_STATE.player) {
  172. return [-1, null];
  173. }
  174. let move, moveScore;
  175. if (player == BOARD_STATE.ai) {
  176. [moveScore, move] = _Minimax_Maximize(boardStates);
  177. } else {
  178. [moveScore, move] = _Minimax_Minimize(boardStates);
  179. }
  180. if (move == null) {
  181. moveScore = 0;
  182. }
  183. // No move, so it's a draw
  184. return [moveScore, move];
  185. }
  186. function _Minimax_Maximize(boardStates) {
  187. let moveScore = Number.NEGATIVE_INFINITY;
  188. let move = null;
  189. for (let x = 0; x < 3; x++) {
  190. for (let y = 0; y < 3; y++) {
  191. if (boardStates[x][y] == BOARD_STATE.blank) {
  192. const newBoardStates = boardStates.map(r => r.slice());
  193. newBoardStates[x][y] = BOARD_STATE.ai;
  194. const [newMoveScore, _] = _Minimax(
  195. newBoardStates, BOARD_STATE.player);
  196. if (newMoveScore > moveScore) {
  197. move = [x, y];
  198. moveScore = newMoveScore;
  199. }
  200. }
  201. }
  202. }
  203. return [moveScore, move];
  204. }
  205. function _Minimax_Minimize(boardStates) {
  206. let moveScore = Number.POSITIVE_INFINITY;
  207. let move = null;
  208. for (let x = 0; x < 3; x++) {
  209. for (let y = 0; y < 3; y++) {
  210. if (boardStates[x][y] == BOARD_STATE.blank) {
  211. const newBoardStates = boardStates.map(r => r.slice());
  212. newBoardStates[x][y] = BOARD_STATE.player;
  213. const [newMoveScore, _] = _Minimax(
  214. newBoardStates, BOARD_STATE.ai);
  215. if (newMoveScore < moveScore) {
  216. move = [x, y];
  217. moveScore = newMoveScore;
  218. }
  219. }
  220. }
  221. }
  222. return [moveScore, move];
  223. }
  224. function _Minimax2(boardStates, aiTurn) {
  225. // First check if the game has already been won.
  226. const winner = _EvaluateBoard(boardStates);
  227. if (winner == BOARD_STATE.ai) {
  228. return [1, null];
  229. } else if (winner == BOARD_STATE.player) {
  230. return [-1, null];
  231. }
  232. let moveCost = Number.NEGATIVE_INFINITY;
  233. if (!aiTurn) {
  234. moveCost = Number.POSITIVE_INFINITY;
  235. }
  236. let move = null;
  237. for (let x = 0; x < 3; x++) {
  238. for (let y = 0; y < 3; y++) {
  239. if (boardStates[x][y] == BOARD_STATE.blank) {
  240. const newBoardStates = boardStates.map(r => r.slice());
  241. if (aiTurn) {
  242. newBoardStates[x][y] = BOARD_STATE.ai;
  243. } else {
  244. newBoardStates[x][y] = BOARD_STATE.player;
  245. }
  246. const [newMoveCost, _] = _Minimax(newBoardStates, !aiTurn);
  247. if (aiTurn) {
  248. if (newMoveCost > moveCost) {
  249. move = [x, y];
  250. moveCost = newMoveCost;
  251. }
  252. } else {
  253. if (newMoveCost < moveCost) {
  254. move = [x, y];
  255. moveCost = newMoveCost;
  256. }
  257. }
  258. }
  259. }
  260. }
  261. if (move != null) {
  262. return [moveCost, move];
  263. }
  264. // No move, so it's a draw
  265. return [0, null];
  266. }