Skip to content

Instantly share code, notes, and snippets.

@shiracamus
Last active June 30, 2021 22:36
Show Gist options
  • Save shiracamus/d691300f25c6cbfca1058992e7b7130f to your computer and use it in GitHub Desktop.
Save shiracamus/d691300f25c6cbfca1058992e7b7130f to your computer and use it in GitHub Desktop.
import random
import pygame
from enum import Enum
# COLOR
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
FLORALWHITE = (255, 250, 240)
LIGHTGRAY = (211, 211, 211)
GRAY = (128, 128, 128)
RED = (255, 0, 0)
GREEN = (0, 128, 0)
BLUE = (0, 0, 255)
ORANGE = (255, 165, 0)
COLORS = ((204, 192, 179), (238, 228, 218), (238, 224, 198), # 2, 4, 8
(243, 177, 116), (243, 177, 116), (248, 149, 90), # 16, 32, 64
(249, 94, 50), (239, 207, 108), (239, 207, 99), # 128, 256, 512,
(239, 203, 82), (239, 199, 57), (239, 195, 41), # 1024, 2048, 4096
(95, 218, 147)) # 8192
class Command(Enum):
NOP = 0
START = 1
UP = 2
DOWN = 3
LEFT = 4
RIGHT = 5
UNDO = 6
RESTART = 7
QUIT = 8
class Board:
def __init__(self, size=4):
"""縦・横がsizeマスのゲーム盤を作成する"""
self.SIZE = size
self._RANGE = range(size)
up_left = range(1, size)
down_right = range(0, size - 1)[::-1]
self._SLIDE = { # (dx, dy, range_x, range_y)
Command.UP: (0, -1, self._RANGE, up_left),
Command.DOWN: (0, +1, self._RANGE, down_right),
Command.LEFT: (-1, 0, up_left, self._RANGE),
Command.RIGHT: (+1, 0, down_right, self._RANGE),
}
self.reset()
def reset(self):
"""初期状態にリセットする"""
self._cells = [[0 for x in self._RANGE] for y in self._RANGE]
self._last = self._cells
def __iter__(self):
"""盤データ(列)のイテレータを返す"""
return iter(map(tuple, self._cells))
def save(self):
""" 1つ前の状態に戻せるように現在の状態を保存する"""
self._last = [row.copy() for row in self._cells]
def load(self):
""" 1つ前の状態に戻す"""
self._cells = self._last
def _empties(self):
"""空いているマスの座標を返す"""
return [(x, y)
for y in self._RANGE
for x in self._RANGE
if self._cells[y][x] == 0]
def random_put_2(self):
"""空いているマスのひとつをランダムに選んで2を置く"""
x, y = random.choice(self._empties())
self._cells[y][x] = 2
def _numbers(self, command):
"""数字があるマスの座標とcommand方向の座標(x, y, cx, cy)を返す"""
if command not in self._SLIDE:
return []
dx, dy, rx, ry = self._SLIDE[command]
return [(x, y, x + dx, y + dy)
for y in ry
for x in rx
if self._cells[y][x] != 0]
def can_slide(self, command):
"""command方向にスライド可能ならTrueを返す"""
# 数字があるマスのcommand方向が0か同じ数字ならスライドできる
return any(self._cells[cy][cx] in (0, self._cells[y][x])
for x, y, cx, cy in self._numbers(command))
def slide(self, command):
"""command方向に数字をスライドさせる"""
sliding = True
while sliding:
sliding = False
for x, y, cx, cy in self._numbers(command):
if self._cells[cy][cx] == 0:
self._cells[cy][cx] = self._cells[y][x]
self._cells[y][x] = 0
sliding = True
def merge(self, command):
"""command方向に同じ数字が並んでいたら数字を合算して得点を返す"""
score = 0
for x, y, cx, cy in self._numbers(command):
if self._cells[cy][cx] == self._cells[y][x]:
self._cells[cy][cx] *= 2
self._cells[y][x] = 0
score += self._cells[cy][cx]
return score
class Game:
_SLIDE = Command.UP, Command.DOWN, Command.LEFT, Command.RIGHT
def __init__(self):
self.board = Board()
self.high_score = 0
self._reset()
def _reset(self):
self.board.reset()
self.message = ""
self.score = 0
self.tick = 0
self._undo_score = 0
self._stage = self._title
def operate(self, command):
self.tick += 1
self._stage(command)
def _title(self, command):
self.message = "push [SPACE] to start game!"
if command == Command.START:
self._start()
def _start(self):
self._reset()
self._stage = self._play
def _play(self, command):
self.message = "W - UP S - DOWN A - LEFT D - RIGHT"
if self.tick == 1:
self.board.random_put_2()
elif self.tick <= 10:
pass
elif command == Command.RESTART:
self._start()
elif command == Command.UNDO:
self.score = self._undo_score
self.board.load()
elif self.board.can_slide(command):
self._undo_score = self.score
self.board.save()
self.board.slide(command)
self.score += self.board.merge(command)
self.board.slide(command)
if self.high_score < self.score:
self.high_score = self.score
self.tick = 0
elif all(not self.board.can_slide(command) for command in self._SLIDE):
self._stage = self._over
self.tick = 0
def _over(self, command):
self.message = "GAME OVER!"
if self.tick > 120:
self._stage = self._title
class View:
def __init__(self, screen):
self._screen = screen
def clear(self, color):
self._screen.fill(color)
def draw_rect(self, rect, color, width=0):
pygame.draw.rect(self._screen, color, rect, width)
def draw_text(self, text, x, y, size, color):
render = pygame.font.Font(None, size).render(text, True, color)
pos = (x - render.get_width() // 2, y - render.get_height() // 2)
self._screen.blit(render, pos)
class BoardView(View):
X = 23
Y = 253
SIZE = 488
def draw(self, board):
cell_size = self.SIZE // board.SIZE
for y, row in enumerate(board):
for x, number in enumerate(row):
cell_x = self.X + x * cell_size
cell_y = self.Y + y * cell_size
rect = (cell_x, cell_y, cell_size, cell_size)
if number == 0:
self.draw_rect(rect, LIGHTGRAY)
else:
index = (number >> 2).bit_length() % len(COLORS)
self.draw_rect(rect, COLORS[index])
number_x = cell_x + cell_size // 2
number_y = cell_y + cell_size // 2
self.draw_text(str(number), number_x, number_y, 76, WHITE)
self.draw_rect(rect, GRAY, 5)
class GameView(View):
WIDTH = 538
HEIGHT = 768
RESTART = pygame.Rect(23, 215, 138, 30)
UNDO = pygame.Rect(442, 215, 92, 30)
def __init__(self, screen):
super().__init__(screen)
self._board = BoardView(screen)
def draw(self, game):
self.clear(FLORALWHITE)
self._board.draw(game.board)
# blink message
if game.tick % 50 < 40:
self.draw_text(game.message, self.WIDTH // 2, 176, 38, BLACK)
# 2048
self.draw_text("2", 38, 61, 76, RED)
self.draw_text("0", 69, 61, 76, GREEN)
self.draw_text("4", 99, 61, 76, ORANGE)
self.draw_text("8", 130, 61, 76, BLUE)
# SCORE
self.draw_rect((184, 23, 153, 76), GRAY)
self.draw_text("SCORE", 261, 42, 30, LIGHTGRAY)
self.draw_text(str(game.score), 261, 76, 30, WHITE)
# HIGH SCORE
self.draw_rect((360, 23, 153, 76), GRAY)
self.draw_text("HIGH SCORE", 437, 42, 30, LIGHTGRAY)
self.draw_text(str(game.high_score), 437, 76, 30, WHITE)
# RESTART / UNDO
self.draw_text("RESTART", 92, 230, 38, ORANGE)
self.draw_text("UNDO", 468, 230, 38, ORANGE)
class Controller:
KEYS = {pygame.K_SPACE: Command.START,
pygame.K_w: Command.UP,
pygame.K_s: Command.DOWN,
pygame.K_a: Command.LEFT,
pygame.K_d: Command.RIGHT}
@classmethod
def command(cls):
"""ウィンドウ操作、キー操作、マウス操作をCommandで返す"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return Command.QUIT
pressed = pygame.key.get_pressed()
for key, command in cls.KEYS.items():
if pressed[key]:
return command
pressed_1, pressed_2, pressed_3 = pygame.mouse.get_pressed()
if pressed_1:
x, y = pygame.mouse.get_pos()
if GameView.UNDO.collidepoint(x, y):
return Command.UNDO
if GameView.RESTART.collidepoint(x, y):
return Command.RESTART
return Command.NOP
def main():
pygame.init()
pygame.display.set_caption("2048")
screen = pygame.display.set_mode((GameView.WIDTH, GameView.HEIGHT))
clock = pygame.time.Clock()
game = Game()
view = GameView(screen)
while (command := Controller.command()) != Command.QUIT:
game.operate(command)
view.draw(game)
pygame.display.update()
clock.tick(30)
pygame.quit()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment