[로데시] 개발

[대학전쟁] 양면빙고 웹 개발 05 - 빙고 판단, player 2명 본문

개인 개발/대학전쟁

[대학전쟁] 양면빙고 웹 개발 05 - 빙고 판단, player 2명

로데시 2026. 1. 20. 22:27
반응형

 

 

 

 


 

0️⃣ 지난 번까지..

대학전쟁에 나온 '양면빙고 (double sided bingo)' 게임을 보고
 
이 게임을 웹으로 만들면 재밌겠다는 생각이 들어 양면빙고 웹 개발을 시작했다.
 

 
지난 글에서는 빙고판에서의 타일 뒤집기, 이동을 구현했다.
 
오늘은 이어서 빙고가 완성되면 게임을 중단하고 player가 행동하면 turn이 넘어가는 기능을 구현해보고자 한다.
 

 

 

 

 

1️⃣ 빙고 판단

📌 util.js 코드
 
export const getVisibleColor = (tile) => {
  const frontId = tile.isFlipped
    ? getPairedTileId(tile.id)
    : tile.id;

  return getTileColorById(frontId);
};

앞면의 색을 판단하는 getVisibleColor 함수를 util.js에 추가한다.
 
타일의 뒤집혀도 색을 판단할 수 있게 isFlipped와 getPairedTileId을 활용했다.
 

 

 
📌 App.js 코드
 
const checkBingo = (boardTiles, targetColor) => {
  const lines = [
  // 행 빙고
  [0,1,2,3], [4,5,6,7], [8,9,10,11], [12,13,14,15],
  // 열 빙고
  [0,4,8,12], [1,5,9,13], [2,6,10,14], [3,7,11,15],
  // 대각선 빙고
  [0,5,10,15], [3,6,9,12],
  ];

  return lines.some(line =>
    line.every(idx => {
      const tile = boardTiles[idx];
      if (!tile) return false;

      return getVisibleColor(tile) === targetColor;
    })
  );
};

빙고를 판단하는 checkBingo(boardTiles, targetColor) 함수를 추가한다.
 
타일 앞면의 색이(getVisibleColor) 나의 main color(targetColor)와 같으면 빙고가 완성되었다고 판단한다.
 

 
const [winner, setWinner] = useState(null); // 승자
const COLORS = ["red", "blue", "yellow"]; // random으로 배정될 main color
const shuffled = COLORS.sort(() => 0.5 - Math.random()); // 색 random
const [myPlayer] = useState({
    id: "me",
    mainColor: shuffled[0],
  });

  const [opponent] = useState({
    id: "opponent",
    mainColor: shuffled[1],
  });

const 5개를 추가한다.
 
ㆍ빙고 완성한 사람을 저장한다.
 
ㆍ타일의 색상을 저장한다.
 
ㆍplayer에게 부여될 main color를 랜덤으로 섞는다.
 
ㆍme/ opponent의 main color를 부여한다. 이때 서로의 색은 중복되지 않는다.
 

 
useEffect(() => {
    const myWin = checkBingo(boardTiles, myPlayer.mainColor);
    const opponentWin = checkBingo(boardTiles, opponent.mainColor);

    if (myWin) {
      setWinner("me");
    } else if (opponentWin) {
      setWinner("opponent");
    }
  }, [boardTiles]);

빙고판(bingoboard)이 바뀔 때 마다 checkBingo 호출될 수 있도록 useEffect을 추가한다.
 

 
useEffect(() => {
  if (winner) {
    console.log("WINNER:", winner);
  }
}, [winner]);

winner가 잘 출력되는지 콘솔창에서 테스트해본다.
 

 

 
📌 winner 출력
 
winner 출력
 

 
📌 winner 출력 (대각선)
 
winner 출력 (대각선)
 

 

 

2️⃣ player 추가

📌 App.js 코드
 
const [currentTurn, setCurrentTurn] = useState("me"); // 턴 상태
const [players, setPlayers] = useState({
  me: {
    id: "me",
    mainColor: shuffled[0],
    tiles: initialTiles.map(t => ({ ...t, owner: "me" })),
  },
  opponent: {
    id: "opponent",
    mainColor: shuffled[1],
    tiles: initialTiles.map(t => ({
      ...t,
      key: `${t.key}-op`,
      owner: "opponent",
    })),
  },
});
const myTiles = players[currentTurn].tiles;

const 3개를 추가한다.
 
ㆍ현재 turn을 저장한다.
 
ㆍ나의 tiles만 볼 수 있게 분리한다.
 

 
const endTurn = () => {
  setCurrentTurn(prev => (prev === "me" ? "opponent" : "me"));
};

turn 변경 함수를 작성한다. 내 turn이 끝나면 현재 턴(current turn)을 상대로, 상대 turn이 끝나면 현재 턴을 나로 바꾼다.
 

 
const handleFlip = (key, fromBoard = false) => {
  // TileList 뒤집기 (턴 유지)
  setPlayers(prev => {
    const next = structuredClone(prev);
    Object.values(next).forEach(player => {
      player.tiles = player.tiles.map(tile =>
        tile.key === key ? { ...tile, isFlipped: !tile.isFlipped } : tile
      );
    });
    return next;
  });

  // Board 뒤집기 (턴 종료)
  if (fromBoard) {
    setBoardTiles(prev =>
      prev.map(cell =>
        cell?.key === key ? { ...cell, isFlipped: !cell.isFlipped } : cell
      )
    );
    endTurn();
  }
};

뒤집으면 turn이 넘어가도록 handFlip 함수에 endTurn를 추가한다.
 

 
const tile = myTiles.find(t => t.key === active.id);

내 타일만 보이도록 handleDragStart 함수를 수정한다.
 

 
const tile = myTiles.find(t => t.key === active.id);

내 타일만 보이도록 handleDragEnd 함수를 수정한다.
 

 
setPlayers(prev => {
  const next = structuredClone(prev);
  next[currentTurn].tiles =
    next[currentTurn].tiles.filter(t => t.key !== active.id);
  return next;
});

endTurn();

타일이 착수되면 타일을 제거되도록 handleDragEnd 함수를 수정한다.
 

 
<BingoBoard
  boardTiles={boardTiles}
  movableCell={movableCell}
  onFlip={(key) => handleFlip(key, true)}
/>

<TileList
  tiles={players[currentTurn].tiles}
  onFlip={(key) => handleFlip(key, false)}
/>

return 되는 BingoBoard와 TileList의 onFlip을 다음과 같이 수정한다.
 

 

 
📌 turn 넘어감
 
양면빙고 turn 넘어감
 
turn이 잘 넘어가고 있다. 또한, 나의 타일 리스트와 상대의 타일 리스트도 잘 분리되고 있다.
 

 

 

3️⃣ main color 표시

📌 App.js 코드
 
<div style={{ marginBottom: 12, marginLeft: "auto", marginRight:"300px", textAlign: "right", }}>
  <strong>My Main Color:</strong>{" "}
  <span style={{ color: currentTurn === "me" ? players.me.mainColor.toUpperCase() : players.opponent.mainColor.toUpperCase() }}>
    {currentTurn === "me" ? players.me.mainColor.toUpperCase() : players.opponent.mainColor.toUpperCase()}
  </span>
</div>

main color를 빙고판과 타일리스트 사이 우측에 표시되도록 코드를 return에 추가해준다.
 

 

 
📌 main color 표시
 
양면빙고 main color 표시
 
player마다 main color가 잘 출력되는 것을 확인할 수 있다.
 

 

 

4️⃣ 빙고 완성 시 동작 stop

📌 App.js 코드
 
{winner && (
  <div className="winner-overlay">
    <h1>Winner: {winner === "me" ? "ME" : "OPPONENT"}</h1>
  </div>
)}

return <DndContext> 태그 하단에 다음과 같은 코드를 추가해준다.
 

 
if (winner) return;

handleFlip, handleDragStart, handleDragEnd 함수 각각 첫 줄에 다음 코드를 추가한다.
 

 
if (!winner) {
  setCurrentTurn(prev => prev === "me" ? "opponent" : "me");
}

endTurn 함수를 다음과 같이 수정한다.
 

 

 
📌 App.css 코드
 
.winner-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  color: white;
  font-size: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
}

빙고가 완성되면 화면 전체에 winner를 표시하고, 착수/뒤집기/이동 등 동작이 불가능하도록 한다.
 

 

 
📌 동작 stop
 
양면빙고 동작 stop
 
화면 전체에 승자가 표시됨과 동시에 동작이 불가한 것을 확인할 수 있다.
 

 

 

5️⃣ 앞으로...

양면빙고를 2명이서 paly 할 수 있는 기능을 추가했다.
 
이제 시간 제한을 만들고 착수된 타일 개수에 따른 빙고판 이동 / 뒤집기 동작이 가능하도록 할 것이다.
 

 

 

 

 

반응형