Connect four
게임에서 마커가 4개 이상 연결되었는지를 체크하는 로직을 작성하기 위하여, 1차적으로 코드를 작성해 보았다.
break
로 중지한다.// src/slices/gameSlice.ts
drop: (state, actions) => {
if (state.winner !== null) {
console.warn(`이미 종료된 게임입니다. 승자는 ${state.winner}입니다.`);
return;
}
if (state.board[actions.payload.lineNumber][0] !== null) {
console.warn(
`${actions.payload.lineNumber + 1} 열은 이미 전부 채워진 열입니다.`
);
return;
}
let location = null;
/**
* 만약 선택한 행의 첫 번째 셀이 null이 아니면 리턴.
* TODO: 위 경우 다시 마커를 둘 수 있도록 처리해야 한다.
*/
for (let i = 6; i >= 0; i--) {
if (state.board[actions.payload.lineNumber][i] === null) {
state.board[actions.payload.lineNumber][i] =
state.currentPlayer === "RED" ? "RED" : "YELLOW";
location = i;
break;
}
}
state.markerCount += 1;
let rowCount = 1;
let colCount = 1;
if (state.markerCount >= 7) {
for (let i = 1; i <= 3; i++) {
if ((rowCount || colCount) >= 4) {
break;
}
// 가로 테스트
if (
actions.payload.lineNumber + i <= 6 &&
state.board[actions.payload.lineNumber + i][location!] ===
actions.payload.player
) {
rowCount += 1;
} else {
break;
}
}
for (let i = 1; i <= 3; i++) {
if ((rowCount || colCount) >= 4) {
break;
}
if (
actions.payload.lineNumber - i >= 0 &&
state.board[actions.payload.lineNumber - i][location!] ===
actions.payload.player
) {
rowCount += 1;
} else {
break;
}
}
// 세로 테스트
for (let i = 1; i <= 3; i++) {
if ((rowCount || colCount) >= 4) {
break;
}
if (
location! + i <= 5 &&
state.board[actions.payload.lineNumber][location! + i] ===
actions.payload.player
) {
colCount += 1;
} else {
break;
}
}
for (let i = 1; i <= 3; i++) {
if ((rowCount || colCount) >= 4) {
break;
}
if (
location! - i >= 5 &&
state.board[actions.payload.lineNumber][location! - i] ===
actions.payload.player
) {
colCount += 1;
} else {
break;
}
}
// TODO: 대각선 테스트
if (colCount >= 4 || rowCount >= 4) {
state.winner = actions.payload.player;
}
}
state.currentPlayer = actions.payload.player === "RED" ? "YELLOW" : "RED";
},
위 코드의 문제점은 반복된 코드가 많고, 대각선은 한 번에 4번을 테스트해야 한다는 것에 있다.
다행히, 중첩 반복문은 존재하지 않지만 가로/세로 체크에 각각 2번, 거기에 대각선까지 4번의 반복문을 작성해야 한다는 점에서 비슷한 코드가 반복된다는 점에서 가독성도 낮고 좋지 않은 코드라고 볼 수 있다.
이러한 문제점을 해결하기 위해 고민하던 중, 코딩 테스트에 자주 등장하는 섬의 개수
와 비슷한 방법으로 해결하면 좋지 않을까, 하는 생각이 들어 direction vector
을 적용해 보기로 했다.
섬의 개수
는 이중 배열 내에 있는 모든 값을 탐색하고, 섬일 경우를 체크하는 로직을 사용한다. 섬은 상하좌우/대각선으로 이루어져 있다.
내 경우는 최근에 둔 마커를 기준으로 마찬가지로 상하좌우 대각선 모든 방향을 탐색할 수 있도록 처리하고, 다른 사람이 둔 마커와 빈 공간을 바다라고 생각하고 처리하면 될 것 같다는 생각이 들어 이 방식을 선택했다.
여기에 추가로 연결되어 있는 것만 체크해 준다.
승리 조건 테스트를 위해 모든 방향을 체크해 주어야 한다.
// brute.ts
const board = [
[null, null, null, null, null, null],
[null, null, null, null, null, null],
[null, null, null, null, null, null],
[null, null, null, null, null, null],
[null, null, null, null, null, null],
[null, null, null, null, null, null],
["RED", null, "RED", "RED", null, null],
] as (null | "RED" | "YELLOW")[][];
// 모든 방향을 체크하기 위해 방향 벡터를 사용한다.
const movement = [
{ dx: 1, dy: 0 }, // 오른쪽
{ dx: -1, dy: 0 }, // 왼쪽
{ dx: 0, dy: 1 }, // 위
{ dx: 0, dy: -1 }, // 아래
{ dx: 1, dy: 1 }, // 오른쪽 아래 대각선
{ dx: -1, dy: 1 }, // 왼쪽 아래 대각선
{ dx: 1, dy: -1 }, // 오른쪽 위 대각선
{ dx: -1, dy: -1 }, // 왼쪽 위 대각선
];
let x = 6;
let y = 1;
let currentPlayer = "RED";
const checkDirection = (x: number, y: number, dx: number, dy: number) => {
let count = 1; // 기존 마커도 추가.
let nx = x + dx; // X축 각 방향별로 1칸씩 이동
let ny = y + dy; // Y축 각 방향별로 1칸씩 이동
while (
// 각 좌표가 보드 내에 있고, 해당 위치에 있는 마커가 현재 유저의 마커와 같은 색상의 마커인지 확인한다.
// 만약 마커가 보드를 넘어갈 경우, 종료.
nx >= 0 &&
ny >= 0 &&
nx <= 6 &&
ny <= 5 &&
board[nx][ny] === currentPlayer
) {
count++;
nx += dx;
ny += dy;
}
return count;
};
for (const { dx, dy } of movement) {
const count = checkDirection(x, y, dx, dy);
if (count >= 4) {
console.log(`${currentPlayer}가 승리했습니다.`);
break;
}
}
위 코드에서는 위, 아래, 오른쪽, 왼쪽, 오른쪽 아래, 오른쪽 위, 왼쪽 위, 왼쪽 아래 대각선을 모두 체크해 준다.
로직은 다음과 같다.
count
가 4 이상일 경우, 조건을 충족하므로 종료시킨다.이 방식의 문제점은, 연결을 테스트하는 로직을 한 방향
으로만 테스트한다는 것이다. 위에 있는 board에서처럼 ["RED", null, "RED", "RED", null, null]
로 이루어져 있고, board[6][1]
의 칸에 두면 분명 4개의 마커가 이어져 있는데도, 카운트를 한 방향으로만 하기 때문에 승자로 처리하지 않는다.
방향 탐색을 가로, 세로, 양수 대각선, 음수 대각선으로 지정하고 두 번 계산
한다.
// brute.ts
drop: (state, actions) => {
if (state.winner !== null) {
console.warn(`이미 종료된 게임입니다. 승자는 ${state.winner}입니다.`);
return;
}
if (state.board[actions.payload.lineNumber][0] !== null) {
console.warn(
`${actions.payload.lineNumber + 1} 열은 이미 전부 채워진 열입니다.`
);
return;
}
let location: null | number = null;
/**
* 만약 선택한 행의 첫 번째 셀이 null이 아니면 리턴.
*/
for (let i = 6; i >= 0; i--) {
if (state.board[actions.payload.lineNumber][i] === null) {
state.board[actions.payload.lineNumber][i] =
state.currentPlayer === "RED" ? "RED" : "YELLOW";
location = i;
break;
}
}
state.markerCount += 1;
// 모든 방향을 체크하기 위해 방향 벡터를 사용한다.
const movement = [
{ dx: 1, dy: 0 }, // 가로
{ dx: 0, dy: 1 }, // 세로
{ dx: -1, dy: 1 }, // 양수 대각선
{ dx: -1, dy: -1 }, // 음수 대각선
];
// 연결 테스트
const checkDirection = (dx: number, dy: number) => {
let count = 1; // 기존 마커도 추가.
let pnx = actions.payload.lineNumber + dx; // X축 각 방향별로 1칸씩 이동
let pny = location! + dy; // Y축 각 방향별로 1칸씩 이동
while (
// 각 좌표가 보드 내에 있고, 해당 위치에 있는 마커가 현재 유저의 마커와 같은 색상의 마커인지 확인한다.
// 만약 마커가 보드를 넘어갈 경우, 종료.
pnx >= 0 &&
pny >= 0 &&
pnx <= 6 &&
pny <= 5 &&
state.board[pnx][pny] === actions.payload.player
) {
count++;
pnx += dx;
pny += dy;
}
let mnx = actions.payload.lineNumber - dx; // X축 각 방향별로 1칸씩 이동
let mny = location! - dy; // Y축 각 방향별로 1칸씩 이동
while (
// 각 좌표가 보드 내에 있고, 해당 위치에 있는 마커가 현재 유저의 마커와 같은 색상의 마커인지 확인한다.
// 만약 마커가 보드를 넘어갈 경우, 종료.
mnx >= 0 &&
mny >= 0 &&
mnx <= 6 &&
mny <= 5 &&
state.board[mnx][mny] === actions.payload.player
) {
count++;
mnx -= dx;
mny -= dy;
}
return count;
};
for (const { dx, dy } of movement) {
const count = checkDirection(dx, dy);
if (count >= 4) {
state.winner = actions.payload.player;
console.log(`${state.winner}가 승리했습니다.`);
return;
}
}
state.currentPlayer = actions.payload.player === "RED" ? "YELLOW" : "RED";
},