Add 15 Puzzle app
Signed-off-by: Eloi Torrents <eloitor@disroot.org>
This commit is contained in:
parent
f50d03e529
commit
92ffa7cd1c
5 changed files with 190 additions and 0 deletions
|
@ -187,6 +187,10 @@ Games:
|
|||
:alt: Snake Game running in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
.. image:: res/screenshots/Puzzle15App.png
|
||||
:alt: 15 Puzzle running in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
Time management apps:
|
||||
|
||||
.. image:: res/screenshots/AlarmApp.png
|
||||
|
|
184
apps/puzzle15.py
Normal file
184
apps/puzzle15.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (C) 2023 Eloi Torrents
|
||||
"""Puzzle 15
|
||||
~~~~~~~~~~~~
|
||||
|
||||
A popular sliding block puzzle game.
|
||||
|
||||
.. figure:: res/screenshots/Puzzle15App.png
|
||||
:width: 179
|
||||
|
||||
Screenshot of the 15 puzzle application
|
||||
"""
|
||||
|
||||
import wasp
|
||||
import widgets
|
||||
import random
|
||||
import fonts
|
||||
from micropython import const
|
||||
|
||||
_FONT = fonts.sans24
|
||||
_GRID_SIZE = const(4)
|
||||
_GRID_PADDING = const(8)
|
||||
_GRID_BACKGROUND = const(0x942F)
|
||||
_EMPTY_BACKGROUND = const(0x9CB1)
|
||||
_CELL_BACKGROUND = const(0xEF19)
|
||||
_CELL_FOREGROUND = const(0x736C)
|
||||
|
||||
_SCREEN_SIZE = const(240)
|
||||
_CELL_SIZE = const((_SCREEN_SIZE - (_GRID_PADDING * (_GRID_SIZE + 1))) // _GRID_SIZE)
|
||||
|
||||
# 2-bit RLE, 96x64, generated from res/icons/puzzle_15_icon.png, 566 bytes
|
||||
icon = (
|
||||
b'\x02'
|
||||
b'`@'
|
||||
b'\x10\xbf\x01 \xbf\x01 \xbf\x01 \x83@\xfaM\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\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 \x83M'
|
||||
b'\x82M\x82M\x82M\x83 \x83M\x82M\x82M\x82M'
|
||||
b'\x83 \x83M\x82M\x82M\x82M\x83 \x83M\x82M'
|
||||
b'\x82M\x82M\x83 \xbf\x01 \xbf\x01 \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 \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\x81\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'\xff\x01\x10'
|
||||
)
|
||||
|
||||
class Puzzle15App():
|
||||
"""Let's solve the 15 puzzle."""
|
||||
NAME = '15'
|
||||
ICON = icon
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the application."""
|
||||
self._state = 0
|
||||
self._confirmation_view = widgets.ConfirmationView()
|
||||
self._start_game()
|
||||
|
||||
def foreground(self):
|
||||
"""Activate the application."""
|
||||
wasp.system.request_event(wasp.EventMask.TOUCH |
|
||||
wasp.EventMask.SWIPE_UPDOWN |
|
||||
wasp.EventMask.SWIPE_LEFTRIGHT)
|
||||
|
||||
self._state = 0
|
||||
self._draw()
|
||||
|
||||
def touch(self,event):
|
||||
"""Notify the application of a touchscreen touch event."""
|
||||
if self._state == 0:
|
||||
self._confirmation_view.draw("{} Moves. Again?".format(self._move_count))
|
||||
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."""
|
||||
draw = wasp.watch.drawable
|
||||
if self._state == 0:
|
||||
move_x, move_y = 0, 0
|
||||
if event[0] == wasp.EventType.LEFT and self._empty_y < _GRID_SIZE - 1:
|
||||
move_y = 1
|
||||
elif event[0] == wasp.EventType.RIGHT and self._empty_y > 0:
|
||||
move_y = -1
|
||||
elif event[0] == wasp.EventType.UP and self._empty_x < _GRID_SIZE - 1:
|
||||
move_x = 1
|
||||
elif event[0] == wasp.EventType.DOWN and self._empty_x > 0:
|
||||
move_x = -1
|
||||
if move_x != 0 or move_y !=0:
|
||||
x, y, b = self._empty_x, self._empty_y, self._board
|
||||
b[x][y], b[x + move_x][y + move_y] = b[x + move_x][y + move_y], b[x][y]
|
||||
self._empty_x += move_x
|
||||
self._empty_y += move_y
|
||||
self._move_count += 1
|
||||
self._update(draw, x, y)
|
||||
self._update(draw, self._empty_x, self._empty_y)
|
||||
|
||||
def _draw(self):
|
||||
"""Draw the display from scratch."""
|
||||
draw = wasp.watch.drawable
|
||||
draw.fill(_GRID_BACKGROUND)
|
||||
draw.set_font(_FONT)
|
||||
for y in range(_GRID_SIZE):
|
||||
for x in range(_GRID_SIZE):
|
||||
self._update(draw, y, x)
|
||||
|
||||
def _update(self, draw, 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))
|
||||
if self._board[row][col] != 0:
|
||||
draw.set_color(_CELL_FOREGROUND, _CELL_BACKGROUND)
|
||||
draw.fill(_CELL_BACKGROUND, x, y, _CELL_SIZE, _CELL_SIZE)
|
||||
draw.string(str(self._board[row][col]), x, y + 16, _CELL_SIZE)
|
||||
else:
|
||||
draw.fill(_EMPTY_BACKGROUND, x, y, _CELL_SIZE, _CELL_SIZE)
|
||||
|
||||
def _start_game(self):
|
||||
"""Start a new game."""
|
||||
self._board = self._create_board()
|
||||
self._empty_x = _GRID_SIZE - 1
|
||||
self._empty_y = _GRID_SIZE - 1
|
||||
self._move_count = 0
|
||||
|
||||
def _get_invCount(self, board):
|
||||
"""Count inversion count.
|
||||
https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/"""
|
||||
num_list = [num for row in board for num in row]
|
||||
inv_count = 0
|
||||
for i, num1 in enumerate(num_list[:-2]):
|
||||
for num2 in num_list[i+1:-1]:
|
||||
if num1 > num2:
|
||||
inv_count += 1
|
||||
return inv_count
|
||||
|
||||
def _getNum(self, v):
|
||||
"""return the next random number"""
|
||||
n = len(v)
|
||||
idx = random.randint(0, n-1)
|
||||
num, v[idx] = v[idx], v[n-1]
|
||||
v.pop()
|
||||
return num
|
||||
|
||||
def _create_board(self):
|
||||
"""Create a new GRID_SIZE x GRID_SIZE board."""
|
||||
board = [[0] * _GRID_SIZE for _ in range(_GRID_SIZE)]
|
||||
v = list(range(1, _GRID_SIZE * _GRID_SIZE))
|
||||
for i in range(_GRID_SIZE):
|
||||
for j in range(_GRID_SIZE):
|
||||
if i != _GRID_SIZE - 1 or j != _GRID_SIZE -1:
|
||||
board[i][j] = self._getNum(v)
|
||||
# Ensure solvability
|
||||
if self._get_invCount(board) % 2 == 1:
|
||||
board[-1][-3], board[-1][-2] = board[-1][-2], board[-1][-3]
|
||||
return board
|
|
@ -84,4 +84,6 @@ Games
|
|||
|
||||
.. automodule:: play2048
|
||||
|
||||
.. automodule:: puzzle15
|
||||
|
||||
.. automodule:: snake
|
||||
|
|
BIN
res/icons/puzzle_15_icon.png
Normal file
BIN
res/icons/puzzle_15_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
BIN
res/screenshots/Puzzle15App.png
Normal file
BIN
res/screenshots/Puzzle15App.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
Loading…
Reference in a new issue