블록을 해당하는 위치에 놓을 수 있는지 확인하는 함수를 마저 작성해보자
지난 글에서는 놓으려는 블록이 보드 내부에 있는지, 첫 수가 유효한지 확인하는 함수를 구현하였다. 오늘 구현할 내용은 아래와 같다.
즉, 연결이 있는지, 닿은 부분이 있는지, 겹치는 부분이 있는지 확인하는 함수를 작성해야 한다. 차근차근 네이밍부터 해보자.
const hasDiagonalConnection = ...
const hasEdgeConnection = ...
const hasOverlap = ...
역시 네이밍이 제일 어려운 것 같다. 추후 조금 더 좋은 이름으로 수정할 수 있겠다는 느낌이 든다. 순서대로 구현을 시작해보자.
hasDiagonalConnection()
requester의 다른 블록 중 적어도 하나와 모서리끼리 연결되어 있는지 확인을 해야한다. 우리는 requester의 블록 정보를 playerIdx로 가져왔으니, 놓으려고 하는 블록의 true인 셀의 모서리를 확인하면 되겠다.
const [row, col] = position;
block.some((blockLine, rowIdx) =>
block.some((blockCell, colIdx) => {
if (blockCell === true) {
// [TODO] 모서리 확인하기
}
})
);
모서리가 어디일까? 모서리를 어떻게 확인하는 것이 좋을까?
const block0 = [[true, false], [true, true], [true, true]];
이 블록을 살펴보자.block0[1][0]
은 대각 연결을 가질 수 있을까? 명백히 아니다. 그 이유는 해당 셀의 상, 하, 우측에 다른 블록의 true 셀이 존재하기 때문이다. block0[1][1]
은 대각 연결을 가질 수 있을까? 직관적으로 '그렇다'고 답할 수 있다. 해당 셀의 우상단에 대각 연결이 존재할 수 있을 것이다.
이번에는 다음 블록을 살펴보자.
const block1 = [[true, true], [true, true]];
block1[0][0]
위치에서는 좌상단 연결이 가능할 것이다. 이 3개의 셀로부터 '대각 연결이 존재할 수 있는 셀'을 얻는 방법을 생각해보자.
어떤 true인 블록의 한 셀을 선택했을 때, 상단 블록 셀이 true이면 우상단, 좌상단에서는 대각 연결이 있을 수 없다(block0[1][0]
). 좌측 블록 셀이 true이면 우상단, 우하단에서는 대각 연결이 있을 수 없다(block1[1][0]
). 하단과 우측의 validation 필요 여부도 같은 방식일 것이다. 이를 한 번 구현해보자. 아래 코드는 위 TODO 주석이 위치하는 부분에 들어간다.
// 우측
if (block[rowIdx][colIdx + 1] === false) {
// 보드의 해당 위치에 playerIdx와 같은 값이 있는지 검사
}
// 좌측
if (block[rowIdx][colIdx - 1] === false) {
// 이하 동일
}
여기서 문제가 발생한다. if statement 내부에서 검사를 수행하게 되면, 우측에 블럭이 없어서 검사를 하더라도 하단에 블럭이 있는 경우 우하단 대각선 검사는 무의미해진다. 검사가 필요한 셀만 찾아내기 위해 임시 배열을 사용해보자.
const diagonalCells = [[true, true], [true, true]];
순서대로 좌상단, 우상단, 좌하단, 우하단을 의미하도록 하면 될 것 같다. 이제 해당 셀이 검사를 필요로 하는지 마킹하도록 위 if statement를 수정해보자.
if (block[rowIdx][colIdx + 1] === true) {
diagonalCells[0][1] = false;
diagonalCells[1][1] = false;
}
if (block[rowIdx][colIdx - 1] === true) {
diagonalCells[0][0] = false;
diagonalCells[1][0] = false;
}
...
문제가 있다. colIdx - 1
등이 블록의 범위를 초과하면 어떡할까? 범위를 초과하는 블록 셀, 즉 block1[0][1]
의 우측 셀의 경우 대각 연결 검사를 진행해야 한다. 다시 말해 if statement를스킵해야 한다. 아래와 같이 수정할 수 있겠다.
if (colIdx < block[0].length - 1 && block[rowIdx][colIdx + 1] === true) {
diagonalCells[0][1] = false;
diagonalCells[1][1] = false;
}
if (colIdx > 0 && block[rowIdx][colIdx - 1] === true) {
diagonalCells[0][0] = false;
diagonalCells[1][0] = false;
}
자바스크립트의 동작 중 if statement 내부에 여러 &&
, ||
연산자가 존재하는 경우 조금 특별한 것이 있다. &&
는 falsy value를 만나면 뒤의 조건을 검사하지 않고 if statement를 통과하고 ||
는 truthy value를 만나면 뒤의 조건을 검사하지 않고 if statement 내부로 진입한다. 이를 통해 undefined 접근으로 발생하는 에러를 보지 않도록 하겠다. 유효성도 체크하고 에러도 건너뛰니 일석이조가 아닐 수 없다.
이제 diagonalCells
배열을 통해 현재 셀에 대각 연결이 존재하는지 확인하겠다. diagonalCells[0][0]
인 경우 좌상단, [0][1]
인 경우 우상단, ...을 확인하면 되겠다.
좌상단은 현재 셀의 포지션 대비 [-1][-1]
이며 우상단은 [1][-1]
, 좌하단은 [-1][1]
, 우하단은 [1][1]
이 된다. 하지만 조금 전 diagonalCells
의 멤버들은 0과 1로 이뤄져있으니 0을 -1로, 1을 1로 변환하여 연산하면 되겠다. 이를 위해 2 * x - 1
을 사용하겠다. 이 값을 탐색 중인 블록의 셀 위치와 포지션 정보까지 합치면 보드 위의 셀(즉 대각 위치에 있는 셀)을 얻을 수 있겠다. 이렇게 복잡하게 얻은 셀의 값이 playerIdx
와 일치하면 대각 연결이 있다고 판단하면 된다.
diagonalCells.some((diagonalCellLine, diagonalCellRow) =>
diagonalCellLine.some((cell, diagonalCellCol) => {
if (!cell) {
return false;
}
const rowOnBoard = row + rowIdx + 2 * diagonalCellRow - 1;
const colOnBoard = col + colIdx + 2 * diagonalCellCol - 1;
if (rowOnBoard < 0 || colOnBoard < 0 || rowOnBoard > 19 || colOnBoard > 19) {
return false;
}
return board[rowOnBoard][colOnBoard] === playerIdx;
})
);
구현체를 정리하면 아래와 같다.
const hasDiagonalConnection = ({ block, position, board, playerIdx, turn }: PlaceBlockDTO) => {
const [row, col] = position;
return block.some((blockLine, rowIdx) =>
blockLine.some((blockCell, colIdx) => {
if (blockCell === true) {
const diagonalCells = [[true, true], [true, true]];
if (colIdx < block[0].length - 1 && block[rowIdx][colIdx + 1] === true) {
diagonalCells[0][1] = false;
diagonalCells[1][1] = false;
}
if (colIdx > 0 && block[rowIdx][colIdx - 1] === true) {
diagonalCells[0][0] = false;
diagonalCells[1][0] = false;
}
if (rowIdx > 0 && block[rowIdx - 1][colIdx] === true) {
diagonalCells[0][0] = false;
diagonalCells[0][1] = false;
}
if (rowIdx < block.length - 1 && block[rowIdx + 1][colIdx] === true) {
diagonalCells[1][0] = false;
diagonalCells[1][1] = false;
}
return diagonalCells.some((diagonalCellLine, diagonalCellRow) =>
diagonalCellLine.some((cell, diagonalCellCol) => {
if (!cell) {
return false;
}
const rowOnBoard = row + rowIdx + 2 * diagonalCellRow - 1;
const colOnBoard = col + colIdx + 2 * diagonalCellCol - 1;
if (rowOnBoard < 0 || colOnBoard < 0 || rowOnBoard > 19 || colOnBoard > 19) {
return false;
}
return board[rowOnBoard][colOnBoard] === playerIdx;
})
);
}
})
);
};
다시 가독성이 우주로 가버렸다. 대각 연결이 있는지 확인을 해야하는 diagonalCells
를 추출하는 함수를 만들어 로직을 분리해 가독성을 올려보자.
const getDiagonalCellsToCheck = ({ block, row, col }: { block: BlockMatrix, row: number, col: number }) => {
const diagonalCells = [[true, true], [true, true]];
if (col < block[0].length - 1 && block[row][col + 1] === true) {
diagonalCells[0][1] = false;
diagonalCells[1][1] = false;
}
if (col > 0 && block[row][col - 1] === true) {
diagonalCells[0][0] = false;
diagonalCells[1][0] = false;
}
if (row > 0 && block[row - 1][col] === true) {
diagonalCells[0][0] = false;
diagonalCells[0][1] = false;
}
if (row < block.length - 1 && block[row + 1][col] === true) {
diagonalCells[1][0] = false;
diagonalCells[1][1] = false;
}
return diagonalCells;
};
const hasDiagonalConnection = ({ block, position, board, playerIdx }: PlaceBlockDTO) => {
const [row, col] = position;
return block.some((blockLine, rowIdx) =>
blockLine.some((blockCell, colIdx) => {
if (blockCell === true) {
const diagonalCells = getDiagonalCellsToCheck({ block, row: rowIdx, col: colIdx });
return diagonalCells.some((diagonalCellLine, diagonalCellRow) =>
diagonalCellLine.some((cell, diagonalCellCol) => {
if (!cell) {
return false;
}
const rowOnBoard = row + rowIdx + 2 * diagonalCellRow - 1;
const colOnBoard = col + colIdx + 2 * diagonalCellCol - 1;
if (rowOnBoard < 0 || colOnBoard < 0 || rowOnBoard > 19 || colOnBoard > 19) {
return false;
}
return board[rowOnBoard][colOnBoard] === playerIdx;
})
);
}
})
);
};
한결 낫다. hasDiagonalConnection
을 순서대로 읽어보면,
block
을 순회하여정도가 될 것 같다.
갑자기 validation 로직을 작성하다 왜 블록을 놓는 함수를 작성할까? 테스트를 위해서이다. 작성하는 validation에 논리적 허점이 있거나 구현에 문제가 있음을 잡아낼 방법이 지금은 없다. 진작 이를 구현하여 테스트 케이스들을 작성해야 했는데 어리석었다. 지금이라도 실수를 만회하기 위해 구현을 해보겠다.
placeBlock()
블록을 놓는 위치는 (position[0], position[1])
이다. 즉, block[0][0]
은 board[position[0]][position[1]]
에 위치하고 block[n][m]
은 board[position[0] + n][position[1] + m]
에 위치한다.
const placeBlock = ({ block, position, board, playerIdx }: PlaceBlockDTO) => {
const [row, col] = position;
block.forEach((blockLine, rowIdx) => {
blockLine.forEach((blockCell, colIdx) => {
if (blockCell) {
board[row + rowIdx][col + colIdx] = playerIdx;
}
});
});
};
블록을 놓을 수 있는지 검사를 한 뒤 호출되기 때문에 이렇게 구현하면 되겠다.
다음 시간에는 놓을 수 있는지 확인하는 로직 뿐만 아니라 테스트에 대해서도 논해보고자 한다. 고지가 눈앞이다.