3가지 발판 유형을 가지는 사다리 게임을 구현하는 알고리즘입니다. 사다리를 리턴할 수 있는 각각의 함수 4개를 구현해 가져옵니다.
main.js
을 만들어 한꺼번에 작성합니다.
reset()
, randomFill()
, analyze()
, display()
함수를 구현합니다.display()
함수로 출력합니다. 5줄을 가진 문자열을 출력합니다.사다리 구조 관련 변수 초기화 : 먼저 고정된 데이터들을 상수로 정의하고, 사다리 정보를 담은 변수를 정의했습니다.
PLAYERS
, Column을 나타내는 HEIGHT
, 발판 정보를 담은 STEP_TYPES
(수평, 우하향 등)row
, col
와 발판을 나타내는 step
을 객체로 저장하는 배열 ladderSteps
함수 구현 : 이번엔 실제 코드 구현한다 생각하고, 비슷하게 써봤습니다.
reset()
: 사다리의 행과 열을 초기화합니다.
randomFill()
: 랜덤하게 3가지 발판 종류를 선택해 사다리 데이터 구조를 채웁니다. 총 발판의 개수도 랜덤하게 결정합니다.
함수 randomFill():
난수 + 인덱스로 발판을 뽑아내기 위해 배열에 3가지 종류의 발판 (HORIZON, RIGHT_DOWN, LEFT_DOWN)을 정의합니다.
각 행을 반복:
각 열을 반복:
발판 삽입 조건을 위해 난수를 생성합니다.
발판 삽입 난수가 0.x 이상이면:
발판 종류 선택을 위한 난수를 생성합니다.
발판 종류를 정의한 배열에 발판 종류 선택 난수로 랜덤한 발판 유형을 선택합니다.
현재 행, 열 위치와 선택된 발판의 유형을 ladderSteps 배열에 추가합니다.
analyze()
: 사다리 데이터 구조를 분석해 결과를 리턴합니다. 수평 발판 연속 또는 상반된 발판 조합이 연속된 경우 false
, 그렇지 않으면 true
를 반환합니다. 좌우만 비교하면 되기에 배열 안에 이웃하는 step
이 있는지 확인하면 됩니다.함수 analyze():
ladderSteps 배열의 각 요소에 대해:
이웃하는 발판들을 찾습니다. 찾았다면:
현재 발판과 다음 발판이 모두 HORIZON이면:
false를 반환합니다.
현재 발판이 RIGHT_DOWN이고 다음 발판이 LEFT_DOWN이면:
false를 반환합니다.
현재 발판이 LEFT_DOWN이고 다음 발판이 RIGHT_DOWN이면:
false를 반환합니다.
반복이 끝나고 여기까지 온다면 true를 반환합니다.
display()
: ladder
와 ladderSteps
를 조합해 문자열을 가공하고, 출력합니다.함수 display():
결과값을 담을 문자열 변수를 선언 및 초기화합니다.
ladder의 각 열을 반복합니다.
반복이 시작할 때 결과값에 '|'를 추가합니다.
ladder의 각 행을 반복합니다.
현재 ladderSteps에서 행, 열에 위치한 데이터가 있는지 찾습니다.(find?)
ladderSteps에 해당되는 발판이 있다면?
해당 발판의 step을 결과값에 추가합니다.
없다면?
공백 3개를 결과값에 추가합니다.
조건이 끝나고 '|'를 추가합니다.
행의 반복이 끝나면 줄바꿈 문자열 `\\n`을 추가합니다.
각 열의 반복이 모두 끝나면 log에 출력합니다.
추가로 직접 입력해서 그 결과값만 봐도 되지만 어제 배운 readline
과 output
을 넣어서 질문 형태와 결과값을 받는 형태로도 구현해보도록 하겠습니다.
// 상수(전부 UPPER + SNAKE)
// 사다리 배경, PLAYERS는 사다리의 Row-행, HEIGHT는 사다리의 Col-열로 생각하기.
const PLAYERS = 5;
const HEIGHT = 5;
// 발판 정보
const STEP_TYPES = {
HORIZON: '---',
RIGHT_DOWN: '\\-\\',
LEFT_DOWN: '/-/',
NOTHING: ' ',
};
// 발판 존재 및 위치 정의 배열: 사다리 데이터에 직접 넣지 말라했으니 발판만 정의함. 행, 열, 발판으로 이루어진 객체가 들어갈 예정.
let ladderSteps = [];
PLAYERS
, HEIGHT
를 사다리의 행과 열로 판단합니다.step
의 종류와 위치 정보가 객체로 속하게끔하고, 문자열 가공에 쓸 수 있도록 ladderSteps
만 구현했습니다.reset()
작성했던 발판을 빈 배열로 재할당합니다.
// 1. 리셋 함수: 발판을 재정의.
const reset = () => {
ladderSteps = [];
};
randomFill()
// 2. 발판 채우기 함수.
// JS 메소드인 random을 이용할 것임.
const randomFill = () => {
const stepTypesArr = [
STEP_TYPES.HORIZON,
STEP_TYPES.RIGHT_DOWN,
STEP_TYPES.LEFT_DOWN,
];
// 사다리의 각 행과 각 열을 순회
for (let row = 0; row < HEIGHT; row++) {
// 실수: 사다리가 5개이니, 사이에 발판을 놓을 수 있는 자리는 4개. 그래서 -1로 수정..
for (let col = 0; col < PLAYERS - 1; col++) {
// 발판을 놓을지 결정하는 난수 생성 (0.0 ~ 1.0)
const stepCondiRandom = Math.random();
// 발판을 놓을 확률을 0.5 이상으로 설정 (0.5 이상일 경우에만 발판을 놓음)
if (stepCondiRandom >= 0.5) {
// 발판의 유형을 결정하는 난수 생성 (0.0 ~ 1.0)
const stepTypeRandom = Math.random();
// 발판을 놓을 위치와 유형을 ladderSteps 배열에 추가
ladderSteps.push({
row,
col,
// stepTypesArr 배열에서 난수로 결정된 발판 유형 선택
// 0.0xx ~ 2.xxx 범위의 값을 floor로 0, 1, 2 중 하나 선택, 발판 지정
step: stepTypesArr[Math.floor(stepTypeRandom * 3)],
});
// console.log(
// stepCondiRandom - 0.7,
// stepTypesArr[Math.floor(stepTypeRandom * 3)],
// );
}
// console.log(ladderSteps);
}
}
};
PLAYERS
, HEIGHT
를 사다리의 행과 열로 판단하기에 각 행, 열의 위치를 가지고 순회합니다.* 3
을 해 0 초과 3 미만의 수를 반환하게 하고, floor를 이용해 0, 1, 2만 나오게 했습니다.i
나 j
대신 row
로 for문 변수를 직관적이게 만들었습니다.analyze()
// 3. 발판 유효 검사 함수.
const analyze = () => {
// console.log('사다리 구조 분석 시작...');
for (let i = 0; i < ladderSteps.length - 1; i++) {
const curStep = ladderSteps[i];
// const nextStep = ladderSteps[i + 1];
const nextStep = ladderSteps.find(
(step) => step.row === curStep.row && step.col === curStep.col + 1,
);
if (nextStep) {
// console.log(
// `현재 발판 단계: ${i} (row: ${curStep.row}, col: ${curStep.col}, step: ${curStep.step}) | 비교 발판 단계 (row: ${nextStep.row}, col: ${nextStep.col}, step: ${nextStep.step})`,
// );
if (curStep.row === nextStep.row) {
if (
curStep.step === STEP_TYPES.HORIZON &&
nextStep.step === STEP_TYPES.HORIZON
) {
// console.log('유효하지 않은 발판 발견: HORIZON 옆 HORIZON');
return false;
}
if (
curStep.step === STEP_TYPES.RIGHT_DOWN &&
nextStep.step === STEP_TYPES.LEFT_DOWN
) {
// console.log('유효하지 않은 발판 발견: RIGHT_DOWN 옆 LEFT_DOWN');
return false;
}
if (
curStep.step === STEP_TYPES.LEFT_DOWN &&
nextStep.step === STEP_TYPES.RIGHT_DOWN
) {
// console.log('유효하지 않은 발판 발견: LEFT_DOWN 옆 RIGHT_DOWN');
return false;
}
}
}
}
// console.log('사다리가 유효합니다!');
return true;
};
ladderSteps
를 검사합니다. 반복문을 돌며 현재 발판과 다음 발판의 유효성을 검사합니다.nextStep
을 ladderSteps[i + 1]
로 단순하게 설정해 다음 인덱스의 발판을 검사했습니다. 이렇게 하면 실제 이웃하는 행인지 검사하지 못합니다. find
메서드를 사용해 정확하게 같은 행(row)에 있고 이웃한 열(col)에 있는지 조건을 따져 고쳤습니다.log
를 찍어 관찰했습니다.display()
// 4. 사다리 출력 함수.
// 가독성을 위해 결과값 줄바꿈을 나타내기 위해서 한 줄 씩 나오도록 수정.
const display = () => {
// let resultLadder = '';
for (let row = 0; row < HEIGHT; row++) {
let resultLadder = '';
// 반복이 시작할 때 결과값에 '|'를 추가.
resultLadder += '|';
// ladderSteps을 반복해 행, 열에 위치한 데이터가 있는지 찾음.
for (let col = 0; col < PLAYERS - 1; col++) {
// 현재 스텝이 있는지 찾음. find
const curStep = ladderSteps.find(
(ladderStep) => ladderStep.row === row && ladderStep.col === col,
);
// console.log(curStep);
// 현재 스텝이 있다면 추가. 아니면 공백 추가.
resultLadder += curStep ? curStep.step : STEP_TYPES.NOTHING;
// 마무리 할 때 버티컬 바 추가
resultLadder += '|';
}
resultLadder += '\\n';
// 반환
console.log(resultLadder);
}
// console.log(resultLadder);
};
row
와 col
을 돌며 실제 사다리를 만듭니다.|
를 만들고, 열이 반복되면서 스텝 여부 및 |
를 추가합니다. 행의 반복이 끝난 후 \n
을 출력합니다.readline
구현따로 입력값을 주지 않는 알고리즘 테스트이긴 하지만 테스트를 용이하게 하려는 것도 있었고, 사용자와의 상호작용도 중요하다 생각해서 추가해봤습니다.
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// 사용자 입력을 받아 함수 실행
const handleUserInput = (input) => {
switch (input.trim()) {
case 'reset':
reset();
display();
break;
case 'randomFill':
randomFill();
display();
break;
case 'analyze':
console.log('사다리 구조 유효성:', analyze());
break;
case 'display':
display();
break;
case 'exit':
rl.close();
break;
default:
console.log(
'올바르지 않는 명령어입니다. 사용 가능한 명령어: reset, randomFill, analyze, display, exit',
);
}
};
rl.on('line', handleUserInput);
console.log(
'명령어를 입력해주세요.: reset, randomFill, analyze, display, exit',
);
display()
를 붙였습니다.exit
를 입력해야 명령어 입력이 끝납니다.rl.on
으로 한 줄 씩, handleUserInput
함수를 사용하게 했습니다.JS의 random()
메소드는 사실 안전하지 않습니다. 참고 : MDN Math.random()
암호학적으로 안전하지 않아 노력만 한다면 패턴이 파악돼 뚫린다는 얘기가 있습니다.
MDN에서도 권장하는 방식인 Math.random()
대신 crypto
모듈을 사용해 난수를 생성하도록 리팩토링하겠습니다.
const readline = require('readline');
const crypto = require('crypto');
...
// JS 메소드인 random을 이용할 것임.
// => random은 안전한 난수가 아니므로 crypto를 사용함.
const getRandom = () => {
// 난수 생성 함수 (0 ~ 1 사이의 난수를 반환)
return crypto.randomBytes(4).readUInt32LE() / 0xffffffff;
};
// 2. 발판 채우기 함수.
const randomFill = () => {
const stepTypesArr = [
STEP_TYPES.HORIZON,
STEP_TYPES.RIGHT_DOWN,
STEP_TYPES.LEFT_DOWN,
];
for (let row = 0; row < HEIGHT; row++) {
for (let col = 0; col < PLAYERS - 1; col++) {
// 발판을 놓을지 결정하는 난수 생성 (0.0 ~ 1.0)
// const stepCondiRandom = Math.random();
const stepCondiRandom = getRandom();
if (stepCondiRandom >= 0.5) {
// 발판의 유형을 결정하는 난수 생성 (0.0 ~ 1.0)
// const stepTypeRandom = Math.random();
const stepTypeRandom = getRandom();
ladderSteps.push({
row,
col,
step: stepTypesArr[Math.floor(stepTypeRandom * 3)],
});
}
}
}
};
crypto
모듈 추가:
crypto
모듈을 사용해 암호학적으로 안전한 난수를 생성해줍니다.crypto.randomBytes()
와 readUInt32LE()
를 사용해 0 ~ 1 사이의 난수를 생성하는 getRandom
함수를 구현했습니다.crypto.randomBytes(4)
: 4바이트(32비트)의 무작위 데이터를 생성하여 버퍼 객체로 반환합니다..readUInt32LE()
: 반환된 버퍼 객체에서 32비트 부호 없는 정수를 Little Endian 방식으로 읽어옵니다./ 0xffffffff
: 생성된 무작위 값을 0과 1 사이의 부동 소수점 숫자로 변환합니다.randomFill
함수 교체:
Math.random()
대신 getRandom()
함수를 사용해 난수를 생성해줍니다.reset()
함수 : 사다리 데이터 초기화randomFill()
함수 : 발판 랜덤 배치 로직 구현analyze()
함수 : 사다리 유효성 검사 로직 구현display()
함수 : 사다리 구조를 문자열로 변환해 출력readline
구현) : 사용자 입력을 받아 함수 실행Math.random()
대신 crypto
모듈 사용해 안전한 난수 생성새롭게 배운 점
crypto
모듈을 사용해 안전한 난수 생성 방법row
, col
쓰기문제 해결에 신경 쓴 부분
가장 어려웠던 부분과 극복 방법
find
메소드를 사용해 문제를 해결.어제 클래스를 쓰면서 공부했었는데 오늘 접근법을 보면서 클래스형을 보니 되게 반가우면서도, 오늘 연습했어야했네 생각이 들기도 했습니다. 문제 보면서도, 이거 '완전 클래스네' 싶었는데 함수형으로 작성해도 무방하다는 글을 보고 '함수형이 더 어렵구나!' 라고 생각이 들어서 함수형으로 작성한 게 다르긴 했습니다.
또, 저는 '사다리 데이터에 출력용 문자열을 집어넣지 말아라'를 발판도 집어넣으면 안된다라고 보고, 굳이 사다리 전체를 만들지 않고, 발판 데이터만 만들어 display()
부분에 만들어서 하도록 했습니다. 직관성을 생각하면 수료생님처럼 만드는 게 맞았는지도 모르겠습니다. ㅠ
물론 핵심적인 기능은 다 구현했고, random
대신 안전한 난수 crypto
를 써서 난수를 생성했고, readline
을 써서 사용자의 input과 질문을 작성하기 위한 output
을 제작해서 동적으로 구현도 했습니다! 재밌는 구현이었습니다.