5
\$\begingroup\$

Gomoku ★★★ (Tic Tac Toe)

There are many Gomoku (5 in a row) game projects on the Internet. The scripts of these projects are freely available. Having analyzed them, I created my own
game code based on algorithms taken from the Internet. I have a simple script. The rules of the game are almost the same, that is, the winner is the one who collects 5 letters in a row (either vertically, horizontally, or diagonally). In case of winning, a winner's line is drawn connecting 5 letters. I would like experienced programmers to point out my mistakes.

<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Gomoku game 5 in a row</title> <style> body { text-align: center; } #canvas { border: 2px solid green; background-color: lightblue; } </style> </head> <body> <h1>Gomoku game 5 in a row. Javascript simple game Canvas.</h1> <p id="gameStateEl"></p> <p id="gameStateE2"></p> <canvas id="mycanvas"></canvas> <div> <button id="ReStart"type="button" onclick="ReStart();">ReStart</button> </div> <script> const canvas = document.getElementById('mycanvas'); const ctx = canvas.getContext("2d"); const CELL_SIZE = 50; const ROWS = 15, COLS = 15; const CELL_COUNT = ROWS * COLS; const PLAY_COLOR = "teal"; var cellCol = PLAY_COLOR; //Positions on the board const boardPosition = {x: 0, y: 0, w: COLS * CELL_SIZE, h: ROWS * CELL_SIZE}; canvas.width = boardPosition.x + boardPosition.w; canvas.height = boardPosition.y + boardPosition.h; var count_1 = 0; var count_2 = 0; var count_3 = 0; const cells = []; var num = ROWS + 5; //Determine whether the current click is X or O var coords = []; //Save the clicked coordinates record = []; var isWin = false, a1, a2, a3, a4, a5; //Define the unplaced state of the chessboard as 0 for(var i = 0; i<num; i++){ var arr = []; for(var j = 0; j < num; j++){ arr.push(0); } record.push(arr); } var isChangePlayer = false; var first_X = 0; var first_Y = 0; var last_X = 0; var last_Y = 0; function setFont() { ctx.font = "60px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; } //The initBoard method initializes the game board by //setting the height and width of the board, //creating an array to represent the board, and filling the array with zeros. function initBoard() { var cellIdx = 0; cellCol = PLAY_COLOR; //game State elements gameStateEl.textContent = "Player_1 no moves."; gameStateE2.textContent = "Player_2 no moves."; } //Drawing board background function drawBoardBackground() { ctx.beginPath(); //Nicknames for ease of reading const bP = boardPosition; /* BM67 local alias */ ctx.rect(bP.x, bP.y, bP.w, bP.h); ctx.fillStyle = "rgba(0,122,0, 0.2)"; ctx.strokeStyle = "fuchsia"; ctx.fill(); ctx.beginPath(); for (let i = 0; i <= COLS; i++) { ctx.moveTo(bP.x, bP.y + i * CELL_SIZE); ctx.lineTo(bP.x + bP.w, bP.y + i * CELL_SIZE); ctx.moveTo(bP.x + i * CELL_SIZE, bP.y); ctx.lineTo(bP.x + i * CELL_SIZE, bP.y + bP.h); } ctx.stroke(); } //draw a cell function drawCell(cellIdx) { let val = ""; const x = (cellIdx % COLS) * CELL_SIZE; const y = (cellIdx / COLS | 0) * CELL_SIZE; ctx.fillStyle = "blue"; ctx.shadowColor = '#000'; ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE); ctx.fillStyle = cellCol; ctx.shadowBlur = 4; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 2; ctx.fillRect(x + 5, y + 5, CELL_SIZE - 10, CELL_SIZE - 10); ctx.fillText(val, x + CELL_SIZE * 0.5, y + CELL_SIZE * 0.5); } //let's draw a game function drawGame() { setFont(); var cellIdx = 0; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawBoardBackground(); while (cellIdx < CELL_COUNT) { drawCell(cellIdx ++); } } //Cell pressed function cellClicked(clickIdx) { /* BM67 returns true if board needs redraw */ const x = clickIdx % COLS; const y = clickIdx / COLS | 0; if(coords.length === 0){ draw_X(x, y ,ctx); isChangePlayer = true; addCoords(x, y, 1); }else{ if(verdictCoords(x, y)){ if(!isChangePlayer){ draw_X(x, y); addCoords(x, y, 1); isChangePlayer = true; }else { draw_O(x, y); addCoords(x, y, 2); isChangePlayer = false; } console.log('isVictory() = ' + isVictory()); if(isVictory() && isChangePlayer === true){ drawLine(x, y); my_color = "blue"; <!-- alert("GAME OVER --- blue"); --> //game over console.log("GAME OVER --- Winner blue"); gameStateEl.textContent = "Player_1 Winner blue " + count_1 + " moves."; } if(isVictory() && isChangePlayer === false){ drawLine(x, y); my_color = "red"; <!-- alert("GAME OVER --- red"); --> //game over console.log("GAME OVER --- Winner red" ); gameStateE2.textContent = "Player_2 Winner red " + count_2 + " moves."; } } } } //Adding an onClick handler to a canvas element canvas.addEventListener("mousedown", function (e) { //Nicknames for ease of reading const bP = boardPosition; /* BM67 local alias */ const x = Math.floor((e.offsetX - bP.x) / CELL_SIZE); /* 0 - 3 on board cell */ const y = Math.floor((e.offsetY - bP.y) / CELL_SIZE); /* 0 - 3 on board cell */ if(isChangePlayer === false){ count_1 += 1; gameStateEl.textContent = "Player_1 - " + count_1 + " moves."; } if(isChangePlayer === true){ count_2 += 1; gameStateE2.textContent = "Player_2 - " + count_2 + " moves."; } if (cellClicked(x + y * COLS)) { moveCount += 1; drawGame(); } if (isVictory() === true) { count_3 += 1; } if (count_3 === 2) { ReStart(); } console.log('count_3 = ' + count_3); }); //Draw a X function draw_X(x, y){ editRecord(x, y, 1); ctx.fillStyle = "blue"; ctx.beginPath(); ctx.font = "40px Verdana"; ctx.fillText("X", x * CELL_SIZE + 25, y * CELL_SIZE + 27); ctx.closePath(); ctx.fill(); } //Draw a O function draw_O(x, y){ editRecord(x, y, 2); ctx.fillStyle = "red"; ctx.beginPath(); ctx.font = "40px Verdana"; ctx.fillText("O", x * CELL_SIZE + 25, y * CELL_SIZE + 27); ctx.closePath(); ctx.fill(); } //Let's draw the winner's line. function drawLine(x, y){ editRecord(x, y, 1); ctx.beginPath(); ctx.moveTo(first_X * CELL_SIZE + 25, first_Y * CELL_SIZE + 25); ctx.lineTo(last_X * CELL_SIZE + 25, last_Y * CELL_SIZE + 25); if(isChangePlayer === true){ ctx.strokeStyle = "blue"; } else{ ctx.strokeStyle = "red"; } ctx.lineWidth = 11; ctx.stroke(); ctx.closePath(); } //Add mark to clicked function addCoords(x, y, num){ coords.push({ x: x, y: y, num: num }) } //Edit the marks on the board, //1 represents the red move, 2 represents the black move function editRecord(x, y, num){ record.forEach(function(item, index){ if(index === y){ item.forEach(function(item, index){ if(x === index){ record[y][x] = num; } }) } }) } //Determine whether the current position is placed function verdictCoords(x, y){ var isTrue = true; coords.forEach(function(item, index){ if(item.x === x && item.y === y){ isTrue = false; } }) return isTrue } //Determine whether the mark on the board //meets the victory condition function isVictory(){ var len = record.length; for(var i = 0; i < record.length; i++){ for(var j=0; j<record[i].length; j++){ if(record[i][j] !==0 ){ if(j < len && i - 1 < len){ //Determine the horizontal direction a1 = record[i][j]; a2 = record[i][j + 1]; a3 = record[i][j + 2]; a4 = record[i][j + 3]; a5 = record[i][j + 4]; if(isEqual(a1, a2, a3, a4, a5)){ first_X = j; first_Y = i; last_X = j + 4; last_Y = i; console.log('first_X = ' + first_X); console.log('first_Y = ' + first_Y); console.log('last_X = ' + last_X); console.log('last_Y = ' + last_Y); isWin = true; } //Determine the direction of the forward slash a1 = record[i][j]; a2 = record[i + 1][j + 1]; a3 = record[i + 2][j + 2]; a4 = record[i + 3][j + 3]; a5 = record[i + 4][j + 4]; if(isEqual(a1, a2, a3, a4, a5)){ first_X = j; first_Y = i; last_X = j + 4; last_Y = i + 4; console.log('first_X = ' + first_X); console.log('first_Y = ' + first_Y); console.log('last_X = ' + last_X); console.log('last_Y = ' + last_Y); isWin = true; } //Judging the vertical direction a1 = record[i][j]; a2 = record[i + 1][j]; a3 = record[i + 2][j]; a4 = record[i + 3][j]; a5 = record[i + 4][j]; if(isEqual(a1, a2, a3, a4, a5)){ first_X = j; first_Y = i; last_X = j; last_Y = i + 4; console.log('first_X = ' + first_X); console.log('first_Y = ' + first_Y); console.log('last_X = ' + last_X); console.log('last_Y = ' + last_Y); isWin = true; } //Determine the direction of the backslash a1 = record[i][j]; a2 = record[i + 1][j - 1]; a3 = record[i + 2][j - 2]; a4 = record[i + 3][j - 3]; a5 = record[i + 4][j - 4]; if(isEqual(a1, a2, a3, a4, a5)){ first_X = j; first_Y = i; last_X = j - 4; last_Y = i + 4; console.log('first_X = ' + first_X); console.log('first_Y = ' + first_Y); console.log('last_X = ' + last_X); console.log('last_Y = ' + last_Y); isWin = true; } } } } } return isWin; } isVictory(); //Determine whether the adjacent 5 numbers are equal, //if they are equal, it means that the winner has been divided function isEqual(a1, a2, a3, a4, a5){ if(a1 == a2 && a2==a3 && a3==a4 && a4 == a5){ return true; }else { return false; } } initBoard(); drawGame(); function ReStart() { document.location.reload(); } </script> </body> </html> 
\$\endgroup\$
3
  • 2
    \$\begingroup\$Consider formatting your code (for future posts/code you write, can't change this one now that it's been answered).\$\endgroup\$
    – ggorlen
    CommentedSep 1, 2024 at 2:45
  • 1
    \$\begingroup\$@ggorlen Formatting is reviewable, feel free to expand a bit on your comment in an answer. Reviews gonin answers, not in comments.\$\endgroup\$
    – Mast
    CommentedSep 1, 2024 at 5:59
  • \$\begingroup\$Thanks for the advice. In the future I will try to format my code more correctly.\$\endgroup\$CommentedSep 2, 2024 at 9:30

5 Answers 5

3
\$\begingroup\$

Formatting:

  • Format your code would look nicer. Function or if-blocks will have indentation and you can more easily spot what belongs together. Formatting is a function of your IDE. You might need to put the JavaScript in a separate file (which seems good practice anyway.).

Naming of variables:

  • e.g. count_1, count_2, count_3 could have been count_X, count_O, count_total
  • e.g. a1, a2, a3, a4, a5
  • e.g. coords does not just store coordinates, also the "mark" of the cell. I called it marks
  • e.g. record It is not what I would think of as a record. It represents the grid of cells on the board. I called it board_grid.
  • e.g. isChangePlayer You meant here is O-player. In my version I referred to the players by index: X = 0 and O = 1. I used a property player_on_turn_index on the game object, which is a number.
  • e.g. isTrue When you start writing isTrue = false, there must be an alarm going of: naming is not logical! :-)

Scoping and objects:

  • You use many global variables that are modified inside global functions. It is usually considered better practice to store state in bundles that belong together as objects with state and methods. You can than access them by storing a limited set of objects as global, you can modify the state inside the methods. This seems easier to understand because you can more easily find which things something belongs and you can better reason about where it could possibly be accessed. It gives you a better general overview of the code.

Repetition:

  • Some of your code was repeted in if and else blocks. e.g. the function cellClicked contains a block for the same action for player 1 and player two. The logic is the same, except for the letter and the color of the mark. It seems more easily to understand if you set the if and else branch for just picking the letter and the color and leave the rest of the code common. It might be better in order not to forget both conditions when you would change something.

More concise formulations:

editRecord

function editRecord(x, y, num){ record.forEach(function(item, index){ if(index === y){ item.forEach(function(item, index){ if(x === index){ record[y][x] = num; } }) } }) } 

This function is not needed at all:

record[y][x] = num 

! :-)

verdictCoords

let isMarked = verdictCoords(x, y) function verdictCoords(x, y){ var isTrue = true; coords.forEach(function(item, index){ if(item.x === x && item.y === y){ isTrue = false; } }) return isTrue } 

It should be equal to:

let isMarked = record[y][x] !== 0 

isEqual

as mentioned in other comments

My version

Based on your version I wanted to create a revision. But because I thought the structure of your initial version wasn't right, it became a whole new version. It's more how I would have done it. I hope you learn from the different structure. (I now saw you have an update, which I didn't look at for this. It looks a lot cleaner, but I would still do the scoping/object-oriented refactor if you want to do it well.)

// @ts-check // NAMING // - cell: a space on the board // - mark: an x or an o // - winning line: a series of marks that causes a win // - direction: direction a line can be drawn: horizontal, vertical, top-left to bottom-right, bottom-right to top-left // - dimension: horizontal or vertical /** * @typedef { { cell_indices: tCellIndices, player_index: number } } tMark * @typedef { number } tPlayerIndex 0 or 1 (X or O) * @typedef { number[] } tCellIndices [y, x] y- and x-indexes of the cell in the board_grid (note: y first) * @typedef { tCellIndices[] } tLineIndices [start_cell_indices, end_cell_indices] * @typedef { (tPlayerIndex|undefined)[][] } tBoardGrid * An array of arrays in order to access cell content by board_grid[y][x]. * Cell contents is undefined for an empty cell or the index of the player for a cell with a mark. */ // I split up the code in two objects: GamePlay and View // - GamePlay is the logic // - View is the UI // I create a Game Play object in a immediately-invoked function expression in order to limit the scope of variables. // There is only 1 GamePlay object during the use of the website. // The GamePlay object contains the properties/methods that are not related to a specific game object. // The GamePlay object contains a game property, which is the game being played. // During the use of the website multiple games can be played and multiple game objects can be created. const GamePlay = (function create_game_play() { /** The number of marks required to win. */ const MARKS_IN_WINNING_LINE_COUNT = 5 const ROW_COUNT = 15 const COLUMN_COUNT = 15 const CELL_COUNT = ROW_COUNT * COLUMN_COUNT // separate function for typing of ReturnType function create_game() { /** @type {tMark[]} */ let marks = []; /** @type {tBoardGrid} */ let board_grid = [] for (let i = 0; i < COLUMN_COUNT; ++i) { let column = new Array(ROW_COUNT) column.fill(undefined) board_grid.push(column) } /** @param {number[]} cell_indices */ function set_mark(cell_indices) { if (!can_set_mark(cell_indices)) return let player_played_index = game.player_on_turn_index let mark = { cell_indices: cell_indices, player_index: player_played_index, } marks.push(mark) board_grid[cell_indices[0]][cell_indices[1]] = player_played_index let winning_lines = check_victory(mark) let has_won = winning_lines.length > 0 if (has_won) { game.player_won_index = player_played_index } else { game.player_on_turn_index = (player_played_index + 1) % 2 } // Call the view with parts to render. // I pass the game object as well in order to prevent need for accessing the static GamePlay.game variable. View.render_set_mark(game, mark, player_played_index, winning_lines) // Some logic I put inside a separate function inside the set_mark function: // - can_set_mark // - check_victory // Check whether a move is permitted. function can_set_mark(cell_indices) { let is_game_over = game.player_won_index !== undefined if (is_game_over) return false let mark = game.board_grid[cell_indices[0]][cell_indices[1]] let is_cell_unmarked = mark === undefined return is_cell_unmarked } /** * Verify whether the move results in winning lines * @param {tMark} mark * @returns {tLineIndices[]} the lines created by the move. Empty array if no winning move. */ // I changed the logic in order to verify order only the lines that are made by the mark. // One reason was that after you find a winning line, any move would result in a winning line, because 1 was already found. // This code also allows you to discover multiple lines at once and find lines with more than 5 marks. function check_victory(mark) { /** * @param {number[]} direction_step * @returns {tLineIndices|undefined} * Verifies whether the mark is within a winning line for a direction. */ function check_victory_in_direction(direction_step) { /** start and end cell indices */ let start_and_end_cell_indices = [] /** number of marks in the line */ let marks_count = 1 // 1 = this mark for (let backward_or_forward_index = 0; backward_or_forward_index < 2; ++backward_or_forward_index) { // in the first iteration, go in backward direction starting from the mark and find the start of the line: -1 // second iteration, go in forward direction starting from the mark and find the end of the line: 1 let backward_or_forward = backward_or_forward_index === 0 ? -1 : 1 // start or end cell depending on the direction of let start_or_end_cell_indices = mark.cell_indices outer: while (true) { let start_or_end_cell_indices_next = [] for (let i = 0; i < start_or_end_cell_indices.length; ++i) { let direction_dimension_step = backward_or_forward * direction_step[i] let a = start_or_end_cell_indices[i] + direction_dimension_step if (a < 0) break outer start_or_end_cell_indices_next[i] = a } let mark_player_index = game.board_grid[start_or_end_cell_indices_next[0]][start_or_end_cell_indices_next[1]] if (mark_player_index !== mark.player_index) break start_or_end_cell_indices = start_or_end_cell_indices_next ++marks_count } start_and_end_cell_indices.push(start_or_end_cell_indices) } if (marks_count >= MARKS_IN_WINNING_LINE_COUNT) { return start_and_end_cell_indices } } // There are 4 possible directions to form a winning line. let POSSIBLE_WINNING_LINE_DIRECTIONS = [ // notation: [y, x] // [step size in vertical dimension, step size in horizontal dimension] [0, 1], // horizontal [1, 0], // vertical [1, 1], // diagonal top-left to bottom-right [1, -1], // diagonal bottom-left to top-right ] let winning_lines = [] for (let line_direction of POSSIBLE_WINNING_LINE_DIRECTIONS) { let winning_line = check_victory_in_direction(line_direction) if (winning_line) { winning_lines.push(winning_line) } } return winning_lines } } let game = { /** @type {tMark[]} array of marks set in the game (by any of both players) */ marks: marks, /** @type {tBoardGrid} cells by y/column index and then by x/row index */ board_grid: board_grid, player_on_turn_index: 0, /** @type {number|undefined} */ player_won_index: undefined, /** @param {number[]} coords */ set_mark(coords) { set_mark(coords) }, } return game } function start_game() { let game = create_game() game_play.game = game View.render_start_game(game) } let game = create_game() var game_play = { get column_count() { return COLUMN_COUNT }, get row_count() { return ROW_COUNT }, get size() { return [ROW_COUNT, COLUMN_COUNT] }, get cell_count() { return CELL_COUNT }, /** @type {ReturnType<typeof create_game>} game */ game: game, start_game() { start_game() }, } return game_play })(); /** * @typedef {(typeof GamePlay.game)&{}} tGame */ const View = (function create_view() { const CELL_SIZE = 50 // Variables initialized with immediately invoked function expression, in order to get typings right let [el_canvas, ctx, array_el_game_state, el_button_restart] = (function initialize() { const el_canvas = document.getElementById('mycanvas'); if (!el_canvas) throw new Error('no element mycanvas'); if (!(el_canvas instanceof HTMLCanvasElement)) throw new Error('my canvas is not a canvas'); const ctx = el_canvas.getContext("2d"); if (!ctx) throw new Error('canvas has no 2d context') let array_el_game_state = ['el_game_state_1', 'el_game_state_2'].map(id => { let el_game_state = document.getElementById(id) if (!el_game_state) throw new Error('no element ' + id) if (!(el_game_state instanceof HTMLElement)) throw new Error(`element ${id} is not a HTML element`) return el_game_state }) let el_button_restart = document.getElementById('el_button_restart') if (!el_button_restart) throw new Error('no element el_button_restart') if (!(el_button_restart instanceof HTMLButtonElement)) throw new Error() return [el_canvas, ctx, array_el_game_state, el_button_restart] })(); // Initialize canvas en canvas context el_canvas.width = GamePlay.column_count * CELL_SIZE el_canvas.height = GamePlay.row_count * CELL_SIZE // these properties only work when set after el_canvas.width and height ctx.font = "40px Verdana"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; // The next two functions can be called from the game logic object. // I let their names start with render_ /** * @param {tGame} game */ function render_start_game(game) { //game State elements array_el_game_state[0].textContent = "Player 1 made no moves."; array_el_game_state[1].textContent = "Player 2 made no moves."; draw_board_background() for (let y = 0; y < game.board_grid.length; ++y) { for (let x = 0; x < game.board_grid.length; ++x) { draw_cell({ cell_indices: [y, x] }) } } } /** * @param {tGame} game * @param {tMark} mark * @param {tPlayerIndex} player_played_turn_index * @param {tLineIndices[]} winning_lines */ function render_set_mark(game, mark, player_played_turn_index, winning_lines) { draw_mark(mark) draw_winning_lines(player_played_turn_index, winning_lines) draw_game_state_text(game, player_played_turn_index, winning_lines.length > 0) } // The next methods are helper methods of the render methods. // I let theire names start with draw_. const MARK_COLORS_BY_PLAYER_INDEX = ['blue', 'red'] const MARKS_BY_PLAYER_INDEX = ['X', 'O'] /** * @param {{ cell_indices: number[] }} mark */ function draw_cell(mark) { let [y, x] = mark.cell_indices.map(a => a * CELL_SIZE); /* ctx.shadowBlur = 0 ctx.shadowOffsetX = 0 ctx.shadowOffsetY = 0 ctx.fillStyle = 'blue' ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE); */ ctx.fillStyle = 'teal'; ctx.shadowBlur = 4; ctx.shadowColor = '#000'; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 2; ctx.fillRect(x + 5, y + 5, CELL_SIZE - 10, CELL_SIZE - 10); } /** * Draw a mark. * @param {tMark} mark */ function draw_mark(mark) { let [y, x] = mark.cell_indices.map(a => a * CELL_SIZE); let mark_sign = MARKS_BY_PLAYER_INDEX[mark.player_index] ctx.fillStyle = MARK_COLORS_BY_PLAYER_INDEX[mark.player_index]; ctx.fillText(mark_sign, x + CELL_SIZE * 0.5, y + CELL_SIZE * 0.5); } /** * Draws a new board. Overpaints the old one. */ function draw_board_background() { let size = GamePlay.size.map(a => a * CELL_SIZE) let [width, height] = size // Draw background ctx.beginPath(); ctx.rect(0, 0, width, height); ctx.fillStyle = "rgba(0,122,0, 0.2)"; ctx.strokeStyle = "fuchsia"; ctx.fill(); // Draw grid /* ctx.beginPath(); for (let i = 0; i <= GamePlay.column_count; i++) { ctx.moveTo(i * CELL_SIZE, 0); ctx.lineTo(i * CELL_SIZE, height); } for (let i = 0; i <= GamePlay.row_count; ++i) { ctx.moveTo(0, i * CELL_SIZE); ctx.lineTo(width, i * CELL_SIZE); } ctx.stroke(); */ } /** * Draw the lines through the winning marks. * @param {tPlayerIndex} player_played_turn_index * @param {tLineIndices[]} winning_lines_start_and_end_cell_indices */ function draw_winning_lines(player_played_turn_index, winning_lines_start_and_end_cell_indices) { if (winning_lines_start_and_end_cell_indices.length === 0) return for (let winning_line_start_and_end_cell_indices of winning_lines_start_and_end_cell_indices) { let [start_cell_index, end_cell_index] = winning_line_start_and_end_cell_indices ctx.beginPath(); ctx.moveTo(start_cell_index[1] * CELL_SIZE + CELL_SIZE / 2, start_cell_index[0] * CELL_SIZE + CELL_SIZE / 2); ctx.lineTo((end_cell_index[1] + 1) * CELL_SIZE - CELL_SIZE / 2, (end_cell_index[0] + 1) * CELL_SIZE - CELL_SIZE / 2); let color = MARK_COLORS_BY_PLAYER_INDEX[player_played_turn_index]; ctx.strokeStyle = color; ctx.lineWidth = 11; ctx.stroke(); ctx.closePath(); } } /** * Update the two game state labels above the board. * @param {tGame} game * @param {tPlayerIndex} player_played_turn_index * @param {boolean} is_victory */ function draw_game_state_text(game, player_played_turn_index, is_victory) { let player_number = player_played_turn_index + 1 let moves_count = game.marks.filter(x => x.player_index === player_played_turn_index).length let text if (is_victory) { text = `Player ${player_number} WON in ${moves_count} moves!` } else { text = `Player ${player_number} made ${moves_count} moves.` } array_el_game_state[player_played_turn_index].textContent = text } // Adding event listeners el_canvas.addEventListener("mousedown", function (e) { const x = Math.floor(e.offsetX / CELL_SIZE); /* 0 - 3 on board cell */ const y = Math.floor(e.offsetY / CELL_SIZE); /* 0 - 3 on board cell */ GamePlay.game.set_mark([y, x]) }); el_button_restart.addEventListener('click', function (ev) { GamePlay.start_game() }) return { render_start_game: render_start_game, render_set_mark: render_set_mark, } })(); GamePlay.start_game()
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Gomoku game 5 in a row</title> <style> body { text-align: center; } #canvas { border: 2px solid green; background-color: lightblue; } </style> </head> <body> <h1>Gomoku game 5 in a row. Javascript simple game Canvas.</h1> <p id="el_game_state_1"></p> <p id="el_game_state_2"></p> <canvas id="mycanvas"></canvas> <div> <button id="el_button_restart" type="button">ReStart</button> </div> <script src="behavior.js"></script> </body> </html>

\$\endgroup\$
0
    5
    \$\begingroup\$
    function isEqual(a1, a2, a3, a4, a5) { if (a1==a2 && a2==a3 && a3==a4 && a4==a5) { return true; } else { return false; } } 

    With 5 elements, this is already quite verbose, but now imagine checking 20 elements or 1,000. It's better to use Array.every to check all elements for a condition. Here you could check if every element equals the first element.

    It's better to test for strict equality (===) to avoid implicit type conversion. Note that 10 == '10' returns true, but 10 === '10' returns false.

    Returning true and false is redundant. You can simply return the condition itself.

    So the function can be reduced to this:

    function isEqual(arr) { return arr.every(val => val === arr[0]); } 

    And now that the function accepts an array instead of individual values, you can also avoid extracting a1, ..., a5 separately and simply pass an Array.slice:

    if (isEqual(record[i].slice(j, j+5))) { ... } 

    And for the vertical check, use Array.map to get the \$j\$th element of each slice:

    if (isEqual(record.slice(i, i+5).map(arr => arr[j]))) { ... } 
    \$\endgroup\$
    2
    • 1
      \$\begingroup\$Thanks for the advice. In the future I will try to use these tips when building my algorithm for any game.\$\endgroup\$CommentedSep 2, 2024 at 9:33
    • 1
      \$\begingroup\$Many thanks to tdy for the tip. Everything worked. I spent two days trying to figure out this problem. Thank you. Now the comparison condition is also satisfied (checked) vertically.\$\endgroup\$CommentedSep 3, 2024 at 18:12
    2
    \$\begingroup\$

    The accepted review has a few useful tips but is a little bit short IMO.

    General

    You used functional programming to tackle this problem. Which is doable but creates a mess real quick! Consider to continue improve your skills and turn this into a OOP project!

    Consistency

    Consistency is key and with programming it's very important. This makes it easier for yourself and others to read your code.

    Functions should always start with non capital. Probably a small oversight as you did do this for other functions:

    ReStart() 

    Structure

    Not to be mean but your structure is an absolute mess. Calling functions, making loops, other functions,... It's all mixed up in eachother.

    One example is that you have this line in both initBoard() as on top:

    var cellCol = PLAY_COLOR; 

    This confusion thus happend to you which is an easy oversight in your code. Try to combine all initialization of your board into that function. Such as this line seems like logic for your board:

    for(var i = 0; i<num; i++){ var arr = []; for(var j = 0; j < num; j++){ arr.push(0); } record.push(arr); } 

    Improvements

    1. You use const and var, which is better than only using var but consider to replace these vars with lets.

    Additional information

    1. Learn how to work with literals, it's just a more readable syntax to combine strings and variables. E.g:

      gameStateEl.textContent = `Player_1 - ${count_1} moves.`; 
    \$\endgroup\$
    0
      1
      \$\begingroup\$

      Made changes according to tdy's advice and Wimanicesir's advice. Fixed errors. The code works as expected. Equality of elements is determined horizontally, vertically and diagonally. Edited the code according to the generally accepted design style.

      <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Gomoku game 5 in a row</title> <style> body { text-align: center; } #canvas { border: 2px solid green; background-color: lightblue; } </style> </head> <body> <h1>Gomoku game 5 in a row. Javascript simple game Canvas.</h1> <p id="gameStateEl"></p> <p id="gameStateE2"></p> <canvas id="mycanvas"></canvas> <div> <button id="reStart"type="button" onclick="reStart();">reStart</button> </div> <script> const canvas = document.getElementById('mycanvas'); const ctx = canvas.getContext("2d"); const CELL_SIZE = 50; const ROWS = 15, COLS = 15; const CELL_COUNT = ROWS * COLS; let cellCol = "teal"; //Positions on the board const boardPosition = {x: 0, y: 0, w: COLS * CELL_SIZE, h: ROWS * CELL_SIZE}; canvas.width = boardPosition.x + boardPosition.w; canvas.height = boardPosition.y + boardPosition.h; let isWin = false; let isChangePlayer = false; let count_1 = 0; let count_2 = 0; let count_3 = 0; //Determine whether the current click is X or O let coords = []; //The initBoard method initializes the game board by //setting the height and width of the board, //creating an array to represent the board, and filling the array with zeros. function initBoard() { const cells = []; let num = ROWS + 5; //Determine whether the current click is X or O let coords = []; //Save the clicked coordinates record = []; let arr = []; let first_X = 0; let first_Y = 0; let last_X = 0; let last_Y = 0; let cellIdx = 0; cellCol = "teal"; gameStateEl.textContent = `Player_1 - ${count_1} moves.`; gameStateE2.textContent = `Player_2 - ${count_2} moves.`; //Define the unplaced state of the chessboard as 0 for(let i = 0; i<num; i++){ let arr = []; for(let j = 0; j < num; j++){ arr.push(0); } record.push(arr); } } //Drawing board background function drawBoardBackground() { ctx.beginPath(); //Nicknames for ease of reading const bP = boardPosition; /* BM67 local alias */ ctx.rect(bP.x, bP.y, bP.w, bP.h); ctx.fillStyle = "rgba(0,122,0, 0.2)"; ctx.strokeStyle = "fuchsia"; ctx.fill(); ctx.beginPath(); for (let i = 0; i <= COLS; i++) { ctx.moveTo(bP.x, bP.y + i * CELL_SIZE); ctx.lineTo(bP.x + bP.w, bP.y + i * CELL_SIZE); ctx.moveTo(bP.x + i * CELL_SIZE, bP.y); ctx.lineTo(bP.x + i * CELL_SIZE, bP.y + bP.h); } ctx.stroke(); } function setFont() { ctx.font = "60px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; } //draw a cell function drawCell(cellIdx) { let val = ""; const x = (cellIdx % COLS) * CELL_SIZE; const y = (cellIdx / COLS | 0) * CELL_SIZE; ctx.fillStyle = "blue"; ctx.shadowColor = '#000'; ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE); ctx.fillStyle = cellCol; ctx.shadowBlur = 4; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 2; ctx.fillRect(x + 5, y + 5, CELL_SIZE - 10, CELL_SIZE - 10); ctx.fillText(val, x + CELL_SIZE * 0.5, y + CELL_SIZE * 0.5); } //let's draw a game function drawGame() { setFont(); let cellIdx = 0; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawBoardBackground(); while (cellIdx < CELL_COUNT) { drawCell(cellIdx ++); } } //Draw a X function draw_X(x, y){ editRecord(x, y, 1); ctx.fillStyle = "blue"; ctx.beginPath(); ctx.font = "40px Verdana"; ctx.fillText("X", x * CELL_SIZE + 25, y * CELL_SIZE + 27); ctx.closePath(); ctx.fill(); } //Draw a O function draw_O(x, y){ editRecord(x, y, 2); ctx.fillStyle = "red"; ctx.beginPath(); ctx.font = "40px Verdana"; ctx.fillText("O", x * CELL_SIZE + 25, y * CELL_SIZE + 27); ctx.closePath(); ctx.fill(); } //Let's draw the winner's line. function drawLine(x, y){ editRecord(x, y, 1); ctx.beginPath(); ctx.moveTo(first_X * CELL_SIZE + 25, first_Y * CELL_SIZE + 25); ctx.lineTo(last_X * CELL_SIZE + 25, last_Y * CELL_SIZE + 25); if(isChangePlayer === true){ ctx.strokeStyle = "blue"; } else{ ctx.strokeStyle = "red"; } ctx.lineWidth = 11; ctx.stroke(); ctx.closePath(); } //Add mark to clicked function addCoords(x, y, num){ coords.push({ x: x, y: y, num: num }) } //Edit the marks on the board, //1 represents the red move, 2 represents the black move function editRecord(x, y, num){ record.forEach(function(item, index){ if(index === y){ item.forEach(function(item, index){ if(x === index){ record[y][x] = num; } }) } }) } //Determine whether the current position is placed function verdictCoords(x, y){ let isTrue = true; coords.forEach(function(item, index){ if(item.x === x && item.y === y){ isTrue = false; } }) return isTrue } //Determine whether the adjacent 5 numbers are equal, //if they are equal, it means that the winner has been divided function isEqual(arr) { return arr.every(val => val === arr[0]); } //Determine whether the mark on the board //meets the victory condition function isVictory(){ let len = record.length; for(let i = 0; i < record.length; i++){ for(let j=0; j<record[i].length; j++){ if(record[i][j] !==0 ){ if(j < len && i - 1 < len){ //Determine the horizontal direction if (isEqual(record[i].slice(j, j+5))){ first_X = j; first_Y = i; last_X = j + 4; last_Y = i; isWin = true; } //Judging the vertical direction if (isEqual(record.slice(i, i+5).map(arr => arr[j]))) { first_X = j; first_Y = i; last_X = j; last_Y = i + 4; isWin = true; } // Diagonal rating: left to right (top to bottom) if (i + 5 <= len && j + 5 <= len && isEqual(record.slice(i, i + 5).map((arr, idx) => arr[j + idx]))) { first_X = j; first_Y = i; last_X = j + 4; last_Y = i + 4; isWin = true; } // Diagonal rating: right to left (top to bottom) if (i + 5 <= len && j - 4 >= 0 && isEqual(record.slice(i, i + 5).map((arr, idx) => arr[j - idx]))) { first_X = j; first_Y = i; last_X = j - 4; last_Y = i + 4; isWin = true; } } } } } return isWin; } //Cell pressed function cellClicked(clickIdx) { /* BM67 returns true if board needs redraw */ const x = clickIdx % COLS; const y = clickIdx / COLS | 0; if(coords.length === 0){ draw_X(x, y ,ctx); isChangePlayer = true; addCoords(x, y, 1); }else{ if(verdictCoords(x, y)){ if(!isChangePlayer){ draw_X(x, y); addCoords(x, y, 1); isChangePlayer = true; }else { draw_O(x, y); addCoords(x, y, 2); isChangePlayer = false; } if(isVictory() && isChangePlayer === true){ drawLine(x, y); my_color = "blue"; gameStateEl.textContent = `Player_1 Winner blue - ${count_1} moves.`; } if(isVictory() && isChangePlayer === false){ drawLine(x, y); my_color = "red"; gameStateE2.textContent = `Player_2 Winner red - ${count_2} moves.`; } } } } //Adding an onClick handler to a canvas element canvas.addEventListener("mousedown", function (e) { //Nicknames for ease of reading const bP = boardPosition; /* BM67 local alias */ const x = Math.floor((e.offsetX - bP.x) / CELL_SIZE); /* 0 - 3 on board cell */ const y = Math.floor((e.offsetY - bP.y) / CELL_SIZE); /* 0 - 3 on board cell */ if(isChangePlayer === false){ count_1 += 1; gameStateEl.textContent = `Player_1 - ${count_1} moves.`; } if(isChangePlayer === true){ count_2 += 1; gameStateE2.textContent = `Player_2 - ${count_2} moves.`; } if (cellClicked(x + y * COLS)) { moveCount += 1; drawGame(); } if (isVictory() === true) { count_3 += 1; } if (count_3 === 2) { reStart(); } }); function reStart() { document.location.reload(); } initBoard(); drawGame(); isVictory(); </script> </body> </html> 
      \$\endgroup\$
        0
        \$\begingroup\$

        On the advice of TDI. I removed function isEqual(a1, a2, a3, a4, a5) from my code. Added function isEqual(arr). Set the condition using the slice() method.

        function isVictory(){ var len = record.length; for(var i = 0; i < record.length; i++){ for(var j=0; j<record[i].length; j++){ if(record[i][j] !==0 ){ if(j < len && i - 1 < len){ //Determine the horizontal direction if (isEqual(record[i].slice(j, j+5))){ isWin = true; } //Judging the vertical direction if (isEqual(record.slice(i, i+5))) { isWin = true; } } } } } return isWin; } 

        I have the victory condition defined horizontally. But for some reason the victory condition is not defined vertically?

        \$\endgroup\$
        2
        • 1
          \$\begingroup\$For vertical, you need to map the \$j\$th element of each slice: isEqual(record.slice(i, i+5).map(arr => arr[j])) (updated my answer)\$\endgroup\$
          – tdy
          CommentedSep 3, 2024 at 17:17
        • \$\begingroup\$Many thanks to tdy for the tip. Everything worked. I spent two days trying to figure out this problem. Thank you. Now the comparison condition is also satisfied (checked) vertically.\$\endgroup\$CommentedSep 3, 2024 at 18:12

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.