Compare commits
10 commits
b32787e5ea
...
749fe3432f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
749fe3432f | ||
|
|
089cb04f25 | ||
|
|
89be0be23c | ||
|
|
8b7c707ffe | ||
|
|
396d03d036 | ||
|
|
a2e352229a | ||
|
|
f80cbf4d75 | ||
|
|
b6a359a6b8 | ||
|
|
0299ba6689 | ||
|
|
ae4230a6d7 |
11 changed files with 341 additions and 11 deletions
|
|
@ -7,6 +7,9 @@ build:
|
|||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3"
|
||||
apt_packages:
|
||||
# App lifecycle graph
|
||||
- graphviz
|
||||
jobs:
|
||||
pre_build:
|
||||
# Ensure appregistry is generated
|
||||
|
|
|
|||
26
README.rst
26
README.rst
|
|
@ -72,15 +72,17 @@ Community
|
|||
|
||||
The wasp-os community is centred around the
|
||||
`github project <https://github.com/wasp-os/wasp-os>`_ and is
|
||||
supplemented with instant messaging via the #wasp-os IRC channel at
|
||||
libera.chat .
|
||||
supplemented with instant messaging in the
|
||||
`#wasp-os:matrix.org <https://matrix.to/#/#wasp-os:matrix.org>`_
|
||||
channel. If you do not have a preferred Matrix chat client then we recommend
|
||||
trying out the
|
||||
`Element web client <https://app.element.io/#/room/#wasp-os:matrix.org>`_
|
||||
Follow the element link and, if you do not already have a matrix account,
|
||||
register yourself. That should be enough to get you chatting!
|
||||
|
||||
If you are unfamiliar with IRC and don't have a preferred client then
|
||||
we recommend connecting to libera.chat using the
|
||||
`matrix/IRC bridge <https://app.element.io/#/room/#wasp-os:libera.chat>`_.
|
||||
The matrix bridge will allow us to receive messages whilst offline. Follow
|
||||
the link above and, if you do not already have a matrix account, register
|
||||
yourself. That should be enough to get you chatting!
|
||||
Alternatively, if you prefer to use IRC then, for all
|
||||
`the usual reasons <https://xkcd.com/1782/>`_ the Matrix channel is also
|
||||
bridged to the #wasp-os IRC channel at libera.chat.
|
||||
|
||||
Videos
|
||||
------
|
||||
|
|
@ -173,6 +175,10 @@ Watch faces:
|
|||
:alt: Shows a time as words in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
.. image:: res/screenshots/ResistorClockApp.png
|
||||
:alt: Resistor colour code clock application running in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
Games:
|
||||
|
||||
.. image:: res/screenshots/Play2048App.png
|
||||
|
|
@ -191,6 +197,10 @@ Games:
|
|||
:alt: 15 Puzzle running in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
.. image:: res/screenshots/FourInARowApp.png
|
||||
:alt: Four In A Row running in the wasp-os simulator
|
||||
:width: 179
|
||||
|
||||
Time management apps:
|
||||
|
||||
.. image:: res/screenshots/AlarmApp.png
|
||||
|
|
|
|||
204
apps/four_in_a_row.py
Normal file
204
apps/four_in_a_row.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
# Copyright (C) 2023 Tony Robinson
|
||||
|
||||
"""Four in a Row
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is the classic two player board game called Four In A Row or Connect4. You play against the computer.
|
||||
|
||||
.. figure:: res/screenshots/FourInARowApp.png
|
||||
:width: 179
|
||||
|
||||
Screenshot of Four In A Row
|
||||
|
||||
There is in intro/menu screen which has very brief instructions, allows you to set the opponent level and gives some stats on the number of games you have won. Touching the screen sets the level from 0 to 6 which corresponds to the number of lookahead plies. Swiping down enters the main game. On your turn a red square counter will appear on the top row. Touch the screen to move to the desired column, optionally touch again if you don't like your choice then swipe down to commit to playing that column. The computer will reply with a yellow counter after a delay (dependent on level). Your aim is to get four in a row before the computer does. At end of game swipe down to return to the intro/menu screen.
|
||||
|
||||
This app is powered by a compact version of the Alpha Beta pruning algorithm. For technical details see https://en.wikipedia.org/wiki/Connect_Four and https://kaggle.com/competitions/connectx/. There isn't space for a transposition table in RAM, so MTDf isn't implmented, nevertheless it can play a challenging game.
|
||||
|
||||
"""
|
||||
|
||||
import wasp, array
|
||||
from micropython import const
|
||||
|
||||
# parameters of the 'Four in a Row' game
|
||||
_NROW = const(6)
|
||||
_NCOLUMN = const(7)
|
||||
# assert _NCOLUMN == _NROW + 1 # need square, top row is where counters are placed
|
||||
_NMIDDLE = const((_NCOLUMN - 1) // 2)
|
||||
_NBOARD = const(_NROW * _NCOLUMN)
|
||||
|
||||
# colours
|
||||
_BLUE = const(0x0010) # Blue: half way to full blue
|
||||
_BLACK = const(0x0000) # Black
|
||||
_cPLAY = const(0xF800) # Red: colour for human PLAYer - becuase I like playing red
|
||||
_cCOMP = const(0xFFE0) # Yellow: colour for COMPputer pieces
|
||||
|
||||
# basics of the display
|
||||
_NPIXEL = const(240)
|
||||
_NCELL = const(_NPIXEL // _NCOLUMN)
|
||||
_NBORDER = const((_NPIXEL - _NCELL * _NCOLUMN) // 2)
|
||||
_NPAD = const(2)
|
||||
|
||||
# states of game
|
||||
_INTRO, _PLAY, _GAMEOVER = const(0), const(1), const(2)
|
||||
_NLEVEL = const(7)
|
||||
|
||||
def _gameOver(bitmap):
|
||||
for s in [1, _NCOLUMN, _NCOLUMN+2, _NCOLUMN+1]:
|
||||
b = bitmap & (bitmap >> s)
|
||||
if b & (b >> 2 * s):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _bitmapGet(bitmap, x, y):
|
||||
return (bitmap >> (y * (_NCOLUMN + 1) + x)) & 1
|
||||
|
||||
def _bitmapSet(bitmap, x, y):
|
||||
return bitmap | 1 << (y * (_NCOLUMN + 1) + x)
|
||||
|
||||
_searchOrder = bytearray(sorted(range(_NCOLUMN), key=lambda x:abs(x - _NMIDDLE)))
|
||||
|
||||
def _swapmin(bitNext, bitLast, low, depth, lob, upb):
|
||||
sumLow = sum(low)
|
||||
|
||||
# list all the legal moves
|
||||
legal = [ n for n in _searchOrder if low[n] != _NROW ]
|
||||
|
||||
# see if we can win
|
||||
for x in legal:
|
||||
if _gameOver(_bitmapSet(bitNext, x, low[x])):
|
||||
return (_NBOARD - sumLow + 1) // 2, x
|
||||
|
||||
# easy wins done, quit now if at search depth or end of board
|
||||
if depth == 0 or sumLow == _NBOARD:
|
||||
return 0, -1 # -1 is not right but DRAW will catch it
|
||||
|
||||
# if can't win then see if we have to block
|
||||
for x in legal:
|
||||
if _gameOver(_bitmapSet(bitLast, x, low[x])):
|
||||
legal = [ x ]
|
||||
break
|
||||
|
||||
bestv = _NBOARD
|
||||
for x in legal:
|
||||
lowp = bytearray(low) # bytearray used as copy.copy()
|
||||
lowp[x] += 1
|
||||
v, _ = _swapmin(bitLast, _bitmapSet(bitNext, x, low[x]), lowp, depth-1, -upb, -lob)
|
||||
if v < bestv:
|
||||
bestv, bestx = v, x
|
||||
upb = min(upb, bestv)
|
||||
if bestv <= lob:
|
||||
break
|
||||
|
||||
return -bestv, bestx
|
||||
|
||||
def rndColumn():
|
||||
return int(wasp.watch.rtc.uptime) % _NCOLUMN # time used as RNG to save space
|
||||
|
||||
class FourInARowApp():
|
||||
NAME = '4 ina row'
|
||||
|
||||
def __init__(self):
|
||||
self.screen = _INTRO
|
||||
self.level = wasp.widgets.Slider(_NLEVEL, x=10, y=150, color=_cPLAY)
|
||||
self.nwin = array.array('H', [0] * _NLEVEL)
|
||||
self.ngame = array.array('H', [0] * _NLEVEL)
|
||||
|
||||
def _showStats(self):
|
||||
nwin = self.nwin[self.level.value]
|
||||
ngame = self.ngame[self.level.value]
|
||||
pc = int(100 * nwin / ngame + 0.5) if ngame > 0 else 0
|
||||
wasp.watch.drawable.string('level %d won %d%%' % (self.level.value, pc), 0, 200, width=_NPIXEL)
|
||||
|
||||
def _place(self, x, y, colour):
|
||||
wasp.watch.drawable.fill(x=_NBORDER + x*_NCELL + _NPAD, y=_NBORDER + (_NROW-y)*_NCELL + _NPAD, w=_NCELL - 2*_NPAD, h=_NCELL - 2*_NPAD, bg=colour)
|
||||
|
||||
def foreground(self):
|
||||
draw = wasp.watch.drawable
|
||||
|
||||
# draw board from state
|
||||
draw.fill()
|
||||
if self.screen == _INTRO:
|
||||
draw.string('FOUR IN A ROW', 0, 30, width=_NPIXEL)
|
||||
draw.string('Touch sets column', 0, 80, width=_NPIXEL)
|
||||
draw.string('swipe down to play', 0, 110, width=_NPIXEL)
|
||||
self.level.draw()
|
||||
self._showStats()
|
||||
else:
|
||||
draw.fill(x=0, y=_NBORDER+_NCELL, w=_NPIXEL, h=_NPIXEL-_NCELL, bg=_BLUE)
|
||||
for x in range(_NCOLUMN):
|
||||
for y in range(_NROW):
|
||||
if _bitmapGet(self.bPLAY, x, y):
|
||||
self._place(x, y, _cPLAY)
|
||||
elif _bitmapGet(self.bCOMP, x, y):
|
||||
self._place(x, y, _cCOMP)
|
||||
self._place(self.x, _NROW, _cPLAY)
|
||||
|
||||
wasp.system.request_event(wasp.EventMask.TOUCH | wasp.EventMask.SWIPE_UPDOWN)
|
||||
|
||||
# touch to set the drop column
|
||||
def touch(self, event):
|
||||
if self.screen == _INTRO:
|
||||
self.level.touch(event)
|
||||
self.level.update()
|
||||
self._showStats()
|
||||
elif self.screen == _PLAY:
|
||||
self._place(self.x, _NROW, _BLACK)
|
||||
self.x = min(max((event[1] - _NBORDER) // _NCELL, 0), _NCOLUMN-1)
|
||||
self._place(self.x, _NROW, _cPLAY)
|
||||
|
||||
def _drop(self, x, colour):
|
||||
if self.low[x] == _NROW:
|
||||
wasp.watch.vibrator.pulse() # can't move here!
|
||||
else:
|
||||
y = self.low[x]
|
||||
for i in range(_NROW, y, -1):
|
||||
self._place(x, i-1, colour)
|
||||
self._place(x, i, _BLUE if i != _NROW else _BLACK)
|
||||
wasp.watch.time.sleep(0.1)
|
||||
if colour == _cPLAY:
|
||||
self.bPLAY = _bitmapSet(self.bPLAY, x, y)
|
||||
else:
|
||||
self.bCOMP = _bitmapSet(self.bCOMP, x, y)
|
||||
self.low[x] += 1
|
||||
|
||||
# swipe down to drop, swipe left/right to exit
|
||||
def swipe(self, event):
|
||||
if event[0] == wasp.EventType.DOWN:
|
||||
if self.screen == _INTRO:
|
||||
self.x = _NMIDDLE
|
||||
self.bPLAY = 0
|
||||
self.bCOMP = 0
|
||||
self.low = bytearray(_NCOLUMN)
|
||||
self.screen = _PLAY
|
||||
self.foreground()
|
||||
if self.ngame[self.level.value] % 2:
|
||||
self._drop(rndColumn(), _cCOMP)
|
||||
elif self.screen == _PLAY and self.low[self.x] != _NROW:
|
||||
# play human PLAYer
|
||||
self._drop(self.x, _cPLAY)
|
||||
if _gameOver(self.bPLAY):
|
||||
wasp.watch.drawable.string('YOU WIN!', 0, 0, width=_NPIXEL)
|
||||
self.nwin[self.level.value] += 1
|
||||
self.screen = _GAMEOVER
|
||||
return
|
||||
|
||||
if sum(self.low) < 3:
|
||||
x = rndColumn()
|
||||
else:
|
||||
depth = self.level.value + 1
|
||||
if depth == 2 and sum(self.low) < 8:
|
||||
depth += 1
|
||||
v, x = _swapmin(self.bCOMP, self.bPLAY, self.low, depth, -_NBOARD, _NBOARD)
|
||||
self._drop(x, _cCOMP)
|
||||
|
||||
if _gameOver(self.bCOMP):
|
||||
wasp.watch.drawable.string('YOU LOSE!', 0, 0, width=_NPIXEL)
|
||||
self.screen = _GAMEOVER
|
||||
return
|
||||
|
||||
self._place(self.x, _NROW, _cPLAY)
|
||||
elif self.screen == _GAMEOVER:
|
||||
self.ngame[self.level.value] += 1
|
||||
self.screen = _INTRO
|
||||
self.foreground()
|
||||
|
|
@ -23,6 +23,8 @@ Watch faces
|
|||
|
||||
.. automodule:: word_clock
|
||||
|
||||
.. automodule:: resistor_clock
|
||||
|
||||
Built-in
|
||||
--------
|
||||
|
||||
|
|
@ -87,3 +89,5 @@ Games
|
|||
.. automodule:: puzzle15
|
||||
|
||||
.. automodule:: snake
|
||||
|
||||
.. automodule:: four_in_a_row
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
tomli
|
||||
recommonmark
|
||||
|
|
|
|||
BIN
res/screenshots/FourInARowApp.png
Normal file
BIN
res/screenshots/FourInARowApp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
res/screenshots/ResistorClockApp.png
Normal file
BIN
res/screenshots/ResistorClockApp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5 KiB |
|
|
@ -28,7 +28,7 @@ in pkgs.mkShell {
|
|||
] ++ ifLinux [
|
||||
bluepy
|
||||
]))
|
||||
pkgs.gcc-arm-embedded
|
||||
pkgs.gcc-arm-embedded-11
|
||||
pkgs.graphviz
|
||||
] ++ ifLinux [
|
||||
pkgs.bluez
|
||||
|
|
|
|||
|
|
@ -368,6 +368,13 @@ def handle_upload(c, fname, tname):
|
|||
|
||||
c.expect('>>> ')
|
||||
|
||||
def send_notification(c, title, body, id):
|
||||
msg= {'title': title, 'body': body}
|
||||
c.sendline(f'wasp.system.notify(id, {msg})')
|
||||
c.sendline(f'watch.vibrator.pulse(ms=wasp.system.notify_duration)')
|
||||
c.expect('>>> ')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Wasp-os command and control client')
|
||||
|
|
@ -377,6 +384,8 @@ if __name__ == '__main__':
|
|||
help="Report remaining battery charge")
|
||||
parser.add_argument('--bootloader', action='store_true',
|
||||
help="Reboot into the bootloader mode for OTA update")
|
||||
parser.add_argument('--body', default='Body',
|
||||
help='Body of the notification')
|
||||
parser.add_argument('--binary', action='store_true',
|
||||
help="Enable non-ASCII mode for upload command")
|
||||
parser.add_argument('--console', action='store_true',
|
||||
|
|
@ -389,6 +398,8 @@ if __name__ == '__main__':
|
|||
help='Execute the contents of a file')
|
||||
parser.add_argument('--eval',
|
||||
help='Execute the provided python string')
|
||||
parser.add_argument('--id', default=1,
|
||||
help='ID of the notification')
|
||||
parser.add_argument('--memfree', action='store_true',
|
||||
help='Report on the current memory usage.')
|
||||
parser.add_argument('--pull',
|
||||
|
|
@ -403,6 +414,10 @@ if __name__ == '__main__':
|
|||
help="Deliver an OTA update to the device")
|
||||
parser.add_argument('--rtc', action='store_true',
|
||||
help='Set the time on the wasp-os device')
|
||||
parser.add_argument('--send-notification', action='store_true',
|
||||
help='Send a notification to the wasp-os device (e.g. wasptool --send-notification --title hello --body world --id 1')
|
||||
parser.add_argument('--title', default='Title',
|
||||
help='Title of the notification')
|
||||
parser.add_argument('--upload',
|
||||
help='Copy the specified file to the wasp-os device')
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
|
|
@ -450,6 +465,9 @@ if __name__ == '__main__':
|
|||
if args.check_rtc:
|
||||
check_rtc(console)
|
||||
|
||||
if args.send_notification:
|
||||
send_notification(console, args.title, args.body, args.id)
|
||||
|
||||
if args.exec:
|
||||
handle_exec(console, args.exec)
|
||||
|
||||
|
|
|
|||
|
|
@ -150,9 +150,11 @@ class Draw565(object):
|
|||
if h is None:
|
||||
h = display.height - y
|
||||
|
||||
display.set_window(x, y, w, h)
|
||||
|
||||
remaining = w * h
|
||||
if remaining == 0:
|
||||
return
|
||||
|
||||
display.set_window(x, y, w, h)
|
||||
|
||||
# Populate the line buffer
|
||||
buf = display.linebuffer
|
||||
|
|
|
|||
88
watch_faces/resistor_clock.py
Normal file
88
watch_faces/resistor_clock.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
# Copyright (C) 2023 Tony Robinson based on the other WASP-OS clock faces
|
||||
"""Resistor Clock Face
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Following https://hackaday.com/2021/07/15/a-perfect-clock-for-any-hackers-ohm
|
||||
display the time as HHMMSS DDMMYYY in 6+8 bands using the resistor colour code.
|
||||
|
||||
This is very good if you want to learn the resistor colour codes, you pick them up very rapidly.
|
||||
|
||||
Colours taken from https://people.duke.edu/~ng46/topics/color-code.htm (grey moved from CCCCCC to C0C0C0)
|
||||
|
||||
Code adapted from fibonacci_clock.py by Johannes Wache
|
||||
Display the time in using the resistor colour codes
|
||||
|
||||
.. figure:: res/screenshots/ResistorClockApp.png
|
||||
:width: 179
|
||||
"""
|
||||
|
||||
import wasp
|
||||
|
||||
_COLOR = [ 0x0000, 0x9B26, 0xF800, 0xFCC0, 0xFFE0, 0x07E0, 0x001F, 0xF81F, 0xC618, 0xFFFF ]
|
||||
_TIMEX = bytearray([0, 40, 83, 123, 166, 206])
|
||||
_DATEX = bytearray([0, 30, 63, 93, 126, 156, 186, 216])
|
||||
|
||||
class ResistorClockApp():
|
||||
NAME = 'Resist'
|
||||
|
||||
def foreground(self):
|
||||
"""Activate the application.
|
||||
|
||||
Configure the status bar, redraw the display and request a periodic
|
||||
tick callback every second.
|
||||
"""
|
||||
wasp.system.bar.clock = False
|
||||
self._draw(True)
|
||||
wasp.system.request_tick(1000)
|
||||
|
||||
def sleep(self):
|
||||
"""Prepare to enter the low power mode.
|
||||
|
||||
:returns: True, which tells the system manager not to automatically
|
||||
switch to the default application before sleeping.
|
||||
"""
|
||||
return True
|
||||
|
||||
def wake(self):
|
||||
"""Return from low power mode.
|
||||
|
||||
Time will have changes whilst we have been asleep so we must
|
||||
udpate the display (but there is no need for a full redraw because
|
||||
the display RAM is preserved during a sleep.
|
||||
"""
|
||||
self._draw()
|
||||
|
||||
def tick(self, ticks):
|
||||
"""Periodic callback to update the display."""
|
||||
self._draw()
|
||||
|
||||
def preview(self):
|
||||
"""Provide a preview for the watch face selection."""
|
||||
wasp.system.bar.clock = False
|
||||
self._draw(True)
|
||||
|
||||
def _draw(self, redraw=False):
|
||||
"""Draw or lazily update the display."""
|
||||
|
||||
draw = wasp.watch.drawable
|
||||
|
||||
if redraw:
|
||||
now = wasp.watch.rtc.get_localtime()
|
||||
draw.fill()
|
||||
wasp.system.bar.draw()
|
||||
else:
|
||||
now = wasp.system.bar.update()
|
||||
if not now: # or self._sec == now[5]:
|
||||
return
|
||||
|
||||
# draw time
|
||||
for i, c in enumerate('%02d%02d%02d' % now[3:6]):
|
||||
draw.fill(x=_TIMEX[i],y=60,w=34,h=80,bg=_COLOR[int(c)])
|
||||
|
||||
# draw date
|
||||
for i, c in enumerate('%02d%02d%04d' % (now[2], now[1], now[0])):
|
||||
draw.fill(x=_DATEX[i],y=180,w=24,h=60,bg=_COLOR[int(c)])
|
||||
|
||||
# Record the second that is currently being displayed
|
||||
# self._sec = now[5]
|
||||
Loading…
Add table
Reference in a new issue