wasp-os/wasp/apps/play2048.py
Miguel Rochefort ffff5ae52b apps: play2048: Add the 2048 game application
2048 is a popular sliding block puzzle game in which tiles are combined
to make the number 2048.

It's one of the few games that are enjoyable to play on such a small
form factor.

This started as a port of a TkInter implementation of the 2048 game. I
implemented all of the TkInter APIs used by the game and it worked on
wasp-os without any code change in the game. However, the performance
was very poor and it consumed too much RAM. I have since reimplemented
the whole game from scratch and managed to achieve acceptable
performance, although more improvements could still be made.

Because names in Python can't start with numbers, I had some trouble
naming things. The module is called "ttfe" (two-thousand-forty-eight),
the class name is Play2048App, and the software.py entry is "Play 2048".

Signed-off-by: Miguel Rochefort <miguelrochefort@gmail.com>
[daniel@redfelineninja.org.uk: Renamed the python filename, normalized
the screenshot and included the app in the docs]
Signed-off-by: Daniel Thompson <daniel@redfelineninja.org.uk>
2021-01-10 18:14:36 +00:00

230 lines
7.9 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Miguel Rochefort
"""Play 2048
~~~~~~~~~~~~
A popular sliding block puzzle game in which tiles are combined to make the
number 2048.
.. figure:: res/2048App.png
:width: 179
Screenshot of the 2048 game application
"""
import wasp
import icons
import widgets
import random
import fonts
from micropython import const
SCREEN_SIZE = const(240)
GRID_PADDING = const(8)
GRID_SIZE = const(4)
CELL_SIZE = const(50)
GRID_BACKGROUND = const(0x942F)
CELL_BACKGROUND = [0x9CB1, 0xEF3B, 0xEF19, 0xF58F, 0xF4AC, 0xF3EB, 0xF2E7, 0xEE6E, 0xEE6C, 0xEE4A, 0xEE27, 0xEE05]
CELL_FOREGROUND = [0x9CB1, 0x736C, 0x736C, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE, 0xFFBE]
CELL_LABEL = ['','2','4','8','16','32','64','128','256','512','1K','2K'] # TODO: Display 1024 and 2048 (text-wrapping)
# 2-bit RLE, generated from res/2048_icon.png, 785 bytes
icon = (
b'\x02'
b'`@'
b'\x10\xbf\x01 \xbf\x01 \xbf\x01 \x83@\x81M\x82M'
b'\x82M\x82M\x83 \x83M\x82M\x82M\x82M\x83 '
b'\x83M\x82M\x82M\x82M\x83 \x83M\x82M\x82M'
b'\x82M\x83 \x83M\x82M\x82M\x82M\x83 \x83M'
b'\x82M\x82M\x82M\x83 \x83M\x82M\x82M\x82M'
b'\x83 \x83M\x82M\x82M\x82M\x83 \x83M\x82M'
b'\x82M\x82M\x83 \x83M\x82M\x82M\x82M\x83 '
b'\x83M\x82M\x82M\x82M\x83 \x83M\x82M\x82M'
b'\x82M\x83 \x83M\x82M\x82M\x82M\x83 \xbf\x01'
b' \xbf\x01 \x83M\x82M\x82M\x82\x80\xfb\x8d\xc0\xdb'
b'\xc3 \xc3M\xc2M\xc2M\xc2\x8d\xc3 \xc3M\xc2M'
b'\xc2M\xc2\x8d\xc3 \xc3M\xc2M\xc2M\xc2\x8d\xc3 '
b'\xc3M\xc2M\xc2M\xc2\x8d\xc3 \xc3M\xc2M\xc2M'
b'\xc2\x8d\xc3 \xc3M\xc2M\xc2M\xc2\x8d\xc3 \xc3M'
b'\xc2M\xc2M\xc2\x8d\xc3 \xc3M\xc2M\xc2M\xc2\x8d'
b'\xc3 \xc3M\xc2M\xc2M\xc2\x8d\xc3 \xc3M\xc2M'
b'\xc2M\xc2\x8d\xc3 \xc3M\xc2M\xc2M\xc2\x8d\xc3 '
b'\xc3M\xc2M\xc2M\xc2\x8d\xc3 \xff\x01 \xff\x01 '
b'\xc3\x8d\xc2M\xc2M\xc2@\xfaM\xc3 \xc3\x8d\xc2\x80'
b'\x81\x8d\xc2\x8d\xc2M\xc3 \xc3\xc0\xfb\xcd@\xdbB\x8d'
b'B\x8dB\x80\xfa\x8dC C\xcdB\xc0\x81\xcdB\xcd'
b'B\x8dC C@\xfbEA\x82AD\x80\xdb\x82\xcd'
b'\x82\xcd\x82\xc0\xfa\xcd\x83 \x83DAAAA\xc1A'
b'C\x82@\x81M\x82M\x82\xcd\x83 \x83\x80\xfb\x88\xc1'
b'\x81\x83\xc0\xdb\xc2M\xc2M\xc2@\xfaM\xc3 \xc3\x87'
b'\x81A\x81\x83\xc2\x80\x81\x8d\xc2\x8d\xc2M\xc3 \xc3\xc0'
b'\xfb\xc7A\xc1\xc4@\xdbB\x8dB\x8dB\x80\xfa\x8dC'
b' C\xc6\x81\xc1\xc5B\xc0\x81\xcdB\xcdB\x8dC '
b'C@\xfbE\x81AF\x80\xdb\x82\xcd\x82\xcd\x82\xc0\xfa'
b'\xcd\x83 \x83DA\xc4AC\x82@\x81M\x82M\x82'
b'\xcd\x83 \x83\x80\xfb\x8d\xc0\xdb\xc2M\xc2M\xc2@\xfa'
b'M\xc3 \xff\x01 \xff\x01 \xc3\x80\x81\x8d\xc2\xc0\xfb'
b'\xcd@\xdbB\x80\xf6\x8dB\xc0\xc8\xcdC C@\x81'
b'M\x80\xdb\x82\xc0\xfb\xcd\x82@\xf6M\x82\x80\xc8\x8d\xc0'
b'\xdb\xc3 \xc3@\x81M\xc2\x80\xfb\x8d\xc2\xc0\xf6\xcd@'
b'\xdbB\x80\xc8\x8dC C\xc0\x81\xcdB@\xfbM\x80'
b'\xdb\x82\xc0\xf6\xcd\x82@\xc8M\x83 \x83\x80\x81\x8d\xc0'
b'\xdb\xc2@\xfbM\xc2\x80\xf6\x8d\xc2\xc0\xc8\xcd@\xdbC'
b' C\x80\x81\x8dB\xc0\xfb\xcdB@\xf6M\x80\xdb\x82'
b'\xc0\xc8\xcd\x83 \x83@\x81M\x82\x80\xfb\x8d\xc0\xdb\xc2'
b'@\xf6M\xc2\x80\xc8\x8d\xc3 \xc3\xc0\x81\xcd@\xdbB'
b'\x80\xfb\x8dB\xc0\xf6\xcdB@\xc8M\x80\xdb\x83 \x83'
b'\xc0\x81\xcd\x82@\xfbM\x82\x80\xf6\x8d\xc0\xdb\xc2@\xc8'
b'M\xc3 \xc3\x80\x81\x8d\xc2\xc0\xfb\xcd@\xdbB\x80\xf6'
b'\x8dB\xc0\xc8\xcdC C@\x81M\x80\xdb\x82\xc0\xfb'
b'\xcd\x82@\xf6M\x82\x80\xc8\x8d\xc0\xdb\xc3 \xc3@\x81'
b'M\xc2\x80\xfb\x8d\xc2\xc0\xf6\xcd@\xdbB\x80\xc8\x8dC'
b' C\xc0\x81\xcdB@\xfbM\x80\xdb\x82\xc0\xf6\xcd\x82'
b'@\xc8M\x83 \xbf\x01 \xbf\x01 \xbf\x01\x10'
)
class Play2048App():
"""Let's play the 2048 game."""
NAME = '2048'
ICON = icon
def __init__(self):
"""Initialize the application."""
self._board = None
self._state = 0
self._confirmation_view = None
def foreground(self):
"""Activate the application."""
wasp.system.request_event(wasp.EventMask.TOUCH |
wasp.EventMask.SWIPE_UPDOWN |
wasp.EventMask.SWIPE_LEFTRIGHT)
self._state = 0
if not self._board:
self._start_game()
self._draw()
def touch(self,event):
"""Notify the application of a touchscreen touch event."""
if self._state == 0:
if not self._confirmation_view:
self._confirmation_view = widgets.ConfirmationView()
self._confirmation_view.draw('Restart game?')
self._state = 1
elif self._state == 1:
if self._confirmation_view.touch(event):
if self._confirmation_view.value:
self._start_game()
self._draw()
self._state = 0
def swipe(self, event):
"""Notify the application of a touchscreen swipe event."""
moved = False
if event[0] == wasp.EventType.UP:
moved = self._shift(1,False)
elif event[0] == wasp.EventType.DOWN:
moved = self._shift(-1,False)
elif event[0] == wasp.EventType.LEFT:
moved = self._shift(1,True)
elif event[0] == wasp.EventType.RIGHT:
moved = self._shift(-1,True)
if moved:
self._add_tile()
def _draw(self):
"""Draw the display from scratch."""
board = self._board
draw = wasp.watch.drawable
draw.fill(GRID_BACKGROUND)
draw.set_font(fonts.sans24)
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
self._update(draw, board[y][x], y, x)
def _update(self, draw, cell, row, col):
"""Update the specified cell of the application display."""
x = GRID_PADDING + (col * (CELL_SIZE + GRID_PADDING))
y = GRID_PADDING + (row * (CELL_SIZE + GRID_PADDING))
draw.set_color(CELL_FOREGROUND[cell], CELL_BACKGROUND[cell])
draw.fill(CELL_BACKGROUND[cell], x, y, CELL_SIZE, CELL_SIZE)
draw.string(CELL_LABEL[cell], x, y + 16, CELL_SIZE)
def _start_game(self):
"""Start a new game."""
self._board = self._create_board()
self._add_tile()
self._add_tile()
def _create_board(self):
"""Create an empty 4x4 board."""
board = []
for _ in range(GRID_SIZE):
board.append([0] * GRID_SIZE)
return board
def _add_tile(self):
"""Add a new tile to a random empty location on the board."""
board = self._board
randint = random.randint
y = randint(0, GRID_SIZE-1)
x = randint(0, GRID_SIZE-1)
while board[y][x] != 0:
y = randint(0, GRID_SIZE-1)
x = randint(0, GRID_SIZE-1)
board[y][x] = 1
self._update(wasp.watch.drawable,1,y,x)
def _shift(self, direction, orientation):
"""Shift and merge the tiles vertically."""
draw = wasp.watch.drawable
update = self._update
board = self._board
moved = False
def read(y, x):
if not orientation:
y,x = x,y
return board[y][x]
def write(y, x, v):
if not orientation:
y,x = x,y
board[y][x] = v
update(draw, v, y, x)
if direction > 0:
s = 0 + 1
e = GRID_SIZE
else:
s = GRID_SIZE - 1 - 1
e = 0 - 1
for y in range(GRID_SIZE):
p = s - direction
for x in range(s,e,direction):
a = read(y,x)
b = read(y,p)
if a != 0:
if a == b:
write(y, p, a + 1)
write(y, x, 0)
moved = True
p += direction
else:
if b != 0:
p += direction
if x != p:
write(y, p, a)
write(y, x, 0)
moved = True
return moved