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:
parent
8a07edb4d8
commit
ffff5ae52b
7 changed files with 241 additions and 4 deletions
|
@ -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
|
||||
|
|
|
@ -32,6 +32,8 @@ Games
|
|||
|
||||
.. automodule:: apps.gameoflife
|
||||
|
||||
.. automodule:: apps.play2048
|
||||
|
||||
.. automodule:: apps.snake
|
||||
|
||||
Integration
|
||||
|
|
BIN
res/2048App.png
Normal file
BIN
res/2048App.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
BIN
res/2048_icon.png
Normal file
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
229
wasp/apps/play2048.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue