connect-four.html 16 KB

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