*이번 시간은 설계를 제대로 하지 않은 업보를 확인하는 시간입니다. 전체적인 설계 없이 4(Draw rect) ~ 8(Field)까지 기능 구현만 집중했더니 Refactoring이 얼마나 힘들어 졌는지 확인해 보세요.
이번 글에서는 Refactoring을 진행해보도록 하겠습니다. Refactoring을 할 코드는 아래와 같습니다.
* Tetris 객체 안에서 요소들이 혼잡한 형태라는 것만 파악하셔도 충분합니다.
import pygame as pg
from typing import List
from copy import deepcopy
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class Tetris:
DROP_EVENT = pg.USEREVENT + 1
EMPTY = "E"
I_MINO = "I"
O_MINO = "O"
S_MINO = "S"
Z_MINO = "Z"
L_MINO = "L"
J_MINO = "J"
T_MINO = "T"
def __init__(self, width: int, height: int,
field_w:int=10, field_h:int=20, tile_size:int=10, h_padding:int=None):
self._size = self.width, self.height = width, height
self._field_w, self._field_h, self._t_size = field_w, field_h, tile_size
self._h_padding = field_h if h_padding is None else h_padding
self._field = [[self.EMPTY for _ in range(self._field_w)] for _ in range(self._field_h + self._h_padding)]
self._mino_dict = {
self.I_MINO: Mino(self.I_MINO, [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
self.O_MINO: Mino(self.O_MINO, [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], (166, 166, 81)),
self.S_MINO: Mino(self.S_MINO, [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (177, 86, 90)),
self.Z_MINO: Mino(self.Z_MINO, [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (143, 173, 60)),
self.L_MINO: Mino(self.L_MINO, [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (73, 94, 158)),
self.J_MINO: Mino(self.J_MINO, [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (202, 121, 65)),
self.T_MINO: Mino(self.T_MINO, [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], (183, 92, 165))
}
self._display_surf = None
self._running = False
self._current_mino = None
self._drop_delay_status = False
self._color_map = dict()
def _init(self):
pg.init()
self._display_surf = pg.display.set_mode(self._size)
self._draw_field_rect_list()
self._running = True
self._current_mino = self._set_mino_init_position(self.I_MINO)
def _set_mino_init_position(self, key):
curr_mino = deepcopy(self._mino_dict[key])
for b in curr_mino.blocks:
b[0] += self._field_w//2
return curr_mino
def _draw_field_rect_list(self):
for w in range(self._field_w):
for h in range(self._field_h):
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
if self._field[h+self._h_padding][w] == self.EMPTY:
pg.draw.rect(self._display_surf, (255, 255, 255), _new_rect, 1)
else:
color = self._mino_dict[self._field[h+self._h_padding][w]].color
pg.draw.rect(self._display_surf, color, _new_rect)
def _update_field(self):
for b in self._current_mino.blocks:
self._field[b[1] + self._h_padding][b[0]] = self._current_mino.name
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self._move( 0,-1)
elif event.key == pg.K_DOWN:
self._move( 0, 1)
elif event.key == pg.K_RIGHT:
self._move( 1, 0)
elif event.key == pg.K_LEFT:
self._move(-1, 0)
elif event.type == self.DROP_EVENT:
self._update_field()
self._current_mino = self._set_mino_init_position(self.I_MINO)
def _system(self):
if self._enable_down():
self._drop_delay_status = False
pg.time.set_timer(self.DROP_EVENT, 0)
elif not self._drop_delay_status:
self._drop_delay_status = True
pg.time.set_timer(self.DROP_EVENT, 500)
def _enable_down(self):
is_enable = True
for b in self._current_mino.blocks:
if b[1] + 1 >= self._field_h or self._field[b[1] + 1 + self._h_padding][b[0]] != self.EMPTY :
is_enable = False
break;
return is_enable
def _update(self):
self._display_surf.fill((0, 0, 0))
self._draw_field_rect_list()
for b in self._current_mino.blocks:
_rect = pg.Rect(b[0]*self._t_size, b[1]*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self._display_surf, (self._current_mino.color), _rect)
pg.display.flip()
def _move(self, x, y):
move_enable = True
for block in self._current_mino.blocks:
if 0 > block[0] + x or block[0] + x >= self._field_w or block[1] + y >= self._field_h:
move_enable = False
break;
if not move_enable:
return False
for block in self._current_mino.blocks:
block[0] += x
block[1] += y
def _quit(self):
pg.quit()
def execute(self):
self._init()
while self._running:
self._event()
self._system()
self._update()
self._quit()
def main():
tetris = Tetris(640, 400)
tetris.execute()
if __name__ == "__main__" :
main()
위 코드의 문제점이 무엇일까요? Tetris를 실행하기 위한 모든 요소들이 한 객체 안에 있다는 것입니다.
테트리스를 실행하기 위한 요소들을 구분하면 다음과 같습니다.
1. Event Queue
2. Backend system
3. Display Window
이 요소들을 하나씩 살펴봅시다.
1. Event Queue
Event Queue는 pygame에서 지원하는 event 기능을 사용하고 있습니다.
테트리스에서는 현재 아래와 같은 주요 event가 발생합니다.
1. 사용자의 key 입력
2. timer 이벤트
이러한 Event 관리는 _event() 함수에서 관리되고 있습니다.
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self._move( 0,-1)
elif event.key == pg.K_DOWN:
self._move( 0, 1)
elif event.key == pg.K_RIGHT:
self._move( 1, 0)
elif event.key == pg.K_LEFT:
self._move(-1, 0)
elif event.type == self.DROP_EVENT:
self._update_field()
self._current_mino = self._set_mino_init_position(self.I_MINO)
2. Backend System
Event queue에 담긴 Event들은 Backend system에서 처리됩니다. 뿐만 아니라 system 자체적으로도 발생하는 일들도 system에서 처리해야 합니다. 지금까지 개발된 system level의 기능은 다음과 같습니다.
1. Field 규격
2. mino 모양 설정
3. key event에 따른 mino move
4. mino move의 enable 설정(boundary 등)
5. drop time event trigger
...
이러한 system적인 부분들은 Code 안에 이곳저곳 흩어져 있습니다. 코드의 품질을 낮추는 원인 중 하나가 되었습니다.
3. Display window
System에서 처리된 것들을 pygame의 surface로 전달하게 됩니다. surface에서 나타낸 것들은 window에 Display되고, 사용자에게 최종적으로 전달됩니다. 지금까지 개발된 display 기능은 다음과 같습니다.
1. window size (현재 640 x 400)
2. tile pixel size
3. fill black surface
4. draw field
5. draw current field
안타깝게도 이런 display 부분도 code에 파편적으로 존재합니다.
즉, 지금 코드는 backend와 display의 코드가 무분별하게 섞여있다는 점입니다.
* 문제 1번 : h_padding은 어디에서 다뤄야 할까요?
보기 1번 : field를 다루는 system 영역
def _update_field(self):
for b in self._current_mino.blocks:
self._field[b[1] + self._h_padding][b[0]] = self._current_mino.name
보기 2번 : rect를 다루는 display 영역
def _draw_field_rect_list(self):
for w in range(self._field_w):
for h in range(self._field_h):
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
if self._field[h+self._h_padding][w] == self.EMPTY:
pg.draw.rect(self._display_surf, (255, 255, 255), _new_rect, 1)
else:
color = self._mino_dict[self._field[h+self._h_padding][w]].color
pg.draw.rect(self._display_surf, color, _new_rect)
정답은...
1번인 system에서 다뤄야 합니다. rect를 그리는 부분에서는 padding값이 어떤 값인지 알 필요가 없어야 하죠.
즉 2번의 함수는 완전 잘못되었다는 것입니다.
* 문제 2번 : mino의 Color는 어디에서 다뤄야 할까요?
보기 1번 : System 영역
def __init__(self, width: int, height: int,
field_w:int=10, field_h:int=20, tile_size:int=10, h_padding:int=None):
self._size = self.width, self.height = width, height
self._field_w, self._field_h, self._t_size = field_w, field_h, tile_size
self._h_padding = field_h if h_padding is None else h_padding
self._field = [[self.EMPTY for _ in range(self._field_w)] for _ in range(self._field_h + self._h_padding)]
self._mino_dict = {
self.I_MINO: Mino(self.I_MINO, [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
self.O_MINO: Mino(self.O_MINO, [[ 0,-1], [ 0, 0], [ 1, 0], [ 1,-1]], (166, 166, 81)),
self.S_MINO: Mino(self.S_MINO, [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (177, 86, 90)),
self.Z_MINO: Mino(self.Z_MINO, [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (143, 173, 60)),
self.L_MINO: Mino(self.L_MINO, [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (73, 94, 158)),
self.J_MINO: Mino(self.J_MINO, [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (202, 121, 65)),
self.T_MINO: Mino(self.T_MINO, [[ 0,-1], [ 0, 0], [-1, 0], [ 1, 0]], (183, 92, 165))
}
보기 2번 : display 영역
def _draw_field_rect_list(self):
for w in range(self._field_w):
for h in range(self._field_h):
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
if self._field[h+self._h_padding][w] == self.EMPTY:
pg.draw.rect(self._display_surf, (255, 255, 255), _new_rect, 1)
else:
color = self._mino_dict[self._field[h+self._h_padding][w]].color
pg.draw.rect(self._display_surf, color, _new_rect)
정답은...
Display 2번입니다. 어떤 mino인지에 대한 분류까지는 system에서 다루고, 그 분류에 대한 Color는 display에서 다뤄야 할 문제입니다. 만약 skin이 변경된다면 system에서 skin 변경을 신경써야 할까요? 그렇지 않습니다. system은 L-mino, I-mino라는 정보만 들고 있고, 그 mino를 어떤 색으로, 어떤 패턴으로 칠할지는 display에서 정해야 합니다.
이제, Tetris Class를 요소에 맞게 분리해 봅시다.
[Refactoring] File 분리하기
app_tetris.py에 있는 event는 건들지 않고, system과 display만 옮기려 합니다.
그리고 system에 Tetris system을 다룰 class를 작성합니다.
Refactoring 과정을 통해, Tetris system Class는 다음과 같이 __init__() method가 정리됩니다.
from copy import deepcopy
from typing import List
import pygame as pg
from enum import Enum, auto
class UserEvent(Enum):
DROP_EVENT = auto()
class Mino:
def __init__(self, blocks: List[List]):
self.blocks = blocks
class System:
drop_event = pg.USEREVENT + UserEvent.DROP_EVENT.value
mino_dict = {
"I": Mino([[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]]),
"O": Mino([[ 0,-1], [ 0, 0], [ 1, 0], [ 1,-1]]),
"S": Mino([[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]]),
"Z": Mino([[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]]),
"L": Mino([[-1,-1], [-1, 0], [ 1, 0], [ 1, 0]]),
"J": Mino([[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]]),
"T": Mino([[ 0,-1], [ 0, 0], [-1, 0], [ 1, 0]])
}
empty = "E"
mino_names = mino_dict.keys()
def __init__(self, field_w: int=10, field_h: int=20, h_padding: int=None):
self._field_w = field_w
self._field_h = field_h
self._h_padding = field_h if h_padding is None else h_padding
self._field = [[False for _ in range(self._field_w)] for _ in range(self._field_h + self._h_padding)]
self._current_mino: Mino = None
self._drop_delay_status: bool = False
* Enum은 살짝 넣어봤습니다.
그 다음에는 Class Display를 작성해볼 차례입니다. 먼저 __init__() 부분입니다.
import pygame as pg
class Display:
mino_color_map = {
"I": (54, 167, 141),
"O": (166, 166, 81),
"S": (177, 86, 90),
"Z": (143, 173, 60),
"L": (73, 94, 158),
"J": (202, 121, 65),
"T": (183, 92, 165),
}
def __init__(self, width: int, height: int, tile_size: int=10):
self.width, self.height, self._t_size = width, height, tile_size
pg.init()
self.display_surf = pg.display.set_mode(self.width, self.height)
이전 코드에 비해서 더욱 명확해졌습니다. 이와 같은 방법을 계속 합니다.
이번에는 _set_mino_init_position()를 refactoring 한 후, system class에 작성하겠습니다.
def _get_mino(self) -> Mino:
key = random.choice(self.mino_names)
return deepcopy(self.mino_dict[key])
def _set_init_position(self, mino: Mino) -> Mino:
for b in mino.blocks:
b[0] += self._field_w // 2
b[1] += self._h_padding
return mino
def _set_next_mino(self, key):
_mino = self._get_mino(key)
self._current_mino = self._set_init_position(_mino)
get_mino를 random하게 바꿔주었습니다. 그리고 padding항목이 _set_init_position으로 들어왔습니다. 앞으로는 field의 y열이 20~39까지를 보여주는 것으로 진행합니다. display에는 field를 잘라서 주면 됩니다.
이번에는 _draw_field_rect_list()를 refactoring 한 후 display class에 작성하겠습니다.
def _draw_field_block(self, field: List[List]):
height, width = len(field), len(field[0])
for h in range(height):
for w in range(width):
if field[h][w] is not False:
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
_color = self.mino_color_map[field[h][w]]
pg.draw.rect(self._display_surf, _color, _new_rect)
def _draw_grid(self, height, width):
for h in range(height):
for w in range(width):
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self._display_surf, self.grid_color, _new_rect, 1)
field에 있는 block을 그리는 것과, _draw_grid로 나뉘었습니다. 당장에 for문 연산량이 2배 늘은 것처럼 보이지만, 실제 컴퓨터가 수행해야 하는 양은 400번 추가입니다. 만약 성능에 문제가 있을 경우 이 부분을 최적화 하면 되지만, 성능에 영향이 없을 것으로 예상됩니다.
이번에는 _update_field()를 refactoring한 후 system class에 작성하겠습니다.
def _field_update(self):
for b in self._current_mino.blocks:
self._field[b[1]][b[0]] = self._current_mino.name
_update_field를 _field_update로 이름을 변경하고, padding을 제외하였습니다.
_event()는 나중에 refactoring한 후 app_tetris.py에 작성하겠습니다.
이번에는 _system()을 refactoring한 후 system class에 작성하겠습니다.
def _is_enable_lock_mino(self):
if self._is_enable_move_down():
self._drop_delay_status = False
pg.time.set_timer(self.DROP_EVENT, UserEvent.STOP_TIMER.value)
else: ## don't move down
if not self._drop_delay_status:
self._drop_delay_status = True
pg.time.set_timer(self.DROP_EVENT, self._drop_delay_millisecond)
_system을 _is_enable_lock_mino라는 이름으로 고쳤습니다. 이 함수는 lock 상태(Field에 남겨지고 다음 mino를 호출할 수 있는 상태)인지 확인하는 함수입니다. 참조문서에서 사용되는 용어인 lock delay에서 따왔습니다.
그리고 기존의 _enable_down()도 _is_enable_move_down()으로 더 명확하게 만들었습니다.
이번에는 _enable_down()을 refactoring한 후 system class에 작성하겠습니다.
위에서 _is_enable_move_down()으로 이름은 수정했지만, 여전히 동작이 깔끔하지 않다는 생각이 듭니다.
동작 방법이 다음과 같습니다.
1. mino가 최하단에 있는지 check
2. 블럭의 밑이 비어있는지 check
그런데 이 2가지 조건을 한번에 합칠 수 있는 아이디어가 생각났습니다.
* Boundary를 똑똑하게 Check할 수 있는 방법
Field의 테두리를 블럭으로 둘러 싼다면
1. 바닥에 도달했는지
2. 조종하는 mino 밑에 블럭이 있는지
이 2가지 조건을 한 가지 조건으로 검사할 수 있습니다.
아이디어가 나왔으니 바로 Field에 적용해 보겠습니다.
class System:
drop_event = pg.USEREVENT + UserEvent.DROP_EVENT.value
mino_dict = {
"I": Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]]),
"O": Mino("O", [[ 0,-1], [ 0, 0], [ 1, 0], [ 1,-1]]),
"S": Mino("S", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]]),
"Z": Mino("Z", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]]),
"L": Mino("L", [[-1,-1], [-1, 0], [ 1, 0], [ 1, 0]]),
"J": Mino("J", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]]),
"T": Mino("T", [[ 0,-1], [ 0, 0], [-1, 0], [ 1, 0]])
}
empty = "E"
mino_names = mino_dict.keys()
boundary = "B" ## 새로 정의
def __init__(self, field_w: int=10, field_h: int=20, h_padding: int=None):
self._field_w = field_w
self._field_h = field_h
self._h_padding = field_h if h_padding is None else h_padding
self._field = self._make_boundary_block(field_w, field_h, self._h_padding) ## 새로 정의
self._current_mino: Mino = None
self._drop_delay_status: bool = False
self._drop_delay_millisecond = 500
## 새로 만든 함수
def _make_boundary_block(self, field_w, field_h, h_padding):
_field = [[self.empty for _ in range(field_w + 2)] for _ in range(field_h + h_padding + 2)]
for i in range(len(_field)):
_field[0][i] = self.boundary
_field[-1][i] = self.boundary
for i in range(len(_field[0])):
_field[i][0] = self.boundary
_field[i][-1] = self.boundary
return _field
그 다음에 _enable_down을 system에 작성합니다.
def _enable_down(self):
is_enable = True
for b in self._current_mino.blocks:
if b[1] + 1 != self.empty:
is_enable = False
break
return is_enable
기존 함수와 비교해 보시면 훨씬 간단하게 표현됨을 알 수 있습니다.
이번에는_update() 함수를 refactoring한 후, display class에 작성해보겠습니다.
def _draw_current_mino(self, current_mino: Mino):
for b in current_mino.blocks:
_rect = pg.Rect(b[0]*self._t_size, b[1]*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self.display_surf, self.mino_color_map[current_mino.name], _rect)
def _update(self, field: List[List], current_mino: Mino):
self.display_surf.fill(self.BLACK)
self._draw_field_block(field)
self._draw_current_mino(current_mino)
pg.display.filp()
흠...작성해보고 나니 뭔가 이상합니다. Mino라는 객체는 Display에는 없습니다. Mino의 Color값만 가져온 상태입니다.
하지만 결국 _update()라는 함수에서는 mino의 위치정보까지 알아야 합니다.
이 문제는 display와 system에서 mino를 한 곳에만 두려고 해서 생긴 문제입니다. 사실 Mino는 양쪽에서 사용하는 중점인 개체입니다.
그렇다면 어떻게 해야 할까요?
Core라는 file을 만들어서 Mino를 관리하고, system과 display 양쪽에서 불러오는 방식으로 진행하면 됩니다.
[Refectoring] 파일을 분리해서 관리하자
그리고 Mino들도 (I, O, S, Z, L, J, T) core에서 소환합니다. 그럼 core.py는 이렇게 작성됩니다.
from typing import List
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
mino_dict = {
"I": Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
"O": Mino("O", [[ 0,-1], [ 0, 0], [ 1, 0], [ 1,-1]], (166, 166, 81)),
"S": Mino("S", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (177, 86, 90)),
"Z": Mino("Z", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (143, 173, 60)),
"L": Mino("L", [[-1,-1], [-1, 0], [ 1, 0], [ 1, 0]], (73, 94, 158)),
"J": Mino("J", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (202, 121, 65)),
"T": Mino("T", [[ 0,-1], [ 0, 0], [-1, 0], [ 1, 0]], (183, 92, 165))
}
* tetris_module/core.py
그리고 이 Keyword는 field에서 사용되는 고유한 Field key값입니다. 이런 key값이 또 empty와 boundary가 있었습니다. 이런 것들도 Core에서 관리합니다.
from typing import List
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
mino_dict = {
"I": Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
"O": Mino("O", [[ 0,-1], [ 0, 0], [ 1, 0], [ 1,-1]], (166, 166, 81)),
"S": Mino("S", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (177, 86, 90)),
"Z": Mino("Z", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (143, 173, 60)),
"L": Mino("L", [[-1,-1], [-1, 0], [ 1, 0], [ 1, 0]], (73, 94, 158)),
"J": Mino("J", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (202, 121, 65)),
"T": Mino("T", [[ 0,-1], [ 0, 0], [-1, 0], [ 1, 0]], (183, 92, 165))
}
empty = "E"
boundary = "B"
이렇게 되면, display와 system은 다음과 같이 작성할 수 있습니다.
* Display 영역
from typing import List
import pygame as pg
import core
from core import Mino
class Display:
mino_dict = core.mino_dict
grid_color = (255, 255, 255)
BLACK = (0, 0, 0)
* tetris_module/display.py
* System 영역
from copy import deepcopy
from typing import List
import pygame as pg
from enum import Enum, auto
import core
from core import Mino
class UserEvent(Enum):
STOP_TIMER = 0
DROP_EVENT = auto()
class System:
drop_event = pg.USEREVENT + UserEvent.DROP_EVENT.value
mino_dict = core.mino_dict
empty = core.empty
boundary = core.boundary
mino_names = mino_dict.keys()
* tetris_module/system.py
그리고, Core에서는 key값으로 사용하는 value들을 enum을 적용해서 겹치지 않도록 합시다.
from typing import List
from enum import Enum, auto
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class KeyString(Enum):
I = auto()
O = auto()
S = auto()
Z = auto()
L = auto()
J = auto()
T = auto()
empty = auto()
boundary = auto()
mino_dict = {
KeyString.I.value: Mino(KeyString.I.value, [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
KeyString.O.value: Mino(KeyString.O.value, [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], (166, 166, 81)),
KeyString.S.value: Mino(KeyString.S.value, [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (177, 86, 90)),
KeyString.Z.value: Mino(KeyString.Z.value, [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (143, 173, 60)),
KeyString.L.value: Mino(KeyString.L.value, [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (73, 94, 158)),
KeyString.J.value: Mino(KeyString.J.value, [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (202, 121, 65)),
KeyString.T.value: Mino(KeyString.T.value, [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], (183, 92, 165))
}
* tetris_module/core.py
그리고 display, system은 core에 맞춰서 다시 바뀝니다.
from copy import deepcopy
from typing import List
import pygame as pg
from enum import Enum, auto
import core
from core import Mino, KeyString
class UserEvent(Enum):
STOP_TIMER = 0
DROP_EVENT = auto()
class System:
drop_event = pg.USEREVENT + UserEvent.DROP_EVENT.value
mino_dict = core.mino_dict
empty = KeyString.empty.value
boundary = KeyString.boundary.value
mino_names = mino_dict.keys()
* tetris_module/system.py
이번에는 _move()라는 함수를 refactoring 후, system에 작성하겠습니다. boundary라는 개념이 없어진 지금은 move 후에 겹치는 도형이 있는지 check만 하면 됩니다.
def _move(self, x, y):
move_enable = True
_test_mino = deepcopy(self._current_mino)
for b in _test_mino.blocks:
if self._field[b[1] + y, b[0] + x] == self.empty:
move_enable = False
break;
if move_enable:
for b in self._current_mino.blocks:
b[0] += x
b[1] += y
* tetris_module/system.py
이제 거의 다 왔습니다!
마지막으로, display와 system을 합쳐줍시다. display와 system에서 빠진 것들을 제외한 것들을 app.py에 작성해봅시다.
import pygame as pg
from typing import List
from copy import deepcopy
from tetris_module.display import Display
from tetris_module.system import System
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class TetrisApp:
def __init__(self, width: int, height: int, tile_size: int=10,
field_w: int=10, field_h: int=20, h_padding: int=None):
self.display = Display(width, height, tile_size)
self.system = System(field_w, field_h, h_padding)
self._running = False
def _init(self):
pg.init()
self._running = True
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self.system._move( 0,-1)
elif event.key == pg.K_DOWN:
self.system._move( 0, 1)
elif event.key == pg.K_RIGHT:
self.system._move( 1, 0)
elif event.key == pg.K_LEFT:
self.system._move(-1, 0)
elif event.type == self.DROP_EVENT:
self.system._field_update()
def _quit(self):
pg.quit()
def execute(self):
self._init()
while self._running:
self._event()
self.display._update()
self._quit()
def main():
tetris = TetrisApp(640, 400)
tetris.execute()
if __name__ == "__main__" :
main()
* tetris_module/app.py
한번 실행해 봅시다. 그리고 안되는 것들을 정리해 봅시다.
1. 객체 밖에서 사용하는데 underscore 붙은 method 수정
2. 사용하지 않는 import, import error 정리(예 : core)
3. 용어에 안맞는 method(예 : field_update) 수정
4. display, system간의 연결 정리
기타 곁가지들까지 정리하면 다음과 같습니다.
* app.py
import pygame as pg
from typing import List
from tetris_module.display import Display
from tetris_module.system import System
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class TetrisApp:
def __init__(self, width: int, height: int, tile_size: int=10,
field_w: int=10, field_h: int=20, h_padding: int=None):
self.display = Display(width, height, tile_size)
self.system = System(field_w, field_h, h_padding)
self._running = False
def _init(self):
self.system.init()
self.display.init()
self._running = True
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self.system.move( 0,-1)
elif event.key == pg.K_DOWN:
self.system.move( 0, 1)
elif event.key == pg.K_RIGHT:
self.system.move( 1, 0)
elif event.key == pg.K_LEFT:
self.system.move(-1, 0)
elif event.type == self.system.drop_event:
## reset시 일어나는 drop_event는 filter
if self.system.drop_delay_status:
self.system.lock_mino()
self.system.get_next_mino()
def _quit(self):
pg.quit()
def execute(self):
self._init()
while self._running:
self._event()
self.system.update()
field = self.system.get_field()
curr_mino = self.system.get_current_mino()
self.display.update(field, curr_mino)
self._quit()
def main():
tetris = TetrisApp(640, 400)
tetris.execute()
if __name__ == "__main__" :
main()
* display.py
import pygame as pg
from numpy.typing import ArrayLike
from . import core
from .core import Mino, KeyString
class Display:
mino_dict = core.mino_dict
empty = KeyString.empty
grid_color = (255, 255, 255)
BLACK = (0, 0, 0)
def __init__(self, width: int, height: int, tile_size: int=10):
self.width, self.height, self._t_size = width, height, tile_size
self.display_surf = None
def init(self):
pg.init()
self.display_surf = pg.display.set_mode((self.width, self.height))
def _draw_field_block(self, field: ArrayLike):
height, width = len(field), len(field[0])
for h in range(height):
for w in range(width):
if field[h, w] != self.empty:
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
_color = self.mino_dict[field[h, w]].color
pg.draw.rect(self.display_surf, _color, _new_rect)
def _draw_grid(self, field: ArrayLike):
height, width = len(field), len(field[0])
for h in range(height):
for w in range(width):
_new_rect = pg.Rect(w*self._t_size, h*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self.display_surf, self.grid_color, _new_rect, 1)
def _draw_current_mino(self, current_mino: Mino):
for b in current_mino.blocks:
_rect = pg.Rect(b[0]*self._t_size, b[1]*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self.display_surf, self.mino_dict[current_mino.name].color, _rect)
def update(self, field: ArrayLike, current_mino: Mino):
self.display_surf.fill(self.BLACK)
self._draw_grid(field)
self._draw_field_block(field)
self._draw_current_mino(current_mino)
pg.display.flip()
* system.py
from copy import deepcopy
import pygame as pg
from enum import Enum, auto
import random
import numpy as np
from numpy.typing import ArrayLike
from . import core
from .core import Mino, KeyString
class UserEvent(Enum):
DROP_EVENT = 1
class System:
drop_event = pg.USEREVENT + UserEvent.DROP_EVENT.value
mino_dict = core.mino_dict
empty = KeyString.empty
boundary = KeyString.boundary
mino_names = list(mino_dict.keys())
drop_delay_status: bool = False
def __init__(self, field_w: int=10, field_h: int=20, h_padding: int=None):
self._field_w = field_w
self._field_w_center = field_w // 2
self._field_h = field_h
self._h_padding = field_h if h_padding is None else h_padding
self._field = self._make_field(field_w, field_h, self._h_padding)
self._current_mino: Mino = None
self._reset_event = 0
self._drop_delay_millisecond = 500
def _make_field(self, field_w, field_h, h_padding) -> ArrayLike:
## boundary, padding
field = np.array([[self.empty for _ in range(field_w + 2)] for _ in range(field_h + h_padding + 2)])
return self._make_boundary(field)
def _make_boundary(self, field: ArrayLike) -> ArrayLike:
for i in range(len(field[0])):
field[0, i] = self.boundary
field[-1, i] = self.boundary
for i in range(len(field)):
field[i, 0] = self.boundary
field[i, -1] = self.boundary
return field
def init(self):
self.get_next_mino()
def get_next_mino(self):
_next_mino_name = self._get_mino_next_name()
self._current_mino = self._get_next_mino_object(_next_mino_name)
def _get_mino_next_name(self):
return random.choice(self.mino_names)
def lock_mino(self):
for b in self._current_mino.blocks:
self._field[b[1], b[0]] = self._current_mino.name
def get_field(self) -> ArrayLike:
## boundary, padding
return self._field[1 + self._h_padding : -1, 1 : -1]
def get_current_mino(self) -> Mino:
output_mino = deepcopy(self._current_mino)
## boundary, padding
for b in output_mino.blocks:
b[0] -= 1
b[1] -= (self._h_padding + 1)
return output_mino
def _get_next_mino_object(self, key) -> Mino:
_mino = deepcopy(self.mino_dict[key])
return self._set_mino_init_position(_mino)
def _set_mino_init_position(self, mino: Mino) -> Mino:
## boundary, padding
for b in mino.blocks:
b[0] += self._field_w_center
b[1] += (self._h_padding + 1)
return mino
def _check_enable_lock_mino(self):
if self._enable_down() and self.drop_delay_status:
self.drop_delay_status = False
pg.time.set_timer(self.drop_event, self._reset_event)
else:
self._set_drop_delay_timer()
def _set_drop_delay_timer(self):
if not self.drop_delay_status:
self.drop_delay_status = True
pg.time.set_timer(self.drop_event, self._drop_delay_millisecond)
def _enable_down(self):
is_enable = True
for b in self._current_mino.blocks:
if self._field[b[1] + 1, b[0]] != self.empty:
is_enable = False
break;
return is_enable
def move(self, x, y):
move_enable = True
_test_mino = deepcopy(self._current_mino)
for b in _test_mino.blocks:
if self._field[b[1] + y, b[0] + x] != self.empty:
move_enable = False
break;
if move_enable:
for b in self._current_mino.blocks:
b[0] += x
b[1] += y
def update(self):
self._check_enable_lock_mino()
* core.py
from typing import List
from enum import Enum, auto
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class KeyString(Enum):
I = auto()
O = auto()
S = auto()
Z = auto()
L = auto()
J = auto()
T = auto()
empty = auto()
boundary = auto()
mino_dict = {
KeyString.I: Mino(KeyString.I, [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], ( 54, 167, 141)),
KeyString.O: Mino(KeyString.O, [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], ( 166, 166, 81)),
KeyString.S: Mino(KeyString.S, [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], ( 177, 86, 90)),
KeyString.Z: Mino(KeyString.Z, [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], ( 143, 173, 60)),
KeyString.L: Mino(KeyString.L, [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], ( 73, 94, 158)),
KeyString.J: Mino(KeyString.J, [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], ( 202, 121, 65)),
KeyString.T: Mino(KeyString.T, [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], ( 183, 92, 165))
}
결과
잘 동작하는 것을 확인할 수 있습니다. 만세
왜 이렇게 Refactoring 을 하는 작업이 길어졌을까요? 그것은 바로 설계부터 어떤 함수를 만들지 정하지 않고, 우선 구현하고싶은 기능만 빠르게 구현하다보니, 이렇게 refactoring하는 작업이 길어진 것입니다.
만약에 설계Level부터 객체 분할을 지정하고 동작해야 하는 method를 미리 나열했으면 작업량은 어떻게 되었을까요? 시작하는 때에는 힘든 작업이 되겠지만, 설계가 끝나고 나면 그 뒤에는 큰 수정이 없는 단순한 작업만 남게 됩니다. 그러면 전체 작업량이 매우 감소하게 됩니다.
다음 시간에는, tetris 요구사항 재 설계를 진행해보도록 하겠습니다.
'Development > Tetris' 카테고리의 다른 글
Python - 테트리스(Tetris) 만들기 (11) - Diagram (0) | 2023.03.26 |
---|---|
Python - 테트리스(Tetris) 만들기 (10) - Requirement (0) | 2023.03.20 |
Python - 테트리스(Tetris) 만들기 (8) - Field (0) | 2023.03.09 |
Python - 테트리스(Tetris) 만들기 (7) - Drop Timer (0) | 2023.03.08 |
Python - 테트리스(Tetris) 만들기 (6) - Tetriminos (2) | 2023.03.07 |