ai-creator

[파이썬 간단한 게임 만들기] 10. 테트리스(Tetris) - 3탄. 구현 본문

유치한 게임

[파이썬 간단한 게임 만들기] 10. 테트리스(Tetris) - 3탄. 구현

ai-creator 2021. 5. 22. 14:20
반응형

유치한게임 에 오신 것을 환영합니다.

오늘은 [테트리스] 입니다.

오늘은 아래와 같은 순서로 배워보겠습니다.

 

1. 학습 목표

2. 사전 준비

3. 사전지식

4. 구현

5. 요약정리

ㅁ 참고 자료

 


1. 학습 목표

테트리스의 경우 제공된 소스코드에 기능을 추가함으로써 완성해가도록 하겠습니다.

runGame()의 동작은 모두 넣어두었고, TODO 및 비어있는 함수를  구현하여 완성해 갑니다.

 

2. 사전 준비

해당 코드는 아래 링크에서 다운로드 받을 수 있습니다.

https://drive.google.com/drive/u/0/folders/13f9L1ZhGDUAqndoz1mdSOXupV6BsO67k

 

11. 테트리스 - Google Drive

이 폴더에 파일이 없습니다.이 폴더에 파일을 추가하려면 로그인하세요.

drive.google.com

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
""" tetris.py - Copyright 2016 Kenichiro Tanaka """
import sys
from math import sqrt
from random import randint
import pygame
 
# 전역 변수
pygame.init()
smallfont = pygame.font.SysFont(None36)
largefont = pygame.font.SysFont(None72)
 
BLACK = (0,0,0)
pygame.key.set_repeat(3030)
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
WIDTH = 12
HEIGHT = 22
INTERVAL = 40
# TODO : FILED값을 채운다.
FIELD = []
COLORS = ((000), (2551650), (00255), (0255255), \
          (02550), (2550255), (2552550), (25500), (128128128))
BLOCK = None
NEXT_BLOCK = None
PIECE_SIZE = 24 # 24 x 24
PIECE_GRID_SIZE = PIECE_SIZE+1 
BLOCK_DATA = (
    (
        (001, \
         111, \
         000),
        (010, \
         010, \
         011),
        (000, \
         111, \
         100),
        (110, \
         010, \
         010),
    ), (
        (200, \
         222, \
         000),
        (022, \
         020, \
         020),
        (000, \
         222, \
         002),
        (020, \
         020, \
         220)
    ), (
        (030, \
         333, \
         000),
        (030, \
         033, \
         030),
        (000, \
         333, \
         030),
        (030, \
         330, \
         030)
    ), (
        (440, \
         044, \
         000),
        (004, \
         044, \
         040),
        (000, \
         440, \
         044),
        (040, \
         440, \
         400)
    ), (
        (055, \
         550, \
         000),
        (050, \
         055, \
         005),
        (000, \
         055, \
         550),
        (500, \
         550, \
         050)
    ), (
        (66, \
        66),
        (66, \
        66),
        (66, \
        66),
        (66, \
        66)
    ), (
        (0700, \
         0700, \
         0700, \
         0700),
        (0000, \
         7777, \
         0000, \
         0000),
        (0070, \
         0070, \
         0070, \
         0070),
        (0000, \
         0000, \
         7777, \
         0000)
    )
)
 
class Block:
    """ 블록 객체 """
    def __init__(self, count):
        self.turn = 0 # TODO : 다양한 모양이 나오게 변경하기 
        self.type = BLOCK_DATA[0# TODO : 다양한 모양이 나오게 변경하기 
        self.data = self.type[self.turn]
        self.size = int(sqrt(len(self.data)))
        self.xpos = randint(28 - self.size)
        self.ypos = 1 - self.size
        self.fire = count + INTERVAL
 
    def update(self, count):
        """ 블록 상태 갱신 (소거한 단의 수를 반환한다) """
        # 아래로 충돌?
        erased = 0
        if is_overlapped(self.xpos, self.ypos + 1self.turn):
            for y_offset in range(BLOCK.size):
                for x_offset in range(BLOCK.size):
                    index = y_offset * self.size + x_offset
                    val = BLOCK.data[index]
                    if 0 <= self.ypos+y_offset < HEIGHT and \
                       0 <= self.xpos+x_offset < WIDTH and val != 0:
                            FIELD[self.ypos+y_offset][self.xpos+x_offset] = val ## 값을 채우고, erase_line()을 통해 삭제되도록 한다.
 
            erased = erase_line()
            go_next_block(count)
 
        if self.fire < count:
            self.fire = count + INTERVAL
            self.ypos += 1
        return erased
 
    def draw(self):
        """ 블록을 그린다 """
        pass
 
def erase_line():
    """ TODO : 행이 모두 찬 단을 지운다. 그리고, 소거한 단의 수를 반환한다 """
    erased = 0
    return erased
 
def is_game_over():
    """ TODO : 게임 오버인지 아닌지 """
    pass
 
def go_next_block(count):
    """ 블록을 생성하고, 다음 블록으로 전환한다 """
    global BLOCK, NEXT_BLOCK
    BLOCK = NEXT_BLOCK if NEXT_BLOCK != None else Block(count)
    NEXT_BLOCK = Block(count)
 
def is_overlapped(xpos, ypos, turn):
    """ TODO : 블록이 벽이나 땅의 블록과 충돌하는지 아닌지 """
    pass
 
def set_game_field():
    """ TODO : 필드 구성을 위해 FIELD 값을 세팅한다. """
    pass
 
def draw_game_field():
    """ TODO : 필드를 그린다 """
    pass
 
def draw_current_block():
    """ TODO : 현재 블록을 그린다 """
    pass
 
def draw_next_block():
    """ TODO : 다음 블록을 그린다 """
    pass
 
def draw_score(score):
    """ TODO : 점수를 표시한다. """
    pass
 
def draw_gameover_message():
    """ TODO : 'Game Over' 문구를 표시한다 """
    pass
 
def runGame():
    """ 메인 루틴 """
    global INTERVAL
    count = 0
    score = 0
    game_over = False
    
    go_next_block(INTERVAL)
 
    set_game_field()
 
    while True:
        clock.tick(10)
        screen.fill(BLACK)
 
        key = None
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                key = event.key
            elif event.type == pygame.KEYUP:
                key = None
 
        game_over = is_game_over()
        if not game_over:
            count += 5
            if count % 1000 == 0:
                INTERVAL = max(1, INTERVAL - 2)
            erased = BLOCK.update(count)
 
            if erased > 0:
                score += (2 ** erased) * 100
 
            # 키 이벤트 처리
            next_x, next_y, next_t = \
                BLOCK.xpos, BLOCK.ypos, BLOCK.turn
            if key == pygame.K_UP:
                next_t = (next_t + 1) % 4
            elif key == pygame.K_RIGHT:
                next_x += 1
            elif key == pygame.K_LEFT:
                next_x -= 1
            elif key == pygame.K_DOWN:
                next_y += 1
 
            if not is_overlapped(next_x, next_y, next_t):
                BLOCK.xpos = next_x
                BLOCK.ypos = next_y
                BLOCK.turn = next_t
                BLOCK.data = BLOCK.type[BLOCK.turn]
 
        # 게임필드 그리기
        draw_game_field()
 
        # 낙하 중인 블록 그리기
        draw_current_block()
 
        # 다음 블록 그리기
        draw_next_block()
        
        # 점수 나타내기
        draw_score(score)
        
        # 게임 오버 메시지 
        if game_over:
            draw_gameover_message()
 
        pygame.display.update()
        
 
runGame()
pygame.quit()
cs

 

위 코드를 실행하면, 검은색 게임판만 보이죠? 코드가 276라인이나 되는데 화면에 보이는건 아무것도 없네요 ㅠㅠ

코드설명

1) 전역변수

- 14~15라인 : 전체 게임판의 사이즈를 의미하는 SCREEN_WIDTH와 SCREEN_HEIGHT입니다.

- 18라인 : WIDTH은 FIELD를 구성하는 PIECE의 열개수를 의미합니다.

- 19라인 : HEIGHT는 IELD를 구성하는 PIECE 조각의 행개수를 의미합니다.

- 20라인 : INTERVAL는 낙하 간격

- 22라인 : FILED는 테트리스 게임이 이뤄지는 판을 의미하며, 블록들의 상태를 유지하는 공간이기도 합니다. 

- 23~24라인 : COLORS : 색을 의미하는 배열

- 25라인 : BLOCK은 게임 중인 현재 블록 객체

- 29라인~122라인 : NEXT_BLOCK은 다음 블록 객체

 

2) Block class

Block 객체를 생성할 수 있는 class입니다.

현 소스에서는 2개의 Block이 생성되죠? 바로, BLOCK과 NEXT_BLOCK입니다.

- 126~133라인 : 생성시 세팅되는 객체 변수들입니다. 상세한 설명은 <Step2>에서 합니다.

 

 

이제 점진적으로 코드를 완성해가면서 나만의 테트리스 게임을 만들어보아요~

 

3. 사전 지식

테트리스는 생각보다 복잡한 게임입니다. 아래내용을 먼저 학습하고 오시면, 구현하기 훨씬 수월하답니다.

https://ai-creator.tistory.com/557?category=807420 

 

[파이썬 간단한 게임 만들기] 10.테트리스(Tetris) - 1탄. 게임 구성 요소 (설명)

테트리스 게임을 완성해봅니다. 재밌게 즐겼던 테트리스 게임을 직접 만들어 본다 생각하니 설레이시죠? 게임을 만들어보면 느끼시겠지만, 생각보다 복잡한 과정들을 거칩니다. 중도에 포기하

ai-creator.tistory.com

 

4. 구현

아래를 단계별로 구현하면서 게임을 완성합니다.

 

Step1) 게임 필드 표현하기
Step2) BLOCK 표현하기
Step3) BLOCK 과 FIELD 경계 처리
Step4) NEXT_BLOCK 표현하기
Step5) 점수
Step6) 게임 종료
Step7) 랜덤하게 블록 모양 변경하기

 

자 그럼, 이제 첫 단계부터 천천히 해볼까요?

 

Step1) 게임 필드 표현하기

구현 목표
- FIELD = [] 
- set_game_field() 
- draw_game_field()

두 함수를 완성하면, 아래와 같이 표현이 됩니다.

Step1) 구현 완료시 게임판 모습


구현해야 할 함수와 자료구조는 설명입니다.

- FIELD = [] : PIECE들로 구성된 자료구조

- set_game_field() : FIELD의 값을 설정함

- draw_game_field() : 설정된 FIELD를 화면에 출력

 

아직도 막막하시죠? 이를 구현하기 위해 필요한 팁들을 설명드리겠습니다.

 

구현팁1 - set_game_field() : FIELD

FIELD의 시작위치와 색상에 대한 설명입니다.

  시작위치 색상
FIELD 25x25 (128, 128, 128)
(0, 0, 0)

이전 설명(Link)에도 언급하였지만,

- FIELD는 하나의 작은 조각(이하 PIECE)들의 모음이며,

- PIECE_SIZE는 24이고,

- PIECE사이에 공백을 두어, GRID표현을 합니다.

 

이를 소스 코드에서 살펴보면

FIELD는 하나의 작은 조각(이하 PIECE)들의 모음 PIECE들이 WIDTH = 12, HEIGHT = 22개로 구성
PIECE_SIZE는 24 PIECE_SIZE = 24
PIECE사이에 공백을 두어, GRID표현 PIECE_GRID_SIZE = PIECE_SIZE+1 

 

구현팁2 - set_game_field() : PIECE

각각의 PIECE는 약속된 값으로 채워집니다.

약속된 값은 

- 색

- 유효성

을 의미하게 됩니다.

여기서 (128, 128, 128) 또는 (0, 0, 0)이죠. 즉,COLORS[0], COLORS[8]로 접근을 할 수 있도록 값이 채워져야 합니다.

자, 그럼 어떤 자료구조를 사용해서 표현하면 좋을까요?

 

구현팁3 - draw_game_field() 

FIELD를 구성하는 PIECE들을 pygame.draw.rect()를 호출하여 표현합니다.

 

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def set_game_field():
    """ TODO : 필드 구성을 위해 FIELD 값을 세팅한다. """
    for i in range(HEIGHT-1):
        FIELD.insert(0, [8, 0,0,0,0,0,0,0,0,0,0 ,8])
    
    FIELD.insert(HEIGHT-1, [8, 8,8,8,8,8,8,8,8,8,8 ,8])
    #print(FIELD)
    
    
 def draw_game_field():
    """ TODO : 필드를 그린다 """
    for y_offset in range(HEIGHT):
        for x_offset in range(WIDTH):
            val = FIELD[y_offset][x_offset]
            color = COLORS[val]
            pygame.draw.rect(screen, 
                            color,
                            (PIECE_GRID_SIZE + x_offset*PIECE_GRID_SIZE, 
                            PIECE_GRID_SIZE + y_offset*PIECE_GRID_SIZE , 
                            PIECE_SIZE , 
                            PIECE_SIZE ))
    #pass

 


Step2) BLOCK 표현하기

구현 목표
- Block class의 draw()
- draw_current_block()

두 함수를 완성하면, 아래와 같이 화면에 표현 됩니다. 해당 도형이 천천히 아래로 내려옵니다. 상/하/좌/우 방향키를 누르면 모양이 바뀌고, 위치도 이동할 수 있습니다.

Step2) 구현 완료시 게임판 모습


블록은 2가지가 표현되는데요.

현재 게임에 참여하는 블록과 다음게임에 참여 하는 블록이 있습니다.

이를 소스 코드에서 살펴보면

현재 게임에 참여하는 블록 BLOCK = None
다음게임에 참여 하는 블록 NEXT_BLOCK = None

 

BLOCK과 관련된 코드를 완성해 봅시다.

 

- Block class의 draw() : Block class로 관리되는 정보를 사용하여, 화면에 표현합니다.

- draw_current_block() : 현재 게임에 참여하는 블록을 화면에 표현합니다.

 

구현팁1. Block class의 draw()

draw() 설명에 앞서 Blcok class를 이해하면 좋겠네요.

<2. 사전준비>에 공유된 소스코드의 124~158라인에 해당됩니다. 그 부분만 발췌했습니다.

class Block:
    """ 블록 객체 """
    def __init__(self, count):
        self.turn = 0 # TODO : 다양한 모양이 나오게 변경하기 
        self.type = BLOCK_DATA[0] # TODO : 다양한 모양이 나오게 변경하기 
        self.data = self.type[self.turn]
        self.size = int(sqrt(len(self.data)))
        self.xpos = randint(2, 8 - self.size)
        self.ypos = 1 - self.size
        self.fire = count + INTERVAL

    def update(self, count):
        """ 블록 상태 갱신 (소거한 단의 수를 반환한다) """
        # 아래로 충돌?
        erased = 0
        if is_overlapped(self.xpos, self.ypos + 1, self.turn):
            for y_offset in range(BLOCK.size):
                for x_offset in range(BLOCK.size):
                    index = y_offset * self.size + x_offset
                    val = BLOCK.data[index]
                    if 0 <= self.ypos+y_offset < HEIGHT and \
                       0 <= self.xpos+x_offset < WIDTH and val != 0:
                            FIELD[self.ypos+y_offset][self.xpos+x_offset] = val ## 값을 채우고, erase_line()을 통해 삭제되도록 한다.

            erased = erase_line()
            go_next_block(count)

        if self.fire < count:
            self.fire = count + INTERVAL
            self.ypos += 1
        return erased

    def draw(self):
        """ 블록을 그린다 """
        pass

1) __init__()

낙하중인 블록을 떠올려 보면 어떤 속성들이 필요할까요?

블록의 모양, 방향, 좌표가 필수적으로 필요하죠. 또 재미요소를 추가할 수 있습니다. 낙하 시작 시간을 설정하여, 일정 시간이 흐르면 블록이 자동으로 이동되고, 단계가 올라갈수록 내려오는 속도를 빨리하는것이죠. 

이런 속성들로 Block class를 만들어 봅니다.

 

Block class로 객체를 생성하면 관리되는 정보입니다.

- trun : 회전을 관리하는 정보

- type : 7가지 블록 모양에 따른 2차원 데이터 

- data : 블록을 구성하는 1차원 데이터  

- size : 블록 크기

- xpos, ypos : 블록의 x, y좌표

- fire : 낙하 시작 시간 (추후 설명)

 

이전 설명(Link)에도 언급한 설명을 상기해보죠.

총 7가지 종류의 블록이 있고, 각각의 회전을 합니다.

이를 3차원의 BLOCK_DATA 로 관리하죠. 각 차원마다 아래와 같은 의미를 지닙니다.

그럼 현재 코드에서 의미하는 블록은 어떤 모양일까요?

- 블록종류 : 0

- 블록방향 : 0

을 가진?

요 도형이겠죠? 

그럼 1차원의 아래와 같은 정보가 self.data에 담기게 됩니다. 

(0, 0, 1, \
 1, 1, 1, \
 0, 0, 0),

즉, 정리를 해보면

만약, BLOCK_DATA[0][0]이라면, 1차원의 (0, 0, 1, 1, 1, 1, 0, 0, 0) 이 된다.

이때 주의할 점은 1차원이라는 점이다.

 

객체의 변수인 xpos, ypos, turn, data가 변경되는 곳을 찾아보세요.

<2. 사전준비>에 공유된 소스코드의 238~254라인에 해당됩니다. 그 부분만 발췌했습니다.

            # 키 이벤트 처리
            next_x, next_y, next_t = \
                BLOCK.xpos, BLOCK.ypos, BLOCK.turn
            if key == pygame.K_UP:
                next_t = (next_t + 1) % 4
            elif key == pygame.K_RIGHT:
                next_x += 1
            elif key == pygame.K_LEFT:
                next_x -= 1
            elif key == pygame.K_DOWN:
                next_y += 1

            if not is_overlapped(next_x, next_y, next_t):
                BLOCK.xpos = next_x
                BLOCK.ypos = next_y
                BLOCK.turn = next_t
                BLOCK.data = BLOCK.type[BLOCK.turn]

즉, 키 이벤트로 위치이동이나 블럭회전이 발생하면 변경되네요.

 

fire가 변경되는 곳을 찾아보세요.

2) update() 

<2. 사전준비>에 공유된 소스코드의 135~154라인에 해당됩니다. 그 부분만 발췌했습니다.

    def update(self, count):
        """ 블록 상태 갱신 (소거한 단의 수를 반환한다) """
        # 아래로 충돌?
        erased = 0
        if is_overlapped(self.xpos, self.ypos + 1, self.turn):
            for y_offset in range(BLOCK.size):
                for x_offset in range(BLOCK.size):
                    index = y_offset * self.size + x_offset
                    val = BLOCK.data[index]
                    if 0 <= self.ypos+y_offset < HEIGHT and \
                       0 <= self.xpos+x_offset < WIDTH and val != 0:
                            FIELD[self.ypos+y_offset][self.xpos+x_offset] = val ## 값을 채우고, erase_line()을 통해 삭제되도록 한다.

            erased = erase_line()
            go_next_block(count)

        if self.fire < count:
            self.fire = count + INTERVAL
            self.ypos += 1
        return erased

 

- 블록 상태를 FIELD에 갱신하는 역할을 합니다. FIELD는 현재 테트리스의 상태 = 블록이 쌓여 있는 상태를 기억하게 됩니다. 기억된 FIELD는 draw_game_field()를 통해서 표현되겠죠?

 

FIELD[self.ypos+y_offset][self.xpos+x_offset] = val

- is_overlapped()는 다른 블록과 충돌하면, True를 반환하는 함수입니다.

  즉, y축으로 1칸 내려 갔을때, 충돌이 발생해 더 내려갈 수 없다면, FIELD에 상태를 갱신하여, 블록이 쌓여 있는 상태를 기억합니다.

- erase_line()은 행이 모두 찬 단을 지운다. 그리고, 소거한 단의 수를 반환하는 함수입니다.

- go_next_block() 다음 블록(NEXT_BLOCK)을 현재 블록(BLOCK)으로 전환하는 역할 후, 다음 블록객체를 생성합니다.

 

update() 메소드는 하는 역할이 꽤 많습니다.

> 다른 블록과 충돌하면,

> 블록이 쌓여 있는 상태를 FIELD에 저장(기억)하고,

> 행이 모두 찬 단을 지우고

> 블록 전환 (NEXT_BLOCK -> BLOCK) 을 합니다.

 

 

3) draw()

- 해당 메소드는 낙하중인 현재 블록을 화면에 표현합니다. (현재 블록만 화면에 표현합니다.)

self.data는 1차원 데이터이므로, index정보를 활용하여 접근하고

도형의 모습은 2차원이므로 self.size 정보를 사용하여 x_offset, y_offset로 2중 for loop을 만들면서 draw.rect()를 호출하면 됩니다.

도형의 값마다 색과 유효성을 체크한다는 점도 잊지 마세요.

 

self.xpos, self.ypos를 시작점으로 하여 게임판에 표현해 봅니다.

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def draw(self):
        """ 블록을 그린다 """
        ## 블록의 조각(piece)의 데이터를 구한다.
        for y_offset in range(self.size):
            for x_offset in range(self.size):
                index = y_offset * self.size + x_offset
                val = self.data[index]
                if 0 <= y_offset + self.ypos < HEIGHT and \
                   0 <= x_offset + self.xpos < WIDTH and val != 0:
                    ## f_xpos = filed에서의 xpos를 계산함
                    f_xpos = PIECE_GRID_SIZE + (x_offset + self.xpos) * PIECE_GRID_SIZE
                    f_ypos = PIECE_GRID_SIZE + (y_offset + self.ypos) * PIECE_GRID_SIZE
                    pygame.draw.rect(screen, COLORS[val],
                                    (f_xpos, 
                                    f_ypos, 
                                    PIECE_SIZE, 
                                    PIECE_SIZE))
                                    
def draw_current_block():
    BLOCK.draw()

 


Step3) BLOCK 과 FIELD 경계 처리

구현 목표
- is_overlapped()

Step3) 구현 완료시 게임판 모습


Step2) 까지 구현을 완료했더니, 이상합니다. 어떤 점이 이상한가요?

제일 아래에 닿으면 도형이 벽을 뚫고 나가 버립니다.

 

이 문제를 해결하기 위해 다음 함수를 완성합니다.

- is_overlapped()

FIELD의 벽인 좌/우/하 세방향 모두 체크하셔야 합니다^^

힌트는 채워진 '값'을 활용하는 것입니다. '값'은 색과 유효성을 판단하는 용도지요. is_overlapped()에서는 값을 이용하여 "유효성"을 체크합니다. 

 

1) 리턴값

True/False로 구현해주세요.

 

2) input param

인풋으로 3개의 파라메터를 받습니다.

호출되는 곳을 살펴보세요. 대부분 다음으로 변경되기전, 체크를 하는 경우입니다. 그러므로, 입력값을 이용해서 (미래의) data에 유효성을 체크해야 합니다.

 

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def is_overlapped(xpos, ypos, turn):
    """ TODO : 블록이 벽이나 땅의 블록과 충돌하는지 아닌지 """
    data = BLOCK.type[turn]
    for y_offset in range(BLOCK.size):
        for x_offset in range(BLOCK.size):
            index = y_offset * BLOCK.size + x_offset
            val = data[index]

            if 0 <= xpos+x_offset < WIDTH and \
                0 <= ypos+y_offset < HEIGHT:
                if val != 0 and \
                    FIELD[ypos+y_offset][xpos+x_offset] != 0:
                    return True
    return False

 


Step4) NEXT_BLOCK 표현하기

구현 목표
draw_next_block() 

Step4) 구현 완료시 게임판 모습


게임판에는 2가지의 블록이 표현된다고 말씀드렸죠?

step2)에서는 현재 게임에 참여하는 블록을 BLOCK을 표현했습니다.

이번 스텝에서는 다음 게임에 참여하는 블록인 NEXT_BLOCK을 표현해봅시다.

 

다음 함수를 완성합니다.

- draw_next_block() 오른쪽 상단 위치에 다음 블록이 표현됩니다.

 

참고) 설계서 

  시작위치 색상
FIELD 25x25 (128, 128, 128)
(0, 0, 0)
점수 500x30 (0, 255, 0)
다음 블록 460 x 100 블록별 색상
GAME OVER 300 x 300 (0, 255, 225)

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def draw_next_block():
    """ TODO : 다음 블록을 그린다 """
    ## 블록의 조각(piece)의 데이터를 구한다.
    for y_offset in range(NEXT_BLOCK.size):
        for x_offset in range(NEXT_BLOCK.size):
            index = y_offset * NEXT_BLOCK.size + x_offset
            val = NEXT_BLOCK.data[index]
            #if 0 <= y_offset + self.ypos < HEIGHT and \
            #   0 <= x_offset + self.xpos < WIDTH and 
            if val != 0: ## 이 조건은 중요함! 0까지 그림을 그린다면, 쌓인 블록이 순간적으로 검정색이 됨.
                ## f_xpos = filed에서의 xpos를 계산함
                f_xpos = 460 + (x_offset) * PIECE_GRID_SIZE
                f_ypos = 100 + (y_offset) * PIECE_GRID_SIZE
                pygame.draw.rect(screen, COLORS[val],
                                (f_xpos, 
                                f_ypos, 
                                PIECE_SIZE, 
                                PIECE_SIZE))

 


Step5) 점수

구현 목표
- draw_score()
- erase_line()

Step5) 구현 완료시 게임판 모습


1) draw_score()

점수는 총 6자리로 표현되며, 모자란 자리수는 0으로 채워져 있습니다.

이 요구조건을 만족하기 위해서는 아래와 같이 코드를 작성합니다.

str(score).zfill(6)

 

점수를 계산하고, 게임판에 표현합니다.

설계서에 기재해둔 좌표를 확인하여 점수를 표현합니다.

 

참고) 설계서

  시작위치 색상
FIELD 25x25 (128, 128, 128)
(0, 0, 0)
점수 500x30 (0, 255, 0)
다음 블록 460 x 100 블록별 색상
GAME OVER 300 x 300 (0, 255, 225)

 

이제 점수를 계산해보겠습니다.

공유되었던 소스코드에 아래와 같은 코드가 있습니다.

erased = BLOCK.update(count)

if erased > 0:
	score += (2 ** erased) * 100

BLOCK.update()를 호출하면, 지워진 행의 수 (erased)를 전달 받습니다.

그리고, erased값으로 연산하여, 점수를 사정합니다.

그럼, 어떤 코드를 구현하면 될까요?

 

BLOCK.update()는 코드는 모두 구현이 되어 있는 상태이고, 지워진 행의 수를 전달해주는 함수가 무엇인가요?

erase_line() 죠.

 

2) erase_line()

erase_line()를 완성해봅시다.

이 때 다소 생각해봐야 할 부분이 있는데요.

"테트리스(Tetris) - 1탄. 게임 구성 요소 (설명)" 에서 배운 "4.점수" 입니다. (참고 링크)

값으로 모두 채워졌으면 => 줄 삭제 => 점수 획득가 된다고 했죠.

그리고, 1줄만 지워지는 것이 아니라 값이 모두 채워진 행이 복수개면 한번에 여러개가 지워지고 점수가 합산된다는 점 잊지 마세요~

이 내용들을 잘 상기하면서 해당 함수를 완성합니다.

 

함수를 완성하면, 한줄이 삭제되면서, 점수 획득이 됨을 알 수 있습니다.

 

FIELD에 모든 정보가 들어가 있고, FIELD 한줄의 값이 모두 0이 아니면(어떤 숫자를 가지면) 리스트를 지우고, 맨 앞에 채워넣으면 됩니다.

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def draw_score(score):
    """ TODO : 점수를 표시한다. """
    score_str = str(score).zfill(6)
    score_image = smallfont.render(score_str, True, (0, 255, 0))
    screen.blit(score_image, (500, 30))
    
def erase_line():
    """ TODO : 행이 모두 찬 단을 지운다. 그리고, 소거한 단의 수를 반환한다 """
    erased = 0
    ypos = HEIGHT-2
    print(FIELD[ypos])
    while ypos >=0:
        if  all(FIELD[ypos]) == True:
            del FIELD[ypos]
            FIELD.insert(0, [8, 0,0,0,0,0,0,0,0,0,0 ,8])
            erased += 1
        else:
            ypos -= 1
    return erased

 


Step6) 게임 종료

구현 목표
- is_game_over() 
- draw_gameover_message()

Step6) 구현 완료시 게임판 모습


이제 게임이 종료되는 조건을 확인해봅시다.

 

1) is_game_over()

사용자가 [x]를 누르지 않는한 언제 게임이 종료되죠?

그렇죠. 블록이 위로 꽉 찼을때죠.

- is_game_over() 

로 게임 종료조건을 판단합니다. True를 반환하면 게임종료인 상황이다! 라는 의미겠죠?

 

'블록이 위에 차 있다' 는 어떻게 확인하면 될까요? 가장 윗라인에 00값이 xx말고 더 있더라~는 조건은 어떨까요?

8이라는 값 2개 이외에 어떠한 숫자가 있는 경우가 되겠죠?

 

2) draw_gameover_message()

게임이 종료되면, 메시지를 출력합니다.

 

두 함수를 완성해봅시다.

 

이게 거의 끝을 향해 달려갑니다. 와우!! 자 이제 마지막으로 무엇을 해야 할까요?

1개의 블록만 나오니 이상하자나요? ㅎㅎ 이 한끗을 채워보시죠.

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기

// 비공개

 


Step7) 랜덤하게 블록 모양 변경하기

구현 목표
- Block class의 __init__()

Step6) 구현 완료시 게임판 모습


블록 모양이 다 정의되어 있기 때문에 랜덤하게 블록 모양을 선택하기만 하면 됩니다.

어디를 수정하면 될까요?

마지막으로 표기된 TODO 소스코드 입니다.

self.turn = 0 # TODO : 다양한 모양이 나오게 변경하기 
self.type = BLOCK_DATA[0] # TODO : 다양한 모양이 나오게 변경하기 

두 곳을 수정하면, 아래와 같이 나옵니다.

 

ㅁ 구현코드

꼭 스스로가 구현을 해보세요. 그후에 완성된 코드를 보면서 학습하시면 많은 도움이 될 것입니다.

고민한만큼 많이 얻어갈 것입니다~!!

더보기
def __init__(self, count):
        self.turn = randint(0,3)
        self.type = BLOCK_DATA[randint(0, 6)]
        self.data = self.type[self.turn]
        self.size = int(sqrt(len(self.data)))
        self.xpos = randint(2, 8 - self.size)
        self.ypos = 1 - self.size
        self.fire = count + INTERVAL

 

 

야호~~~~~ 

드디어 내 손으로 직접 테트리스를 만들었네요!!! 정말 대단하죠???????

긴 시간 고생많으셨습니다.

 

5. 요약정리

이 소스코드 내에는 레벨이 상승될 수록 블록이 빠르게 내려오는 부분이 포함되어 있습니다.

어느부분인지 확인하시고, 어떻게 동작하는지도 생각해보세요.

많은 도움이 될 것입니다.

 

 

ㅁ 참고 자료

- (책) 게임으로 배우는 파이썬

 


도움이 되셨다면, 좋아요 / 구독 버튼 눌러주세요~

 

저작물의 저작권은 작성자에게 있습니다.
공유는 자유롭게 하시되 댓글 남겨주세요~
상업적 용도로는 무단 사용을 금지합니다.
끝까지 읽어주셔서 감사합니다^^

 

반응형
Comments