TS스터디 팀원들과 함께 매주 주차별 과제를 진행합니다
https://github.com/bradtraversy/vanillawebprojects
const correct = document.getElementById('word') as HTMLDivElement;
const wrong = document.getElementById('wrong-letters') as HTMLDivElement;
const duplication = document.getElementById('notification-container') as HTMLDivElement;
const popup = document.getElementById('popup-container') as HTMLDivElement;
const playAgainButton = document.getElementById('play-button') as HTMLButtonElement;
const finalMessage = document.getElementById('final-message') as HTMLElement;
const finalMessageRevealWord = document.getElementById('final-message-reveal-word') as HTMLElement;
const hangman = Array.from(document.querySelectorAll('.figure-part')) as HTMLElement[]; // HTML요소들이 들어있는 일반 배열
type answerState = {
stateName: 'answerState';
words: string[];
};
type wrongState = {
stateName: 'wrongState';
words: string[];
};
type playableState = {
stateName: 'playableState';
isPlayable: boolean;
};
type stateType = answerState | wrongState | playableState;
const useState = <T extends stateType>(status: T): [() => T, (state: T) => void] => {
let initialState = status;
const state = () => initialState as T;
const setState = (newState: T) => {
initialState = newState;
// playableState상태가 변경 됐을 때는 리렌더링을 안 함
if (newState.stateName === 'answerState') {
answerRender();
}
if (newState.stateName === 'wrongState') {
wrongRender();
}
};
return [state, setState];
};
const [getAnswerState, setAnswerState] = useState({
stateName: 'answerState',
words: [],
} as answerState);
const [getwrongState, setwrongState] = useState({
stateName: 'wrongState',
words: [],
} as wrongState);
const [getPlayableState, setPlayableState] = useState({
stateName: 'playableState',
isPlayable: true,
} as playableState);
const getRandomAnswer = () : string => {
const words = ['application', 'programming', 'interface', 'wizard'];
let randomIndex = Math.floor(Math.random() * words.length);
return words[randomIndex];
};
// 글자를 맞췄을 때의 리렌더링 로직
const answerRender = () : void => {
const current : string [] = getAnswerState().words;
correct.innerHTML = '';
const compareResult = randomAnswer.split('').map((i) => {
return current.includes(i) ? i : '';
});
console.log(compareResult)
for (let i of compareResult) {
correct.innerHTML += `<span class="letter">${i}</span>`;
}
let check = new Set(randomAnswer);
if (current.length === check.size) {
finalMessage.innerText = 'Congratulations! You won! 😃';
finalMessageRevealWord.innerText = '';
popup.style.display = 'flex';
setPlayableState({
stateName: 'playableState',
isPlayable: false,
});
}
};
// 글자를 틀렸을 때의 리렌더링 로직
const wrongRender = () : void => {
const current : string [] = getwrongState().words;
if (current.length > 0) {
wrong.innerHTML = `<p>Wrong</p>`;
current.forEach((i) => {
wrong.innerHTML += `<span>${i}</span>`;
});
}
if (current.length === 0) {
wrong.innerHTML = '';
}
for (let i = 0; i < hangman.length; i++) {
if (i < current.length) hangman[i].style.display = 'block';
if (i >= current.length) hangman[i].style.display = 'none';
}
if (current.length === hangman.length) {
finalMessage.innerText = 'Unfortunately you lost. 😕';
finalMessageRevealWord.innerText = `...the word was: ${randomAnswer}`;
popup.style.display = 'flex';
setPlayableState({
stateName: 'playableState',
isPlayable: false,
});
return;
}
};
// 이미 입력했던 값을 입력했을 때 발생
const showDuplication = () : void => {
duplication.classList.add('show');
setTimeout(() => {
duplication.classList.remove('show');
}, 2000);
};
// 첫 판 시작
let randomAnswer = getRandomAnswer();
console.log(randomAnswer);
answerRender();
// 키보드 입력 이벤트
const keyboardEvent = (e: KeyboardEvent) : void => {
if (!getPlayableState().isPlayable) return;
let currentAnswer = getAnswerState().words;
let currentWrong = getwrongState().words;
let keyboardInput = e.key.toLowerCase();
if (randomAnswer.includes(keyboardInput)) {
// 중복처리
if (!currentAnswer.includes(keyboardInput)) {
currentAnswer.push(keyboardInput);
setAnswerState({
stateName: 'answerState',
words: currentAnswer,
});
return;
}
if (currentAnswer.includes(keyboardInput)) {
showDuplication();
return;
}
}
if (!randomAnswer.includes(keyboardInput)) {
// 중복처리
if (!currentWrong.includes(keyboardInput)) {
currentWrong.push(keyboardInput);
setwrongState({
stateName: 'wrongState',
words: currentWrong,
});
return;
}
if (currentWrong.includes(keyboardInput)) {
showDuplication();
return;
}
}
};
// 재시작
const reStart = () : void => {
// 정답 재생성
randomAnswer = getRandomAnswer();
console.log(randomAnswer);
// 팝업창 지우기
popup.style.display = 'none';
// 모든 상태 초기화 및 초기화된 값을 기반으로 리렌더링
setPlayableState({
stateName: 'playableState',
isPlayable: true,
} as playableState);
setAnswerState({
stateName: 'answerState',
words: [],
} as answerState);
setwrongState({
stateName: 'wrongState',
words: [],
} as wrongState);
};
// 이벤트 할당
window.addEventListener('keydown', keyboardEvent);
playAgainButton.addEventListener('click', reStart);
기본 세팅
키보드로 알파벳 입력
게임이 끝나게 되면
id="word” 인 div태그 안에 , 아래와 같이 span 요소로 입력 값을 추가해야 한다
<div class="word" id="word">
<span class="letter">i</span>
<span class="letter">n</span>
<span class="letter">t</span>
<span class="letter"></span>
<span class="letter"></span>
</div>
최종 정답을 맞춘다면
<div class="popup-container" id="popup-container">
<div class="popup">
<h2 id="final-message"></h2>
<h3 id="final-message-reveal-word"></h3>
<button id="play-button">Play Again</button>
</div>
</div>
id="wrong-letters"인 div 태그 안에 , 아래와 같이 p요소와 span 요소로 입력값을 추가해야 한다
<div class="wrong-letters-container">
<div id="wrong-letters">
<p>wrong!</p>
<span>x</span>
<span>y</span>
<span>z</span>
</div>
</div>
행맨을 그릴 때는 display속성을 block으로 해준다
행맨이 다 그려지는 횟수까지 진행됐다면 그 판은 실패한 것이다
<div class="popup-container" id="popup-container">
<div class="popup">
<h2 id="final-message"></h2>
<h3 id="final-message-reveal-word"></h3>
<button id="play-button">Play Again</button>
</div>
</div>
이번 과제의 주요 구현 사항 입니다
const correct = document.getElementById('word') as HTMLDivElement;
const wrong = document.getElementById('wrong-letters') as HTMLDivElement;
const duplication = document.getElementById('notification-container') as HTMLDivElement;
const popup = document.getElementById('popup-container') as HTMLDivElement;
const playAgainButton = document.getElementById('play-button') as HTMLButtonElement;
const finalMessage = document.getElementById('final-message') as HTMLElement;
const finalMessageRevealWord = document.getElementById('final-message-reveal-word') as HTMLElement;
const hangman = Array.from(document.querySelectorAll('.figure-part')) as HTMLElement[];
이번 로직에 사용될 DOM 요소들을 가져옵니다
여기서 행맨 그림을 그리는데 사용되는 .figure-part 의 경우, 여러개의 요소들을 가져와야 하므로
querySelectorAll로 가져오고 행맨 그림을 그릴 때, 이 요소들을 순회해야 합니다
TS에서 querySelectorAll() 을 사용하면 반환 타입이 NodeList가 되는데 NodeList형태는
Array.from을 통해서 일반 배열로 변환이 가능하므로 이 방법을 사용합니다
type answerState = {
stateName: 'answerState';
words: string[];
};
type wrongState = {
stateName: 'wrongState';
words: string[];
};
type playableState = {
stateName: 'playableState';
isPlayable: boolean;
};
type stateType = answerState | wrongState | playableState;
이번 로직에서 사용되는 상태값은 3개 입니다
이번에도 지난주와 마찬가지로 타입에 따른 리렌더링 로직을 사용하기 때문에
discriminate union 으로 공통 키값인 stateName을 사용합니다
자세한 설명은 바닐라로 useState 구현해보기
const useState = <T extends stateType>(status: T): [() => T, (state: T) => void] => {
let initialState = status;
const state = () => initialState as T;
const setState = (newState: T) => {
initialState = newState;
// playableState상태가 변경 됐을 때는 리렌더링을 안 함
if (newState.stateName === 'answerState') {
answerRender();
}
if (newState.stateName === 'wrongState') {
wrongRender();
}
};
return [state, setState];
};
지난주와 마찬가지로 useState를 사용하고 타입에 따른 리렌더링 로직을 구분 합니다
다만 게임이 끝났는지를 알려주는 상태인 playableState상태가 변경됐을 경우에는
리렌더링을 진행하지 않습니다
const [getAnswerState, setAnswerState] = useState({
stateName: 'answerState',
words: [],
} as answerState);
const [getwrongState, setwrongState] = useState({
stateName: 'wrongState',
words: [],
} as wrongState);
const [getPlayableState, setPlayableState] = useState({
stateName: 'playableState',
isPlayable: true,
} as playableState);
const getRandomAnswer = () : string => {
const words = ['application', 'programming', 'interface', 'wizard'];
let randomIndex = Math.floor(Math.random() * words.length);
return words[randomIndex];
};
정답을 지정해두고 랜덤으로 0~3 사이의 인덱스를 생성해서 해당 단어를 정답으로 반환합니다
const answerRender = () : void => {
const current : string [] = getAnswerState().words;
correct.innerHTML = '';
const compareResult = randomAnswer.split('').map((i) => {
return current.includes(i) ? i : '';
});
for (let i of compareResult) {
correct.innerHTML += `<span class="letter">${i}</span>`;
}
let check = new Set(randomAnswer);
if (current.length === check.size) {
finalMessage.innerText = 'Congratulations! You won! 😃';
finalMessageRevealWord.innerText = '';
popup.style.display = 'flex';
setPlayableState({
stateName: 'playableState',
isPlayable: false,
});
}
};
💡 상태값을 기반으로 동작하므로 추후 게임을 초기화 할 때도 동일하게 재사용 합니다
['', 'r', '', '', 'r', '', 'm', 'm', 'i', '', '']
progamin (8글자) === progamin (8글자)
그러므로 조건문을 사용해서 정답이 된다면 정답 관련된 로직을 진행하고
playableState상태를 false로 바꿉니다
const wrongRender = () : void => {
const current : string [] = getwrongState().words;
if (current.length > 0) {
wrong.innerHTML = `<p>Wrong</p>`;
current.forEach((i) => {
wrong.innerHTML += `<span>${i}</span>`;
});
}
if (current.length === 0) {
wrong.innerHTML = '';
}
for (let i = 0; i < hangman.length; i++) {
if (i < current.length) hangman[i].style.display = 'block';
if (i >= current.length) hangman[i].style.display = 'none';
}
if (current.length === hangman.length) {
finalMessage.innerText = 'Unfortunately you lost. 😕';
finalMessageRevealWord.innerText = `...the word was: ${randomAnswer}`;
popup.style.display = 'flex';
setPlayableState({
stateName: 'playableState',
isPlayable: false,
});
return;
}
};
💡 상태값을 기반으로 동작하므로 추후 게임을 초기화 할 때도 동일하게 재사용 합니다
const showDuplication = () : void => {
duplication.classList.add('show');
setTimeout(() => {
duplication.classList.remove('show');
}, 2000);
};
const keyboardEvent = (e: KeyboardEvent) : void => {
if (!getPlayableState().isPlayable) return;
let currentAnswer = getAnswerState().words;
let currentWrong = getwrongState().words;
let keyboardInput = e.key.toLowerCase();
if (randomAnswer.includes(keyboardInput)) {
// 중복처리
if (!currentAnswer.includes(keyboardInput)) {
currentAnswer.push(keyboardInput);
setAnswerState({
stateName: 'answerState',
words: currentAnswer,
});
return;
}
if (currentAnswer.includes(keyboardInput)) {
showDuplication();
return;
}
}
if (!randomAnswer.includes(keyboardInput)) {
// 중복처리
if (!currentWrong.includes(keyboardInput)) {
currentWrong.push(keyboardInput);
setwrongState({
stateName: 'wrongState',
words: currentWrong,
});
return;
}
if (currentWrong.includes(keyboardInput)) {
showDuplication();
return;
}
}
};
const reStart = () : void => {
// 정답 재생성
randomAnswer = getRandomAnswer();
console.log(randomAnswer);
// 팝업창 지우기
popup.style.display = 'none';
// 모든 상태 초기화 및 초기화된 값을 기반으로 리렌더링
setPlayableState({
stateName: 'playableState',
isPlayable: true,
} as playableState);
setAnswerState({
stateName: 'answerState',
words: [],
} as answerState);
setwrongState({
stateName: 'wrongState',
words: [],
} as wrongState);
};