185 lines
6.7 KiB
Python
185 lines
6.7 KiB
Python
|
# 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
|