diff --git a/README.rst b/README.rst index bf916f6..82cfb67 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/apps/puzzle15.py b/apps/puzzle15.py new file mode 100644 index 0000000..02c8fee --- /dev/null +++ b/apps/puzzle15.py @@ -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 diff --git a/docs/apps.rst b/docs/apps.rst index 409a4e4..8efb029 100644 --- a/docs/apps.rst +++ b/docs/apps.rst @@ -84,4 +84,6 @@ Games .. automodule:: play2048 +.. automodule:: puzzle15 + .. automodule:: snake diff --git a/res/icons/puzzle_15_icon.png b/res/icons/puzzle_15_icon.png new file mode 100644 index 0000000..87c167b Binary files /dev/null and b/res/icons/puzzle_15_icon.png differ diff --git a/res/screenshots/Puzzle15App.png b/res/screenshots/Puzzle15App.png new file mode 100644 index 0000000..b35b98c Binary files /dev/null and b/res/screenshots/Puzzle15App.png differ