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>
This commit is contained in:
Miguel Rochefort 2021-01-04 22:12:39 -05:00 committed by Daniel Thompson
parent 8a07edb4d8
commit ffff5ae52b
7 changed files with 241 additions and 4 deletions

View File

@ -189,6 +189,10 @@ using one of the techniques is the Application Writer's guide.
:alt: Calculator running in the wasp-os simulator
:width: 179
.. image:: res/2048App.png
:alt: Let's play the 2048 game (in the wasp-os simulator)
:width: 179
.. image:: res/SnakeApp.png
:alt: Snake Game running in the wasp-os simulator
:width: 179

View File

@ -32,6 +32,8 @@ Games
.. automodule:: apps.gameoflife
.. automodule:: apps.play2048
.. automodule:: apps.snake
Integration

BIN
res/2048App.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
res/2048_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

229
wasp/apps/play2048.py Normal file
View File

@ -0,0 +1,229 @@
# 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

View File

@ -19,10 +19,11 @@ class SoftwareApp():
('fibonacci_clock', wasp.widgets.Checkbox(0, 120, 'Fibonacci Clock')),
('gameoflife', wasp.widgets.Checkbox(0, 160, 'Game Of Life')),
('musicplayer', wasp.widgets.Checkbox(0, 0, 'Music Player')),
('snake', wasp.widgets.Checkbox(0, 40, 'Snake Game')),
('flashlight', wasp.widgets.Checkbox(0, 80, 'Torch')),
('testapp', wasp.widgets.Checkbox(0, 120, 'Test')),
('timer', wasp.widgets.Checkbox(0, 160, 'Timer')),
('play2048', wasp.widgets.Checkbox(0, 40, 'Play 2048')),
('snake', wasp.widgets.Checkbox(0, 80, 'Snake Game')),
('flashlight', wasp.widgets.Checkbox(0, 120, 'Torch')),
('testapp', wasp.widgets.Checkbox(0, 160, 'Test')),
('timer', wasp.widgets.Checkbox(0, 0, 'Timer')),
)
self.si = wasp.widgets.ScrollIndicator()
self.page = 0

View File

@ -15,6 +15,7 @@ manifest = (
'apps/musicplayer.py',
'apps/launcher.py',
'apps/pager.py',
'apps/play2048.py',
'apps/settings.py',
'apps/software.py',
'apps/steps.py',