앞서 만들었던 오목에 자신감을 얻고 다음 프로젝트로 리버시를 만들기로 한다. 

 

Javascript 오목 게임 만들기

오목 게임. 아이들이랑 놀 겸해서 만들어봤다. 완성본은 아래에서... omog-geim.bryanko555.repl.co HTML,CSS는 별거 없고, 게임승리 이미지를 안보이게 넣어뒀다가 오목이 만들어졌을 때 나타나게 했다. 큰

mrkool.tistory.com

리버시 게임은, 돌을 놓아서 자기편의 돌 사이에 상대편의 돌이 끼어 있는 형태를 만들게 되면 따먹을 수 있는데, 단, 따먹은 돌은 없어지는 게 아니라 자기편의 돌로 변한 채로 그 자리에 있게 된다. 즉, 상대편의 돌을 뒤집어 자기편의 돌로 만드는 것이다.

완성된 게임은 아래에서...

https://sad-jennings-0ef849.netlify.app/

 

HTML,CSS는 별거 없고, 게임승리 이미지를 안보이게 넣어뒀다가 오목이 만들어졌을 때 나타나게 했다.

이번에는 P5.js를 사용했기 때문에, P5.js파일을 읽어와야 된다.

 

처음 시작할 때는 오목보다 쉽게 만들 수 있을 줄 알았다. 그런데, 두 배는 더 어렵게, 시간을 더 들여서 만들었다.

오목에서보다 더 고려해야 할 점이,

] 돌을 놓을 수 있는 곳이 제한되어 있다. 오목처럼 아무곳에나 둘 수 있는 것이 아니어서, 둘 수 있는 칸을 매번 계산해서 보여줘야 된다. (검정색,하얀색 조그만 사각형)

] 게임 막판 즈음에는 돌을 놓을 수 없는 경우가 발생하고, 그렇게되면 1차는 상대방에게 순서를 넘겨야되고, 그 다음에도 놓을 곳이 없으면 게임 종료 시킨다.

] 오목은 돌의 색깔이 정해지면 계속 그대로이지만, 리버시는 매번 색깔이 바뀐다.

] 마지막에 자기 돌의 수가 많은 쪽이 이기는 게임이라, 돌의 수를 매번 세어서 보여줘야 한다.

 

오목에서처럼 각 칸을 숫자의 배열로 구성시키고, 거기서 연산을 해나간다. 

아래와 같이 빈 칸은 -1, 흰색 돌 0, 검정색 돌 1, 흰색이 둘 수 있는 곳 2, 검정색이 둘 수 있는 곳 3으로 숫자 배열을 계속 바꿔나가고, 그 배열에 맞춰 매 번 캔버스에 그림을 그려준다.

돌을 놓을 수 있는 곳은, 돌을 잡을 수 있는 곳에만 놓을 수 있는데, 이 경우를 찾는 곳에서 가장 애를 먹었던 것 같다.

매 번 각각의 칸에서 8방향을 모두 체크하고, 나와 다른 색깔의 돌이 있으면, 그 방향으로 다음 돌을 확인해나가다가, 그 끝에 나와 동일한 색의 돌이 있는 경우가 돌을 놓을 수 있는 경우다. 

 

돌이 놓아지는 경우에도 그 돌의 8방향을 모두 확인해서 잡은 돌의 색을 바꿔준다. (배열의 숫자 0을 1로, 아니면 1을 0으로...)

 

아래에 Javascript 파일 전문.  P5.js를 사용했지만, 그림 그리는 것만 좀 편해진 것이고, javascript의 기본 원리는 동일하다.

 

 

모던 자바스크립트 Deep Dive:자바스크립트의 기본 개념과 동작 원리

COUPANG

www.coupang.com

function setup(){
  canvasWidth = 600;
  canvasHeight = 600;
  cvs= createCanvas(canvasWidth,canvasHeight);
  background('#1b5e20');
    
  row = 8;
  rowWidth = canvasWidth / row;
  radius = rowWidth-20;
  count = 1// 돌을 놓는 차례. 1을 검정색으로 설정
  blackScore = whiteScore = 2
  blackAvailable = whiteAvailable = 2
  pass = 0;
  checkDirection = [[1-1], [10], [11], [01], [-11], [-10], [-1-1], [0-1],];
  board = new Array();
  buffer = new Array();
  boardHistory = new Array();
  tik = new Audio("tik.mp3");
  beep = new Audio("beep.wav");
  ending = new Audio("ending.wav");
  plz = new Audio("plz.m4a");
  oneMore = new Audio("oneMore.m4a");
 
  winScreen = document.querySelector('.winShow');
  turnPass = document.querySelector(".turnPass");
  // 보드 격자를 그리고, 모든 칸에 -1을 입력
  boardInit();
 
  // 시작 돌 4개 
  board[3][3= board[4][4= 1;
  board[4][3= board[3][4= 0;
  
  checkAvailable();
  drawBoard();
  
  document.querySelector("#black").innerHTML = "검정색 점수 : " + blackScore;
  document.querySelector("#white").innerHTML = "하얀색 점수 : " + whiteScore;
  button1 = document.querySelector("#withdraw");
  button2 = document.querySelector("#reload");
  troImg = document.querySelector("#trophyImg");
  
  // 무르기 버튼 동작
  button1.addEventListener('mouseup', ()=>{
    plz.currentTime = 0.5;
    plz.play();
    withdraw();
  })
  button2.addEventListener('mouseup', ()=>{
    oneMore.currentTime = 0.5;
    oneMore.play();
    setTimeout(() => {
      location.reload();
    }, 2000);
  })
 
  canvasPosition = cvs.position(0,30);
}
 
function boardInit(){
  // 보드 격자를 그리고, 모든 칸에 -1을 입력
  stroke("black");
  strokeWeight(2);
  fill('#1b5e20');
  for (i = 0; i < 8; i++) {
    board[i] = new Array(8);
    for (j = 0; j < 8; j++) {
      rect(i * rowWidth, j * rowWidth, rowWidth, rowWidth);
      board[i][j] = -1;
    }
  }
  console.log('-1 초기화')
   fill("black");
   circle(rowWidth * 2, rowWidth * 210);
   circle(rowWidth * 2, rowWidth * 610);
   circle(rowWidth * 6, rowWidth * 210);
   circle(rowWidth * 6, rowWidth * 610);
}
 
// 마우스 클릭하면 그 칸의 위치 파악
document.addEventListener("mouseup", (e) => {
  // console.log('offset' , e.offsetX, e.offsetY);
  // console.log('page' , e.pageX, e.pageY);
  
  if(e.offsetX > 0 && e.offsetX < canvasWidth && e.pageY > canvasPosition.y && e.offsetY > 0 &&  e.offsetY < canvasHeight){
    
  let rowX = floor(e.offsetX / rowWidth);
  let rowY = floor(e.offsetY / rowWidth);
  
  main(rowX, rowY);
  }
});
 
// 객체 복사하는데, 이전 값을 계속 참조하는 것을 막는 함수
function cloneObj(obj){
  const result = [];
  // console.log(obj)
  for( i = 0 ; i< 8 ;i++ ){
    result[i] = JSON.parse(JSON.stringify(obj[i]));
  }
  return result;
}
// 메인 함수. 클릭된 칸에 흰색(0), 검은색(1) 입력하고, 
// 뒤집어야 하는 함수 호출, 보드 전체 그리는 함수 호출
function main(x, y) {
 
  // 돌을 놓을 수 있는 곳일때만 주요 함수들 실행
  if (board[y][x] == 2 || board[y][x] == 3) {
    tik.currentTime = 0.5;
    tik.play();
    board[y][x] = count % 2;
    // 돌을 놓을 수 있는 위치에 3(검정)이나 2(흰색)로 보드에 입력하는 함수 호출
    checkReversible(x, y);
    count++;
    // 돌을 뒤집어야 하는 경우에, 바뀐 색으로 보드에 입력
    checkAvailable();
    let boardTemp = board;
    // board배열상태로 보드를 그려줌 
    drawBoard();
    // 검정,하얀 돌  개수 계산
    scoreCalc();
    // 무르기를 위해서 매 차례의 보드 배열을 통째로 boardHistory배열에 입력
     boardHistory.push(cloneObj(board));
 
 
 
    // console.log('boardHistory', boardHistory);
  } else {
    beep.play();
    console.log('Not Here')
  }
}
 
// 무르기 함수
function withdraw(){
  boardInit();
  boardHistory.pop();
  board = cloneObj(boardHistory.slice(-1)[0]);
  // board = boardHistory.slice(-1)[0];
  console.log(boardHistory);
  count--;
  drawBoard();
  scoreCalc();
}
 
// 점수 계산 함수
// 점수 계산 및 돌을 둘 수 있는 곳이 몇 개 있는지도 확인
// 만약 돌을 둘 수 있는 곳이 없으면, 나중에 패스하도록
function scoreCalc(){
  blackScore = whiteScore = 0;
  blackAvailable = whiteAvailable = 0
  for(i=0;i<8;i++){
    blackScore += board[i].filter((e) => e === 1).length;
    whiteScore += board[i].filter((e) => e === 0).length;
    blackAvailable += board[i].filter((e) => e === 3).length;
    whiteAvailable += board[i].filter((e) => e === 2).length;
  }
  document.querySelector("#black").innerHTML = "검정색 점수 : " + blackScore;
  document.querySelector("#white").innerHTML = "하얀색 점수 : " + whiteScore;
  
  if (count % 2 == 0 && whiteAvailable == 0 && pass != 2) {
    console.log("no white move");
    pass++;
  } 
  if (count % 2 == 1 && blackAvailable == 0 && pass != 2) {
    console.log("no black move");
    pass++;
  }
 
  // 놓을 곳이 없으면 상대방에게 차례 넘김
  if(pass == 1){
    console.log("다음 차례로 넘어갑니다.");
    turnPass.style.zIndex = 3;
    turnPass.style.visibility = "visible";
    turnPass.style.animationName = "moveDown";
    console.log(turnPass);
    setTimeout(() => {  
      turnPass.style.zIndex = -1;
    }, 2500);    
    count++;
  }
 
  // 혹시 둘 곳이 없어서 다음 차례로 넘어간 경우 때문에
  // 가능한 곳을 다시 찾고, 다시 그려주는 함수 호출
   pass++;
   checkAvailable();
   drawBoard();
 }
 
// 배열 상태대로 보드 전체에 돌 및 놓을 수 있는 위치를 그려줌
function drawBoard(){
   noStroke();
  
  // board배열에 따라 흰색,검정색,작은 사각형 그려주고
  for (i = 0; i < 8; i++) {
    for (j = 0; j < 8; j++) {
      switch (board[i][j]) {
        case 0:
          fill("white");
          circle((j+1)*rowWidth - floor(rowWidth/2), (i+1)*rowWidth - floor(rowWidth/2), radius);
          break;
        case 1:
          fill("black");
          circle((j+1)*rowWidth - floor(rowWidth/2), (i+1)*rowWidth - floor(rowWidth/2), radius);
          break;
        case 2:
          fill("rgba(255,255,255,1)");
          rect((j+1)*rowWidth - floor(rowWidth/2)-4, (i+1)*rowWidth - floor(rowWidth/2)-410,  10  );
          break;
        case 3:
          fill("rgba(0,0,0,1)");
          rect((j+1)*rowWidth - floor(rowWidth/2)-4, (i+1)*rowWidth - floor(rowWidth/2)-410,  10  );
          break;
        default : 
          fill("#1b5e20");
          rect((j+1)*rowWidth - floor(rowWidth/2)-8, (i+1)*rowWidth - floor(rowWidth/2)-816,  16  );
          break;
        }
    }
  }
}
 
// 원을 뒤집어야 하는지 여부 확인해서 0(하얀색),1(검정색)을 좌표에 입력
function checkReversible(x,y){
  // 8방향으로 체크하는 루프
  for (j = 0; j < checkDirection.length; j++) {
    cX = x;
    cY = y;
    while (cX >= 0 && cY >= 0 && cX < 8 && cY < 8) {
      cX = cX + checkDirection[j][0];
      cY = cY + checkDirection[j][1];
      if (cX < 0 || cY < 0 || cX >= 8 || cY >= 8) {
        break;
      } else {
          if (board[cY][cX] == count%2 || board[cY][cX] == -1 || board[cY][cX]  == 2 || board[cY][cX] == 3) {
            buffer.push([cX, cY]);
            break;
          } else {
            buffer.push([cX, cY]);
          }
      }
    }
    // console.log(buffer)
    if (buffer.length >= 1) {
      if (
        count%2 == board[buffer[buffer.length - 1][1]][buffer[buffer.length - 1][0]]
      ) {
        // console.log('here', count % 2);
        for (i = 0; i < buffer.length; i++) {
          board[buffer[i][1]][buffer[i][0]] = count%2;
        }
      }
    }
    buffer = [];
  }
}
 
// 돌을 놓을 수 있는 위치 파악해서, 검정색 가능위치(3), 하얀색 가능위치(2)로 board에 기록
function checkAvailable(){
  // 둘 곳 없어서 패스된게 두 번이 되면 게임 종료
  if(pass>1){
    console.log('game over')
    turnPass.style.visibility="hidden";
    gameOver();
  }
 
  // 이전 차례에서 2,3이었던 값을 -1로 초기화하는 함수 호출
  resetAvailable();
 
  boardCopy = board;
  // console.log(boardCopy)
  for (i=0;i<checkDirection.length;i++){
    for(k=0;k<checkDirection.length;k++){
      tX = i;
      tY = k;
  
  for (j = 0; j < checkDirection.length; j++) {
    // 검사할 좌표들을 aX,aY로 설정
    aX = tX;
    aY = tY;
    // 8방향으로 검사하는 루프
    while (aX >= 0 && aY >= 0 && aX < 8 && aY < 8) {
    aX += checkDirection[j][0];
    aY += checkDirection[j][1];
    // 검사 영역이 8x8 판 밖으로 나가면 멈추고,
    if (aX < 0 || aY < 0 || aX >= 8 || aY >= 8) {
      break;
    } 
    // 판 안에서 검사하는 칸들을 배열에 넣는다.
    else {
      buffer.push([aX, aY, boardCopy[aY][aX]]);
      // 검사한 칸의 색이 이번 차례의 색과 같거나, 빈 칸이면 배열에 넣기를 멈춘다.
      // 아니라면 검사한 칸의 값을 계속 배열에 넣는 루프를 반복한다.
      if (
        boardCopy[aY][aX] === count % 2 ||
        boardCopy[aY][aX] === -1 ||
        boardCopy[aY][aX] === 2 ||
        boardCopy[aY][aX] === 3 ||
        boardCopy[tY][tX] !== -1
      ) {
        break;
      } 
    }
  }
 
  // 만약 돌들의 배열이 돌을 놓을 수 있는 조건이면 3(검은색)이나 2(흰색)을 입력한다 
  if (
    buffer.length > 1 &&
    boardCopy[buffer[0][1]][buffer[0][0]] != count % 2 &&
    boardCopy[buffer[buffer.length - 1][1]][buffer[buffer.length - 1][0]] === count%2
  ) {
    // console.log(count);
    switch (count % 2 ) {
      case 0:
        board[tY][tX] = 2;
        break;
      case 1:
        board[tY][tX] = 3;
        break;
    }
    pass = 0;
  } 
  buffer = [];
}
}
}
}
 
function resetAvailable(){
  for (i=0;i<row;i++){
    for(k=0;k<row;k++){
      if(board[i][k]==2 || board[i][k]== 3){
        board[i][k]= -1;
      }
    }
  }
};
 
// 승리 화면 표시
function gameOver(x) {
  ending.play();
  // 음악이 재생되도록 약간의 시차를 두고 화면 표시
  setTimeout(() => {
    winScreen.style.visibility = 'visible';
    winScreen.style.zIndex = 2;
    if(blackScore > whiteScore){
      document.querySelector(".comment").innerHTML = "<br>" + "검정색 승리"
    }
    if(blackScore < whiteScore){
      document.querySelector(".comment").innerHTML = "<br>" + "하얀색 승리";
    }
    if(blackScore == whiteScore){
      document.querySelector(".comment").innerHTML = "<br>" + "무승부";
    }
 
    console.log(winScreen)
    troImg.style.animationName = "trophy";
  }, 300);
  
   
}
cs

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받을 수 있습니다.

+ Recent posts