connect-four.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Connect 4</title>
  6. <link rel="stylesheet" href="styles.css">
  7. <link href="favicon.ico" rel="icon" type="image/x-icon" />
  8. </head>
  9. <body>
  10. <div class='text'>
  11. <h1 id="h1">Connect 4</h1>
  12. <br>
  13. </div>
  14. <button onclick="start_game();" class='button' id='button'>Start Game</button>
  15. <script>
  16. function start_game() {
  17. "use strict";
  18. let button = document.getElementById("button");
  19. let h1 = document.getElementById("h1");
  20. h1.remove();
  21. button.remove();
  22. // game parameters
  23. const DELAY_COMP = 0.5; // seconds for the computer to take its turn
  24. const GRID_CIRCLE = 0.7; // circle size as a fraction of cell size
  25. const GRID_COLS = 7; // number of game columns
  26. const GRID_ROWS = 6; // number of game rows
  27. const MARGIN = 0.02; // margin as a fraction of the shortest screen dimension
  28. // colour variables
  29. const COLOR_BACKGROUND = "mintcream";
  30. const COLOR_COMP = "yellow";
  31. const COLOR_COMP_DRK = "olive";
  32. const COLOR_FRAME = "dodgerblue";
  33. const COLOR_FRAME_BUTT = "royalblue";
  34. const COLOR_PLAY = "red";
  35. const COLOR_PLAY_DRK = "darkred";
  36. const COLOR_TIE = "darkgrey";
  37. const COLOR_TIE_DRK = "black";
  38. const COLOR_WIN = "black";
  39. // text variables
  40. const TEXT_COMP = "Computer";
  41. const TEXT_PLAY = "YOU";
  42. const TEXT_TIE = "DRAW";
  43. const TEXT_WIN = "WON!";
  44. // cell class
  45. class Cell {
  46. constructor(left, top, w, h, row, col) {
  47. this.bot = top + h;
  48. this.left = left;
  49. this.right = left + w;
  50. this.top = top;
  51. this.w = w;
  52. this.h = h;
  53. this.row = row;
  54. this.col = col;
  55. this.cx = left + w / 2;
  56. this.cy = top + h / 2;
  57. this.r = w * GRID_CIRCLE / 2;
  58. this.highlight = null;
  59. this.owner = null;
  60. this.winner = false;
  61. }
  62. contains(x, y) {
  63. return x > this.left && x < this.right && y > this.top && y < this.bot;
  64. }
  65. // draw the circle or hole
  66. draw(/** @type {CanvasRenderingContext2D} */ ctx) {
  67. // owner colour
  68. let color = this.owner == null ? COLOR_BACKGROUND : this.owner ? COLOR_PLAY : COLOR_COMP;
  69. // draw the circle
  70. ctx.fillStyle = color;
  71. ctx.beginPath();
  72. ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2);
  73. ctx.fill();
  74. // draw highlighting
  75. if (this.winner || this.highlight != null) {
  76. // colour
  77. color = this.winner ? COLOR_WIN : this.highlight ? COLOR_PLAY : COLOR_COMP;
  78. // draw a circle around the perimeter
  79. ctx.lineWidth = this.r / 4;
  80. ctx.strokeStyle = color;
  81. ctx.beginPath();
  82. ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2);
  83. ctx.stroke();
  84. }
  85. }
  86. }
  87. // set up the canvas and context
  88. var canv = document.createElement("canvas");
  89. document.body.appendChild(canv);
  90. var ctx = canv.getContext("2d");
  91. // game variables
  92. var gameOver, gameTied, grid = [], playersTurn, timeComp;
  93. // dimensions
  94. var height, width, margin;
  95. setDimensions();
  96. // event listeners
  97. canv.addEventListener("click", click);
  98. canv.addEventListener("mousemove", highlightGrid);
  99. window.addEventListener("resize", setDimensions);
  100. // game loop
  101. var timeDelta, timeLast;
  102. requestAnimationFrame(loop);
  103. function loop(timeNow) {
  104. // initialise timeLast
  105. if (!timeLast) {
  106. timeLast = timeNow;
  107. }
  108. // calculate the time difference
  109. timeDelta = (timeNow - timeLast) / 1000; // seconds
  110. timeLast = timeNow;
  111. // update
  112. goComputer(timeDelta);
  113. // draw
  114. drawBackground();
  115. drawGrid();
  116. drawText();
  117. // call the next frame
  118. requestAnimationFrame(loop);
  119. }
  120. function checkWin(row, col) {
  121. // get all the cells from each direction
  122. let diagL = [], diagR = [], horiz = [], vert = [];
  123. for (let i = 0; i < GRID_ROWS; i++) {
  124. for (let j = 0; j < GRID_COLS; j++) {
  125. // horizontal cells
  126. if (i == row) {
  127. horiz.push(grid[i][j]);
  128. }
  129. // vertical cells
  130. if (j == col) {
  131. vert.push(grid[i][j]);
  132. }
  133. // top left to bottom right
  134. if (i - j == row - col) {
  135. diagL.push(grid[i][j]);
  136. }
  137. // top right to bottom left
  138. if (i + j == row + col) {
  139. diagR.push(grid[i][j]);
  140. }
  141. }
  142. }
  143. // if any have four in a row, return a win!
  144. return connect4(diagL) || connect4(diagR) || connect4(horiz) || connect4(vert);
  145. }
  146. function connect4(cells = []) {
  147. let count = 0, lastOwner = null;
  148. let winningCells = [];
  149. for (let i = 0; i < cells.length; i++) {
  150. // no owner, reset the count
  151. if (cells[i].owner == null) {
  152. count = 0;
  153. winningCells = [];
  154. }
  155. // same owner, add to the count
  156. else if (cells[i].owner == lastOwner) {
  157. count++;
  158. winningCells.push(cells[i]);
  159. }
  160. // new owner, new count
  161. else {
  162. count = 1;
  163. winningCells = [];
  164. winningCells.push(cells[i]);
  165. }
  166. // set the lastOwner
  167. lastOwner = cells[i].owner;
  168. // four in a row is a win
  169. if (count == 4) {
  170. for (let cell of winningCells) {
  171. cell.winner = true;
  172. }
  173. return true;
  174. }
  175. }
  176. return false;
  177. }
  178. function click(ev) {
  179. if (gameOver) {
  180. newGame();
  181. return;
  182. }
  183. if (!playersTurn) {
  184. return;
  185. }
  186. selectCell();
  187. }
  188. function createGrid() {
  189. grid = [];
  190. // set up cell size and margins
  191. let cell, marginX, marginY;
  192. // portrait
  193. if ((width - margin * 2) * GRID_ROWS / GRID_COLS < height - margin * 2) {
  194. cell = (width - margin * 2) / GRID_COLS;
  195. marginX = margin;
  196. marginY = (height - cell * GRID_ROWS) / 2;
  197. }
  198. // landscape
  199. else {
  200. cell = (height - margin * 2) / GRID_ROWS;
  201. marginX = (width - cell * GRID_COLS) / 2;
  202. marginY = margin;
  203. }
  204. // populate the grid
  205. for (let i = 0; i < GRID_ROWS; i++) {
  206. grid[i] = [];
  207. for (let j = 0; j < GRID_COLS; j++) {
  208. let left = marginX + j * cell;
  209. let top = marginY + i * cell;
  210. grid[i][j] = new Cell(left, top, cell, cell, i, j);
  211. }
  212. }
  213. }
  214. function drawBackground() {
  215. ctx.fillStyle = COLOR_BACKGROUND;
  216. ctx.fillRect(0, 0, width, height);
  217. }
  218. function drawGrid() {
  219. // frame and butt
  220. let cell = grid[0][0];
  221. let fh = cell.h * GRID_ROWS;
  222. let fw = cell.w * GRID_COLS;
  223. ctx.fillStyle = COLOR_FRAME;
  224. ctx.fillRect(cell.left, cell.top, fw, fh);
  225. ctx.fillStyle = COLOR_FRAME_BUTT;
  226. ctx.fillRect(cell.left - margin / 2, cell.top + fh - margin / 2, fw + margin, margin);
  227. // cells
  228. for (let row of grid) {
  229. for (let cell of row) {
  230. cell.draw(ctx);
  231. }
  232. }
  233. }
  234. function drawText() {
  235. if (!gameOver) {
  236. return;
  237. }
  238. // set up text parameters
  239. let size = grid[0][0].h;
  240. ctx.fillStyle = gameTied ? COLOR_TIE : playersTurn ? COLOR_PLAY : COLOR_COMP;
  241. ctx.font = size + "px dejavu sans mono";
  242. ctx.lineJoin = "round";
  243. ctx.lineWidth = size / 10;
  244. ctx.strokeStyle = gameTied ? COLOR_TIE_DRK : playersTurn ? COLOR_PLAY_DRK : COLOR_COMP_DRK;
  245. ctx.textAlign = "center";
  246. ctx.textBaseline = "middle";
  247. // draw the text
  248. let offset = size * 0.55;
  249. let text = gameTied ? TEXT_TIE : playersTurn ? TEXT_PLAY : TEXT_COMP;
  250. if (gameTied) {
  251. ctx.strokeText(text, width / 2, height / 2);
  252. ctx.fillText(text, width / 2, height / 2);
  253. } else {
  254. ctx.strokeText(text, width / 2, height / 2 - offset);
  255. ctx.fillText(text, width / 2, height / 2 - offset);
  256. ctx.strokeText(TEXT_WIN, width / 2, height / 2 + offset);
  257. ctx.fillText(TEXT_WIN, width / 2, height / 2 + offset);
  258. }
  259. }
  260. function goComputer(delta) {
  261. if (playersTurn || gameOver) {
  262. return;
  263. }
  264. // count down till the computer makes its selection
  265. if (timeComp > 0) {
  266. timeComp -= delta;
  267. if (timeComp <= 0) {
  268. selectCell();
  269. }
  270. return;
  271. }
  272. // set up the options array
  273. let options = [];
  274. options[0] = []; // computer wins
  275. options[1] = []; // block the player from winning
  276. options[2] = []; // no significance
  277. options[3] = []; // give away a win
  278. // loop through each column
  279. let cell;
  280. for (let i = 0; i < GRID_COLS; i++) {
  281. cell = highlightCell(grid[0][i].cx, grid[0][i].cy);
  282. // column full, go to the next column
  283. if (cell == null) {
  284. continue;
  285. }
  286. // first priority, computer wins
  287. cell.owner = playersTurn;
  288. if (checkWin(cell.row, cell.col)) {
  289. options[0].push(i);
  290. } else {
  291. // second priority, block the player
  292. cell.owner = !playersTurn;
  293. if (checkWin(cell.row, cell.col)) {
  294. options[1].push(i);
  295. } else {
  296. cell.owner = playersTurn;
  297. // check the cell above
  298. if (cell.row > 0) {
  299. grid[cell.row - 1][cell.col].owner = !playersTurn;
  300. // last priority, let player win
  301. if (checkWin(cell.row - 1, cell.col)) {
  302. options[3].push(i);
  303. }
  304. // third priority, no significance
  305. else {
  306. options[2].push(i);
  307. }
  308. // deselect cell above
  309. grid[cell.row - 1][cell.col].owner = null;
  310. }
  311. // no row above, third priority, no significance
  312. else {
  313. options[2].push(i);
  314. }
  315. }
  316. }
  317. // cancel highlight and selection
  318. cell.highlight = null;
  319. cell.owner = null;
  320. }
  321. // clear the winning cells
  322. for (let row of grid) {
  323. for (let cell of row) {
  324. cell.winner = false;
  325. }
  326. }
  327. // randomly select a column in priority order
  328. let col;
  329. if (options[0].length > 0) {
  330. col = options[0][Math.floor(Math.random() * options[0].length)];
  331. } else if (options[1].length > 0) {
  332. col = options[1][Math.floor(Math.random() * options[1].length)];
  333. } else if (options[2].length > 0) {
  334. col = options[2][Math.floor(Math.random() * options[2].length)];
  335. } else if (options[3].length > 0) {
  336. col = options[3][Math.floor(Math.random() * options[3].length)];
  337. }
  338. // highlight the selected cell
  339. highlightCell(grid[0][col].cx, grid[0][col].cy);
  340. // set the delay
  341. timeComp = DELAY_COMP;
  342. }
  343. function highlightCell(x, y) {
  344. let col = null;
  345. for (let row of grid) {
  346. for (let cell of row) {
  347. // clear existing highlighting
  348. cell.highlight = null;
  349. // get the column
  350. if (cell.contains(x, y)) {
  351. col = cell.col;
  352. }
  353. }
  354. }
  355. if (col == null) {
  356. return;
  357. }
  358. // highlight the first unoccupied cell
  359. for (let i = GRID_ROWS - 1; i >= 0; i--) {
  360. if (grid[i][col].owner == null) {
  361. grid[i][col].highlight = playersTurn;
  362. return grid[i][col];
  363. }
  364. }
  365. return null;
  366. }
  367. function highlightGrid(/** @type {MouseEvent} */ ev) {
  368. if (!playersTurn || gameOver) {
  369. return;
  370. }
  371. highlightCell(ev.clientX, ev.clientY);
  372. }
  373. function newGame() {
  374. playersTurn = Math.random() < 0.5;
  375. gameOver = false;
  376. gameTied = false;
  377. createGrid();
  378. }
  379. function selectCell() {
  380. let highlighting = false;
  381. OUTER: for (let row of grid) {
  382. for (let cell of row) {
  383. if (cell.highlight != null) {
  384. highlighting = true;
  385. cell.highlight = null;
  386. cell.owner = playersTurn;
  387. if (checkWin(cell.row, cell.col)) {
  388. gameOver = true;
  389. }
  390. break OUTER;
  391. }
  392. }
  393. }
  394. // don't allow selection if no highlighting
  395. if (!highlighting) {
  396. return;
  397. }
  398. // check for a tied game
  399. if (!gameOver) {
  400. gameTied = true;
  401. OUTER: for (let row of grid) {
  402. for (let cell of row) {
  403. if (cell.owner == null) {
  404. gameTied = false;
  405. break OUTER;
  406. }
  407. }
  408. }
  409. // set game over
  410. if (gameTied) {
  411. gameOver = true;
  412. }
  413. }
  414. // switch the player if no game over
  415. if (!gameOver) {
  416. playersTurn = !playersTurn;
  417. }
  418. }
  419. function setDimensions() {
  420. height = window.innerHeight;
  421. width = window.innerWidth;
  422. canv.height = height;
  423. canv.width = width;
  424. margin = MARGIN * Math.min(height, width);
  425. newGame();
  426. }
  427. }
  428. </script>
  429. </body>
  430. </html>