From 92ffa7cd1c7b6f0ceb8fc3820cb72cd0c12739a6 Mon Sep 17 00:00:00 2001 From: Eloi Torrents Date: Wed, 19 Jul 2023 12:33:35 +0200 Subject: [PATCH] Add 15 Puzzle app Signed-off-by: Eloi Torrents --- README.rst | 4 + apps/puzzle15.py | 184 ++++++++++++++++++++++++++++++++ docs/apps.rst | 2 + res/icons/puzzle_15_icon.png | Bin 0 -> 7548 bytes res/screenshots/Puzzle15App.png | Bin 0 -> 5893 bytes 5 files changed, 190 insertions(+) create mode 100644 apps/puzzle15.py create mode 100644 res/icons/puzzle_15_icon.png create mode 100644 res/screenshots/Puzzle15App.png 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 0000000000000000000000000000000000000000..87c167b90dde60d512f4adccf0a2e024a82143e2 GIT binary patch literal 7548 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 WX%It#u4 literal 0 HcmV?d00001 diff --git a/res/screenshots/Puzzle15App.png b/res/screenshots/Puzzle15App.png new file mode 100644 index 0000000000000000000000000000000000000000..b35b98ce603b090dede8239bc28d1d125ecdc0ad GIT binary patch literal 5893 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 literal 0 HcmV?d00001