4
\$\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
    Commented Sep 1 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
    Commented Sep 1 at 5:59
  • \$\begingroup\$ Thanks for the advice. In the future I will try to format my code more correctly. \$\endgroup\$ Commented Sep 2 at 9:30

3 Answers 3

4
\$\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\$ Commented Sep 2 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\$ Commented Sep 3 at 18:12
1
\$\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\$
1
  • \$\begingroup\$ Thank you, good advice. I will try to apply them in the future. \$\endgroup\$ Commented Sep 4 at 17:53
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\$
3
  • 1
    \$\begingroup\$ If this code does not work as you expect, you should post a new question on stackoverflow.com \$\endgroup\$
    – toolic
    Commented Sep 3 at 14:32
  • 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
    Commented Sep 3 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\$ Commented Sep 3 at 18:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.