ㅇㅇㅈ Blog

프론트엔드 수행중

0%

Game-WORDLE part.1

WORDLE 게임 만들기

  • 보드판은 총 5x5 사이즈
  • 한 타일에 알파벳 하나씩 입력할 수 있고 한 줄은 한 번의 시도이다
  • 단어를 입력하고 엔터를 누르면 타일의 색이 바뀌면서 정답에 대한 힌트를 알려주고 다음 줄로 이동

html 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- index.html -->
<body>
<div class="container">
<div class="board--wrap">
<!-- 보드 한 줄 -->
<div data-row="0" class="game-board">
<!-- 타일 한 칸 -->
<div data-cell="0" class="board__text"></div>
<div data-cell="1" class="board__text"></div>
<div data-cell="2" class="board__text"></div>
<div data-cell="3" class="board__text"></div>
<div data-cell="4" class="board__text"></div>
</div>
<div data-row="1" class="game-board">
<div data-cell="0" class="board__text"></div>
<div data-cell="1" class="board__text"></div>
<div data-cell="2" class="board__text"></div>
<div data-cell="3" class="board__text"></div>
<div data-cell="4" class="board__text"></div>
</div>
<div data-row="2" class="game-board">
<div data-cell="0" class="board__text"></div>
<div data-cell="1" class="board__text"></div>
<div data-cell="2" class="board__text"></div>
<div data-cell="3" class="board__text"></div>
<div data-cell="4" class="board__text"></div>
</div>
</div>
</div>
</body>

game-board들은 한 줄이 되고
그 안의 board__text는 각 줄의 한 칸이 된다

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* style.css */
.container {
width: 1000px;
margin: 0 auto;
padding: 40px;
display: flex;
align-items: center;
flex-direction: column;
position: relative;
}
.container .board--wrap{
margin-bottom: 200px;
}
.container .board--wrap .game-board {
display: flex;
}
.container .board--wrap .game-board .board__text {
width: 80px;
height: 80px;
margin: 2px;
border: 1px solid #ccc;
font-size: 40px;
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
}

Javascript

초기값

1
2
3
4
5
6
let word = 'APPLE' // 정답 단어

let gameOver = false;

let boardRow = 0; // 보드 줄 초기값
let tileNum = 0; // 보드 칸 초기값

각 타일에 접근하기

1
let rowArr = document.querySelectorAll('.game-board')

각 줄을 querySelectorAll로 가져온다
console.log(rowArr)로 확인 해 보면
nodeList가 가져와 진다
배열의 형태로 보이지만 배열이 아닌 유사배열이다.
그러므로 배열에서 제공하는 메서드가 안먹히는 경우가 있다

mdn문서를 찾아보니

nodelist는 array는 아니지만 forEach()를 사용하여 반복할 수 있고,
Array.from()을 사용하여 Array로 변환 할 수도 있다.
주의점은 오래된 브라우저에서는 forEach()나 Array.from()을 지원 하지 않는다

https://developer.mozilla.org/ko/docs/Web/API/NodeList

콘솔에서 요소를 클릭해보면 사용가능한 속성들이 나오는데 그중에서 자식노드에 접근할 수 있는
children 속성이다

rowArr[index번호].children[index번호] 로 각 타일에 접근이 가능해졌다!!
이 부분에 많이 막혔었다.. 줄 따로 칸 따로 받아와서 조작할려다 보니
계속 뭔가가 꼬여서 안됐었는데 줄의 요소만 받아와서 그 줄의 자식요소들에 접근하니 쉬워졌다

keyboard event

키보드를 눌렀을때 각 칸에다가 누른 키보드의 글자를 넣는다

1
2
3
document.addEventListener('keyup', event => {
console.log(event)
})


키를 눌렀다 떼면 이벤트가 발생하고 그 이벤트를 콘솔로 찍었을때 사용가능한 속성들이 나온다
그 중 keyCode를 쓸려고 적었더니 밑줄이 쫙..

mdn 문서를 찾아보니 빨간색 휴지통 모양과 함께

더 이상 사용되지 않음: 이 기능은 더 이상 권장되지 않습니다. 일부 브라우저는 여전히 이를 지원할 수 있지만 관련 웹 표준에서 이미 제거되었거나 삭제 과정에 있거나 호환성 목적으로만 유지될 수 있습니다. 사용을 피하고 가능하면 기존 코드를 업데이트하십시오. 이 페이지 하단의 호환성 표를 참조하여 결정을 내리십시오. 이 기능은 언제든지 작동을 중지할 수 있습니다.

이라는 설명이 keyCode 외에도 중지되는 속성들이 몇 개 더 있었다
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code


  • 그럼 난 무엇으로 키보드 범위를 지정할 수 있는가?
    A-Z까지의 범위만 입력받기 위해 keyCode를 사용할려고 했는데 권장하지 않는다니 다른걸 써야 했다
1
2
3
4
5
6
7
8
9
10
document.addEventListener('keyup', event => {
if(gameOver) return
// 현재 초기값으로 gameOver는 false이니 return하지 않고 밑의 로직이 실행 된다

if('a' <= event.key && 'z' >= event.key){
// 키보드 a-z까지만 눌렀을때 동작한다
console.log(event.key)
// a에서 z사이의 키보드만 출력되고 나머지는 눌러도 아무 반응 없다
}
})

keyCode가 숫자로 출력되어 더 그럴싸 하게 보일뿐 키값의 순서는 똑같았다
그냥 key 속성으로도 범위를 지정 할 수 있었다.

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('keyup', event => {
if('a' <= event.key && 'z' >= event.key){
// 키보드 a-z까지 눌렀을때만 동작한다
}
else if('Backspace' === event.key){
// backspace를 눌렀을때만 동작한다
}
else if('Enter' === event.key){
// enter를 눌렀을때만 동작한다
}
})

이제 각 키보드를 눌렀을때 동작하는 부분에
그에 알맞는 로직을 짜주면 된다..

타일에 글자 넣기

  1. 현재 줄(row)의 index를 알아낸다
  2. 현재 줄(row)의 타일(tile)의 index를 알아낸다
  3. 그 타일(tile)에 키보드로 누른 키값(event.key)을 텍스트로 넣는다
  4. 타일(tile)의 index는 정답단어의 길이를 넘지 않는다!

현재 초기값으로
boardRow = 0
tileNum = 0
word = ‘APPLE’ 인 상태
좌측 위인 0, 0에서 시작한다
만약 키를 눌러 텍스트가 입력되면 다음 칸인 0, 1로 이동한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
document.addEventListener('keyup', event => {
if('a' <= event.key && 'z' >= event.key){
// 키보드 a-z까지 눌렀을때만 동작한다
if(tileNum < word.legnth ){
// 현재 tileNum = 0 이고 word.length는 5이니 true, if문 조건에 만족해 실행 된다
// 0부터 시작해 4까지 총 5번 입력가능 5부터는 false이니 더이상 입력 되지 않는다
let currTile = rowArr[boardRow].children[tileNum]
// 글자가 적힐 실제 타일 = rowArr[0].cildren[0] 인 상태
if(currTile.textContent === ''){ // 현재 타일안이 비었으면 if문 true
currTile.textContent = event.key.toUpperCase() // 키보드로 누른 값을 대문자화해서 넣어준다
tileNum += 1; // 그리고 tileNum을 +1 해줌으로 한칸 오른쪽으로 이동 할 수 있게 한다
}
// 여기까지 작동이되면 rowArr[0].children[1]로 넘어 가게 된다
}
}
else if('Backspace' === event.key){
// backspace를 눌렀을때만 동작한다
}
else if('Enter' === event.key){
// enter를 눌렀을때만 동작한다
}
})

타일에 글자 지우기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
document.addEventListener('keyup', event => {
if('a' <= event.key && 'z' >= event.key){
// 키보드 a-z까지 눌렀을때만 동작한다
}
else if('Backspace' === event.key){
// backspace를 눌렀을때만 동작한다

if(tileNum > 0) {
// 처음 0,0에서 시작할때는 tileNum도 0이므로 false
// 첫 글자를 입력하고 나면 tileNum이 +1 이 되므로 한 글자당 +1, 총 5가 될 수 있다

tileNum -= 1;
let currTile = rowArr[boardRow].children[tileNum]
// 이 부분이 순서가 중요하다
currTile.textContent = ''
}
}
else if('Enter' === event.key){
// enter를 눌렀을때만 동작한다
}
})

tileNum -= 1; 과
let currTile = rowArr[boardRow].children[tileNum] 의 순서가 중요
타일에 5글자를 꽉 채웠을 경우 tileNum은 5가 된다
근데 nodeList로 받아온 children[tileNum]의 tileNum 부부은 인덱스 번호이기 때문에 5번째 칸의 인덱스 번호가 4이다!
그렇기 때문에 -1 을 먼저 해주고 현재 타일의 텍스트를 비워주는게 중요하다

다음 줄(row)로 넘어가기

엔터를 누르면 boardRow가 +1 이 되고 tileNum = 0이 되어야 한다
좌표가 1,0 에서 시작 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.addEventListener('keyup', event => {
if('a' <= event.key && 'z' >= event.key){
// 키보드 a-z까지 눌렀을때만 동작한다
}
else if('Backspace' === event.key){
// backspace를 눌렀을때만 동작한다
}
else if('Enter' === event.key && tileNum === word.length){
// enter를 눌렀을때만 동작한다
// 2~3글자만 누르고 엔터를 누를 경우도 있으니 조건에 추가해준다
// tileNum의 숫자가 word의 단어길이와 같을때만 조건 충족
gameCheck()
//gmaeCheck 함수는 정답여부 체크 따로 작성한다
boardRow += 1;
tileNum = 0;
}
})

정답여부 체크

  • 각 타일에 적힌 글자를 조회해 정답 글자와 비교한다
  • 타일에 적힌 글자가 정답에 포함되어 있으며, 순서까지 같으면 초록색
  • 타일에 적힌 글자가 정답에 포함만 되어있으면 노란색
  • 아무것도 일치 하지 않으면 회색
  • 각 색상은 클래스 추가로 변경 해준다
  • 클래스는 .board__text와 일치선택자로 작성하는게 중요 (띄어쓰기하면 자식 선택자가 된다)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .container .board--wrap .game-board .board__text.good{
    background-color: #22c566;
    }
    .container .board--wrap .game-board .board__text.littlegood{
    background-color: #c0c522;
    }
    .container .board--wrap .game-board .board__text.bad{
    background-color: #ccc;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    함수 생성
    function gameCheck(){
    let wordCheck = 0;
    // 초록색 글자를 체크할 변수 wordCheck가 5가 되면 한 줄이 다 정답인셈
    for(let i = 0; i < word.length; i++){
    //정답단어의 길이만큼 반복한다 0부터 시작이니 0~4 총 5번 반복
    let currTile = rowArr[boardRow].children[i]
    // 현재 타일을 조회한다
    // 엔터를 한 번 쳤다면 현재 타일은 rowArr[0].children[0~4] 번째
    // 즉, n번째 줄의 i만큼 반복하는 0~4번째 인덱스의 타일을 조회 한다.
    let answer = currTile.textContent;
    // answer는 조회하는 타일 안의 text
    if(answer === word[i]){
    // answer가 맞는 자리에 있으면
    // 그리고 정답갯수 +1
    currTile.classList.add('good');
    wordCheck += 1;
    }
    else if(word.includes(answer)){
    // 정답단어 안에 answer가 포함되어 있는 경우
    currTile.classList.add('littelgood')
    }
    else {
    // 그리고 아무것도 해당 되지 않는 경우
    currTile.classList.add('bad')
    }

    // 게임 성공 여부
    if(wordCheck === word.length){
    // 정답 갯수와 정답단어의 길이가 같으면 gameOver가 true!
    gameOver = true;
    }
    }
    }

모달창 띄우기

  • 정답 여부를 체크해서 정답/실패 모달창을 띄워준다
  • gameCheck()함수 안의 게임 성공 여부에 추가 작성한다
1
2
3
<!-- index.html -->
<div class="popup">
</div>

index.html에 popup 자리 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* style.css */
.popup {
width: 300px;
height: 300px;
background-color: rgba(255, 255, 255, 0.8);
position: absolute;
top: 0;
bottom: 0;
margin: auto;
border: 1px solid rgb(226, 226, 226);
border-radius: 9px;
display: none;
font-size: 40px;
}
.popup .popup--btn{
width: 80px;
height: 40px;
border: none;
background-color: rgb(48, 48, 48);
color: white;
border-radius: 5px;
}
.popup .popup--btn:active{
box-shadow: 3px 3px 0 rgba(0,0,0,0.5);
transform: scale(0.9);
}
.popup.display-flex {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}

현재 모달창은 display:none 상태이다
이후에 정답/실패를 해서 창이 생기게 하기 위해 일치 선택자로 .display–flex를 추가 작성한다

  1. 몇 번째 줄이든 성공시 정답 모달창이 뜬다
  2. 실패시에는 마지막 줄까지 가서 실패 모달창이 떠야 한다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    function gameCheck(){
    // ...

    // 게임 성공 여부
    if(wordCheck === word.length){
    gameOver = true; // 성공시 종료

    // modal 생성
    let modal = document.querySelector('.popup')
    modal.innerHTML = `
    정답
    <button class="popup--btn">Reset</button>
    `
    model.classList.add('.display--flex')
    // .display--flex 클래스가 붙으면서 display:none 이 flex로 바뀌며 나타난다

    // btn reload
    const resetBtn = document.querySelector('.popup--btn')
    resetBtn.addEventListener('click',()=>{
    window.location.reload()
    // 버튼을 눌렀을 경우 창을 다시 새로고침 해주자......
    })
    }
    // 실패 여부
    else if(wordCheck < word.legnth && rowArr[boardRow].dataset.row === '4'){
    // 정답갯수가 단어의 길이보다 작을경우
    // wordCheck < word.legnth만 넣을 경우 첫 줄에서 실패 할시 바로 실패 모달창이 떠버린다..
    // 마지막줄까지 가서 체크하는 조건문을 추가해줘야 한다
    // 다른 방법도 있겠지만.. 잘 모르겠어서 dataset을 이용했다
    // 마지막줄의 dataset.row는 4(문자열)이므로 마지막줄까지 작성해야 조건문이 만족된다
    gameOver = true; // 게임종료

    // modal 생성
    let modal = document.querySelector('.popup')
    modal.innerHTML = `
    실패
    <button class="popup--btn">reset</button>
    `
    modal.classList.add('display-flex')

    // btn reload
    const resetBtn = document.querySelector('.popup--btn')
    resetBtn.addEventListener('click',()=>{
    window.location.reload()
    })
    }
    }

dataset

1
<div data-row="0" class="game-board"> </div>

각 줄(row)에는 data-row가 0부터4까지 들어가있다
콘솔로 조회해 보면 0~4 문자열이 출력된다.

1
console.log(rowArr[boardRow].dataset.row)


button 추가는 part.2