diff --git a/.gitignore b/.gitignore index ac605c4..89ca057 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /.vscode /venv +__pycache__ + /MicroPythonDumbDisplay.iml /MicroPythonDumbDisplay.code-workspace @@ -17,3 +19,4 @@ uDumbDisplay.code-workspace /OLD.md +/setup_OLD.py diff --git a/README.md b/README.md index 80274eb..2422c24 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# DumbDisplay MicroPython Library (v0.5.0) +# DumbDisplay MicroPython Library (v0.6.0) DumbDisplay MicroPython Library -- workable with Python 3 -- is a port of the [DumbDisplay Arduino Library](https://github.com/trevorwslee/Arduino-DumbDisplay) to MicroPython / Python 3 for the [DumbDisplay Android app](https://play.google.com/store/apps/details?id=nobody.trevorlee.dumbdisplay) @@ -135,7 +135,7 @@ Other then the `DumbDisplay` object, you will need to create one or more layer o |:--:| || -- `LayerGraphical` -- a graphical LCD that you can draw to with common drawing directives +- `LayerGraphical` -- a graphical LCD that you can draw to with common TFT LCD drawing directives ``` from dumbdisplay.core import * from dumbdisplay.layer_graphical import * @@ -148,31 +148,46 @@ Other then the `DumbDisplay` object, you will need to create one or more layer o |:--:| || -- `LayerSelection` -- a row / column / grid of TEXT based LCDs mostly for showing selection choices +- `Layer7SegmentRow` -- a single 7-segment digit, or a row of **n** 7-segments digits ``` from dumbdisplay.core import * - from dumbdisplay.layer_selection import * + from dumbdisplay.layer_7segrow import * dd = DumbDisplay() - l = LayerSelection(dd) + l = Layer7SegmentRow(dd) + ``` + + example: + |[`demo_Layer7SegmentRow()` in `dd_demo.py`](dd_demo.py)| + |:--:| + || +- +- `LayerTurtle` -- a Python Turtle like canvas that you can draw to using Python Turtle like commands + ``` + from dumbdisplay.core import * + from dumbdisplay.layer_turtle import * + dd = DumbDisplay() + l = LayerTurtle(dd) ``` example: - |[`demo_LayerSelection()` in `dd_demo.py`](dd_demo.py)| + |[`demo_LayerTurtle()` in `dd_demo.py`](dd_demo.py)| |:--:| - || + || -- `Layer7SegmentRow` -- a single 7-segment digit, or a row of **n** 7-segments digits + +- `LayerSelection` -- a row / column / grid of TEXT based LCDs mostly for showing selection choices ``` from dumbdisplay.core import * - from dumbdisplay.layer_7segrow import * + from dumbdisplay.layer_selection import * dd = DumbDisplay() - l = Layer7SegmentRow(dd) + l = LayerSelection(dd) ``` - + example: - |[`demo_Layer7SegmentRow()` in `dd_demo.py`](dd_demo.py)| + |[`demo_LayerSelection()` in `dd_demo.py`](dd_demo.py)| |:--:| - || + || + - `LayerPlotter` -- a "plotter" for plotting real-time data ``` @@ -213,7 +228,7 @@ Other then the `DumbDisplay` object, you will need to create one or more layer o as shown in the example * you can configure the joystick to be a horizontal or vertical slider by changing the `directions` parameter to `LayerJoystick` - - param `maxStickValue`: the max value of the stick; e.g. 255 or 1023 (the default); min is 15 + - param `max_stick_value`: the max value of the stick; e.g. 255 or 1023 (the default); min is 15 - param `directions`: "lr" or "hori": left-to-right; "tb" or "vert": top-to-bottom; "rl": right-to-left; "bt": bottom-to-top; use "+" combines the above like "lr+tb" to mean both directions; "" the same as "lr+tb" * feedback -- to be talked about later -- is enabled by default, and you can poll for feedback like @@ -334,6 +349,12 @@ MIT # Change History +v0.6.0 +- added DDRootLayer +- added LayerTurtle +- added more examples +- bug fixes + v0.5.0 - ported "level options" for LayerGraphical - ported LayerSelection diff --git a/_dev_test.py b/_dev_test.py index fbd9afa..6b9d8a0 100644 --- a/_dev_test.py +++ b/_dev_test.py @@ -1,8 +1,5 @@ import random -import time -import math - - +from dumbdisplay.full import * from dumbdisplay_examples.utils import create_example_wifi_dd @@ -25,6 +22,179 @@ def run_melody(): import samples.melody.main +def test_turtleTracked(): + from dumbdisplay.layer_turtle import LayerTurtleTracked + dd = create_example_wifi_dd() + l = LayerTurtleTracked(dd, 2000, 2000) + l.backgroundColor("ivory") + l.border(3, "blue") + if True: + sync = True + distance = 1 + angle = 10 + for i in range(0, 180): + l.penColor("red") + l.penSize(20) + l.forward(distance) + l.rightTurn(angle) + distance += 1 + coor = l.pos(sync=sync) + if True: + l.goTo(0, 0, with_pen=False) + l.penColor("blue") + l.penSize(10) + l.circle(15 * i, centered=True) + l.goTo(coor[0], coor[1], with_pen=False) + print(f"* LOOP[{i}] turtle pos: {coor}") + if not sync: + dd.sleep(0.1) + else: + l.forward(100) + coor = l.pos() + print(f"* INIT turtle pos: {coor}") + l.forward(100) + coor = l.pos() + print(f"* INIT 2 turtle pos: {coor}") + while True: + coor = l.pos() + print(f"* turtle pos: {coor}") + dd.sleep(2) + +def test_passive_turtleTracked(sync: bool = True): + from dumbdisplay.layer_turtle import LayerTurtleTracked + def _setup(dd: DumbDisplay) -> LayerTurtleTracked: + l = LayerTurtleTracked(dd, 2000, 2000) + l.backgroundColor("ivory") + l.border(3, "blue") + return l + def _loop(l: LayerTurtleTracked, i: int, distance: int): + l.penColor("red") + l.penSize(20) + l.forward(distance) + l.rightTurn(10) + if True: + r = 50 + int(random.random() * 30) + r2 = r + 10 + int(random.random() * 20) + a = int(random.random() * 45) + a2 = a + 10 + int(random.random() * 45) + centered = random.random() < 0.5 + l.penColor("green") + l.penSize(10) + shape = i % 7 + if shape == 0: + l.rectangle(r, r2, centered=centered) + elif shape == 1: + l.centeredPolygon(r, 5, inside=centered) + elif shape == 2: + l.polygon(r, 5) + elif shape == 3: + l.arc(r, r2, a, a2, centered=centered) + elif shape == 4: + l.circle(r, centered=centered) + elif shape == 5: + l.isoscelesTriangle(r, a) + else: + l.dot(r, color="darkblue") + coor = l.pos(sync=sync) + if i % 2 == 0: + l.goTo(0, 0, with_pen=False) + l.penColor("blue") + l.penSize(5) + r = 5 * i + l.circle(r, centered=True) + l.goTo(coor[0], coor[1], with_pen=False) + print(f"* LOOP[{i}] turtle pos: {coor}") + # if not sync: + # l.dd.sleep(0.2) + dd = create_example_wifi_dd() + distance = 1 + i = 0 + l: LayerTurtleTracked = None + freeze_for_steps = 10 + while True: + (connected, reconnecting) = dd.connectPassive() + if connected: + if l is None: + l = _setup(dd) + # if freeze_for_steps > 0: + # dd.freezeDrawing() + distance = 1 + i = 0 + else: + if reconnecting: + dd.masterReset() + l = None + else: + if i <= 300: + if i < 300: + # if freeze_for_steps > 0 and i == 0: + # dd.freezeDrawing() + _loop(l, i=i, distance=distance) + if freeze_for_steps > 0: + if (i + 1) % freeze_for_steps == 0: + dd.unfreezeDrawing(refreeze_after_draw=True) + print(f" --- unfreeze") + else: + if freeze_for_steps > 0: + dd.unfreezeDrawing() + print(f" --- FINAL[{i}] unfreeze") + else: + coor = l.pos(sync=sync) + print(f"* ENDED[{i}] turtle pos: {coor}") + l.dd.sleep(1) + if i > 305: + l.clear() + l.home(with_pen=False) + distance = 0 + i = -1 + distance = distance + 1 + i = i + 1 + + + +def test_auto_pin_remaining(): + rooted = True + + dd = create_example_wifi_dd() + + if rooted: + root_layer = DDRootLayer(dd, 200, 300) + root_layer.border(5, "darkgreen") + root_layer.backgroundColor("lightgreen") + status_layer = LayerGraphical(dd, 300, 80) + dd.configPinFrame(100, 100) + dd.pinLayer(status_layer, 0, 0, 100, 35) + + test_remaining = True + + ll = [] + if test_remaining: + dd.pinAutoPinLayers(AutoPin("H").build(), 0, 35, 100, 65) + for i in range(3): + l = LayerLedGrid(dd, i + 1, i + 1) + l.offColor("yellow") + ll.append(l) + dd.addRemainingAutoPinConfig(AutoPin("V", ll[0], ll[1]).build()) + else: + l1 = LayerLedGrid(dd) + l1.offColor("yellow") + l2 = LayerLedGrid(dd, 2, 2) + l2.offColor("pink") + dd.pinAutoPinLayers(AutoPin("H", l1, l2).build(), 0, 35, 100, 65) + ll.append(l1) + ll.append(l2) + + while True: + for l in ll: + l.toggle() + print("... ", end="") + dd.sleep(1) + print("...") + if dd.isReconnecting(): + break # since haven't setup for reconnection (like with recordLayerSetupCommands) ... may as well break out of the loop + print("... ASSUME disconnected") + + def test_margin(): from dumbdisplay.layer_ledgrid import LayerLedGrid dd = create_example_wifi_dd() @@ -51,22 +221,6 @@ def run_passive_blink_app(): app.run() -def run_sliding_puzzle_app(): - from dumbdisplay_examples.sliding_puzzle.sliding_puzzle_app import SlidingPuzzleApp - print(f"*** SlidingPuzzleApp ***") - suggest_move_from_dir_func = lambda board_manager: random.randint(0, 3) - app = SlidingPuzzleApp(dd=create_example_wifi_dd(), suggest_move_from_dir_func=suggest_move_from_dir_func) - app.run() - -def run_mnist_app(): - from dumbdisplay_examples.mnist.mnist_app import MnistApp - print(f"*** MnistApp ***") - inference_func = lambda board_manager: random.randint(0, 9) - app = MnistApp(dd=create_example_wifi_dd(), inference_func=inference_func) - app.run() - - - def test_read_readme(): from pathlib import Path this_directory = Path(__file__).parent @@ -81,27 +235,20 @@ def test_find_packages(): if __name__ == "__main__": - #test_LayerLedGrid(2, 2) - #test_LayerLcd() - #test_LayerGraphical() - #test_Layer7SegmentRow() - #test_LayerSelection() - #test_LayerPlotter() - - test_AutoPin() - - #run_passive_blink_app() - #run_sliding_puzzle_app() - #run_mnist_app() - #run_debug() - #run_doodle() - #run_graphical() - #run_melody() + test_passive_turtleTracked(sync=True) - #test_margin() - #run_debugBlepriority("MyBLEDevice") + #test_auto_pin_remaining() - #test_read_readme() - #test_find_packages() + # run_debug() + # run_doodle() + # run_graphical() + # run_melody() + # + # test_margin() + # + # run_debugBlepriority("MyBLEDevice") + # + # test_read_readme() + # test_find_packages() diff --git a/dd_demo.py b/dd_demo.py index a2422c9..8fad7e0 100644 --- a/dd_demo.py +++ b/dd_demo.py @@ -5,12 +5,12 @@ def _create_demo_dd(): - ''' + """ Create a DumbDisplay instance for demo purposes. If in MicroPython, it will connect using WiFi (***assuming*** `_my_secret.py`). If in Python, it will connect using Inet. Note that there should be a single DumbDisplay instance for the whole application. - ''' + """ from dumbdisplay.dumbdisplay import DumbDisplay if DumbDisplay.runningWithMicropython(): # connect using WIFI: @@ -28,13 +28,13 @@ def _create_demo_dd(): def demo_LayerLedGrid(col_count = 1, row_count = 1, sub_col_count = 1, sub_row_count = 1): - ''' + """ Demonstrate LayerLedGrid. :param col_count: number of columns in the grid :param row_count: number of rows in the grid :param sub_col_count: number of sub-columns in each grid cell :param sub_row_count: number of sub-rows in each grid cell - ''' + """ from dumbdisplay.layer_ledgrid import LayerLedGrid # Create a DumbDisplay instance @@ -77,7 +77,7 @@ def demo_LayerGraphical(): # Create a LayerGraphical layer and set it up, like background color and border # The LayerGraphical layer is 150 pixels wide and 100 pixels high # Note that this size is not the size on the DumbDisplay app's canvas - # All layers will be scaled to fit the DumbDisplay app's canvas, keeping the aspect ratio. + # All layers will be scaled to fit the DumbDisplay app's canvas, keeping the aspect ratio l = LayerGraphical(dd, 150, 100) l.backgroundColor("azure") l.border(3, "blue") @@ -155,14 +155,14 @@ def demo_LayerPlotter(): dd.timeslice() -def demo_LayerJoystick(maxStickValue: int = 1023, directions: str = ""): +def demo_LayerJoystick(max_stick_value: int = 1023, directions: str = ""): from dumbdisplay.layer_joystick import LayerJoystick # Create a DumbDisplay instance dd = _create_demo_dd() - # Create a LayerJoystick layer with the specified maxStickValue and directions, and set it up, like border and colors - l = LayerJoystick(dd, maxStickValue=maxStickValue, directions=directions) + # Create a LayerJoystick layer with the specified max_stick_value and directions, and set it up, like border and colors + l = LayerJoystick(dd, max_stick_value=max_stick_value, directions=directions) l.border(5, "blue") l.colors(stick_color="green", stick_outline_color="darkgreen") @@ -171,6 +171,30 @@ def demo_LayerJoystick(maxStickValue: int = 1023, directions: str = ""): if fb: print(f"* Feedback: {fb.type} at ({fb.x}, {fb.y})") +def demo_LayerTurtle(): + from dumbdisplay.layer_turtle import LayerTurtle + + # Create a DumbDisplay instance + dd = _create_demo_dd() + + # Create a LayerTurtle layer and set it up, like background color and border + # The LayerTurtle layer is 400 pixels wide and 250 pixels high, with (0, 0) at the center + # Note that this size is not the size on the DumbDisplay app's canvas + # All layers will be scaled to fit the DumbDisplay app's canvas, keeping the aspect ratio + l = LayerTurtle(dd, 400, 250) + l.backgroundColor("ivory") + l.border(3, "blue") + + distance = 5 + angle = 30 + for i in range(0, 50): + l.forward(distance) + l.rightTurn(angle) + distance += 1 + + while True: + dd.timeslice() + def demo_AutoPin(): from dumbdisplay.full import LayerLedGrid, LayerLcd, LayerGraphical, Layer7SegmentRow, LayerSelection, AutoPin @@ -291,7 +315,10 @@ def run_mnist_app(): if __name__ == "__main__": - demo_Feedback() + #run_sliding_puzzle_app() + #run_mnist_app() + #demo_AutoPin() + demo_LayerTurtle() if True: demo_LayerLedGrid(2, 2) @@ -301,6 +328,7 @@ def run_mnist_app(): demo_LayerSelection() demo_LayerPlotter() demo_LayerJoystick(directions="") # directions can be "", "lr" or "tb" + demo_LayerTurtle() demo_AutoPin() demo_Feedback() diff --git a/dumbdisplay/_exp__ddtunnel_basic.py b/dumbdisplay/_exp__ddtunnel_basic.py index 7cf9472..f994251 100644 --- a/dumbdisplay/_exp__ddtunnel_basic.py +++ b/dumbdisplay/_exp__ddtunnel_basic.py @@ -2,7 +2,7 @@ class _exp_DDBasicTunnel(_exp_DDTunnel): - '''tunnel is ONLY supported with DumbDisplayWifiBridge -- https://www.youtube.com/watch?v=0UhRmXXBQi8''' + """tunnel is ONLY supported with DumbDisplayWifiBridge -- https://www.youtube.com/watch?v=0UhRmXXBQi8""" def __init__(self, dd, end_point) -> None: super().__init__(dd, end_point) # tunnel_id = dd._createTunnel(end_point) diff --git a/dumbdisplay/ddcmds.py b/dumbdisplay/ddcmds.py new file mode 100644 index 0000000..34f489f --- /dev/null +++ b/dumbdisplay/ddcmds.py @@ -0,0 +1,3 @@ +DDC_setlevelanchor = "#3a" +DDC_movelevelanchorby = "#3b" +DDC_setlevelrotate = "#4e" diff --git a/dumbdisplay/ddimpl.py b/dumbdisplay/ddimpl.py index 4e1adf9..d48a8a3 100644 --- a/dumbdisplay/ddimpl.py +++ b/dumbdisplay/ddimpl.py @@ -1,7 +1,7 @@ import time, _thread from .ddiobase import DDInputOutput -from .ddlayer import DDLayer +from .ddlayer import DDLayer, _DD_INT_ARG # the followings will add time_ms and sleep_ms to the time module ... globally if not 'ticks_ms' in dir(time): @@ -12,10 +12,14 @@ #_DD_LIB_COMPATIBILITY = 2 #_DD_LIB_COMPATIBILITY = 7 # for :drag #_DD_LIB_COMPATIBILITY = 8 # for feedback type -_DD_LIB_COMPATIBILITY = 9 # joy stick valuerange (not used) +#_DD_LIB_COMPATIBILITY = 9 # joy stick valuerange (not used) #_DD_SID = 'MicroPython-c2' -_DD_SID = f"MicroPython-c{_DD_LIB_COMPATIBILITY}" +#_DD_SID = "MicroPython-c9" # joy stick valuerange (not used) +#_DD_SID = "MicroPython-c14" # bring forward since v0.5.1 +_DD_SID = "MicroPython-c15" + +_ROOT_LAYER_ID = "00" # hardcoded _DBG_TNL = False @@ -26,16 +30,51 @@ _VALIDATE_GAP: int = 1000 _RECONNECTING_VALIDATE_GAP: int = 500 +_INIT_ACK_SEQ = 0 + +def _NEXT_ACK_SEQ(ack_seq: int) -> int: + if True: + ack_seq = (ack_seq + 1) % 62 + else: + ack_seq = (ack_seq + 1) % 10 + return ack_seq + +def _ACK_SEQ_TO_ACK_STR(ack_seq: int) -> str: + if True: + if ack_seq < 10: + return str(ack_seq) + elif ack_seq < 36: + return chr(ord('A') + ack_seq - 10) + else: + return chr(ord('a') + ack_seq - 36) + else: + return str(ack_seq) + +def _ACK_STR_TO_ACK_SEQ(ack_str: str) -> int: + if True: + if ack_str >= '0' and ack_str <= '9': + return int(ack_str) + elif ack_str >= 'A' and ack_str <= 'Z': + return ord(ack_str) - ord('A') + 10 + elif ack_str >= 'a' and ack_str <= 'z': + return ord(ack_str) - ord('a') + 36 + else: + raise ValueError("Invalid ACK string: " + ack_str) + else: + return int(ack_str) + + + class IOProxy: - def __init__(self, io): + def __init__(self, io: DDInputOutput): self._io = io self.data: str = '' - self.last_keep_alive_ms = 0 - self.reconnect_keep_alive_ms = 0 - self.reconnecting = False - self.reconnect_enabled = False - self.reconnect_RC_id = None + self.last_keep_alive_ms: int = 0 + self.reconnect_keep_alive_ms: int = 0 + self.reconnecting: bool = False + self.reconnect_enabled: bool = False + self.reconnect_RC_id: str = None def available(self): done = '\n' in self.data while (not done) and self._io.available(): @@ -44,7 +83,9 @@ def available(self): done = '\n' in s return done def read(self) -> str: - #print(self.data) + # if True: # TODO: disable printing of received data + # data = self.data[:-1] + # print(data) idx = self.data.index('\n') s = self.data[0:idx] self.data = self.data[idx + 1:] @@ -54,7 +95,7 @@ def read(self) -> str: # return self.data #def clear(self): # self.data = '' - def print(self, s): + def print(self, s: str): self._io.print(s) def keepAlive(self): self.last_keep_alive_ms = time.ticks_ms() @@ -170,6 +211,7 @@ def __init__(self, io: DDInputOutput): self._connected = False self._compatibility = 0 self._connected_iop: IOProxy = None + self._root_layer = None self._layers: dict[str, DDLayer] = {} self._tunnels: dict = {} self.last_validate_ms = 0 @@ -178,9 +220,9 @@ def timeslice(self): self._checkForFeedback() def delay(self, seconds: float = 0): - ''' + """ deprecated; use sleep() instead - ''' + """ self.sleep_ms(seconds * 1000) def sleep(self, seconds: float = 0): @@ -201,6 +243,8 @@ def sleep_ms(self, ms: int = 0): def _master_reset(self): + if self._root_layer is not None: + self._root_layer.release() layers = set(self._layers.values()) for layer in layers: layer.release() @@ -253,16 +297,29 @@ def _createLayer(self, layer_type: str, *params) -> str: layer_id = str(self._allocLayerNid()) self._sendCommand(layer_id, "SU", layer_type, *params) return layer_id + def _setRootLayer(self, width: int, height: int, contained_alignment: str) -> str: + if self._root_layer is not None: + self._root_layer.release() + self._connect() + self._sendCommand(None, "ROOT", _DD_INT_ARG(width), _DD_INT_ARG(height), contained_alignment); + layer_id = _ROOT_LAYER_ID + return layer_id def _reorderLayer(self, layer_id: str, how: str): self._sendCommand(layer_id, "REORD", how) def _deleteLayer(self, layer_id: str): self._sendCommand(layer_id, "DEL") def _onCreatedLayer(self, layer: DDLayer): - self._layers[layer.layer_id] = layer + if layer.layer_id == _ROOT_LAYER_ID: + _root_layer = layer + else: + self._layers[layer.layer_id] = layer # def _onCreatedLayer(self, layer): # self.layers[layer.layer_id] = layer def _onDeletedLayer(self, layer_id: str): - del self._layers[layer_id] + if layer_id == _ROOT_LAYER_ID: + self._root_layer = None + else: + del self._layers[layer_id] # def _ensureConnectionReady(self): # if self._connected: @@ -364,12 +421,15 @@ def _sendSpecial(self, special_type: str, special_id: str, special_command: str, self._io.print(special_data) self._io.print('\n') #self.switchDebugLed(False) - def _sendCommand(self, layer_id: str, command: str, *params): + def _sendCommand(self, layer_id: str, command: str, *params, ack_seq: int = None): self._checkForFeedback() #self.switchDebugLed(True) try: if layer_id is not None: self._io.print(layer_id) + if ack_seq is not None: + self._io.print('@') + self._io.print(_ACK_SEQ_TO_ACK_STR(ack_seq)) self._io.print('.') self._io.print(command) for i in range(0, len(params)): @@ -386,6 +446,11 @@ def _sendCommand(self, layer_id: str, command: str, *params): #self.switchDebugLed(False) + def _is_reconnecting(self) -> bool: + if self._connected_iop is not None: + return self._connected_iop.reconnecting + else: + return False def _checkForFeedback(self): feedback = self._readFeedback() @@ -398,7 +463,7 @@ def _checkForFeedback(self): pass #self._onFeedbackKeepAlive() else: - #print(feedback)#### + #print(feedback) # TODO: disable debug if feedback.startswith('= "0" and type <= "9": - x = int(type) + fb_type = feedback[0:idx] + if len(fb_type) == 1 and fb_type >= "0" and fb_type <= "9": + x = int(fb_type) y = 0 - type = "click" + text = None + fb_type = "click" else: - if type == "C": - type = "click" - elif type == "D": - type = "doubleclick" - elif type == "L": - type = "longpress" - elif type == "M": - type = "move" - elif type == "u": - type = "up" - elif type == "d": - type = "down" - elif type == "c": - type = "custom" + if fb_type == "C": + fb_type = "click" + elif fb_type == "D": + fb_type = "doubleclick" + elif fb_type == "L": + fb_type = "longpress" + elif fb_type == "M": + fb_type = "move" + elif fb_type == "u": + fb_type = "up" + elif fb_type == "d": + fb_type = "down" + elif fb_type == "c": + fb_type = "custom" feedback = feedback[idx + 1:] idx = feedback.index(',') - x = int(feedback[0:idx]) - y = int(feedback[idx + 1:]) + idx2 = feedback.find(',', idx + 1) + x_str = feedback[0:idx] + if x_str != "": + x = int(x_str) + else: + x = 0 + if idx2 == -1: + y_str = feedback[idx + 1:] + text = None + else: + y_str = feedback[idx + 1:idx2] + text = feedback[idx2 + 1:] + if y_str != "": + y = int(y_str) + else: + y = 0 else: - type = "click" + fb_type = "click" x = 0 y = 0 + text = None layer = self._layers.get(lid) if layer is not None: - layer._handleFeedback(type, x, y) - except: - pass + if fb_type.startswith("_"): + ack_seq = fb_type[1:] if len(fb_type) > 1 else None + layer._handleAck(ack_seq, x, y, text) + else: + layer._handleFeedback(fb_type, x, y) # TODO: set text as feedback text + except Exception as e: + #print("** EXCEPT: " + feedback) + if True: + raise e def _readFeedback(self) -> str: - (need_reconnect, keep_alive_diff_ms) = self._validateConnection() + validate_res = self._validateConnection() + if validate_res is None: + return None + (need_reconnect, keep_alive_diff_ms) = validate_res if need_reconnect: self.onDetectedDisconnect(keep_alive_diff_ms) if not self._connected_iop.available(): @@ -491,6 +582,8 @@ def _validateConnection(self): else: (need_reconnect, keep_alive_diff_ms) = (None, None) return (need_reconnect, keep_alive_diff_ms) + else: + return None def _setReconnectRCId(self, rc_id: str): if self._connected_iop: self._connected_iop.setReconnectRCId(rc_id) diff --git a/dumbdisplay/ddio_ble.py b/dumbdisplay/ddio_ble.py index 4a59c4c..5efedd9 100644 --- a/dumbdisplay/ddio_ble.py +++ b/dumbdisplay/ddio_ble.py @@ -71,12 +71,12 @@ def _ble_irq(self, event, data): #print("E:" + str(event)) if event == 1: - '''Central disconnected''' + """Central disconnected""" self._conn_handle, _, _, = data self._connected() elif event == 2: - '''Central disconnected''' + """Central disconnected""" conn_handle, _, _, = data if conn_handle == self._conn_handle: self.conn_handle = None @@ -84,7 +84,7 @@ def _ble_irq(self, event, data): self._advertiser() elif event == 3:#4: - '''New message received''' + """New message received""" #conn_handle, value_handle, = data #print("...") buffer = self.ble.gatts_read(self._rx) diff --git a/dumbdisplay/ddio_inet.py b/dumbdisplay/ddio_inet.py index 8dde6b6..c685186 100644 --- a/dumbdisplay/ddio_inet.py +++ b/dumbdisplay/ddio_inet.py @@ -3,7 +3,7 @@ import socket class DDIOInet(DDIOSocket): - '''Python Internet connection''' + """Python Internet connection""" def __init__(self, port = DD_DEF_PORT): super().__init__(port) self.ip = self._get_ip() diff --git a/dumbdisplay/ddio_socket.py b/dumbdisplay/ddio_socket.py index 50917f8..9d3f29b 100644 --- a/dumbdisplay/ddio_socket.py +++ b/dumbdisplay/ddio_socket.py @@ -4,7 +4,7 @@ import socket class DDIOSocket(DDInputOutput): - def __init__(self, port): + def __init__(self, port: int): self.ip = "???" self.port = port self.sock = None diff --git a/dumbdisplay/ddio_wifi.py b/dumbdisplay/ddio_wifi.py index 2a5620f..51229e2 100644 --- a/dumbdisplay/ddio_wifi.py +++ b/dumbdisplay/ddio_wifi.py @@ -6,7 +6,7 @@ import network class DDIOWifi(DDIOSocket): - def __init__(self, ssid, password, port = DD_DEF_PORT): + def __init__(self, ssid: str, password: str, port: int = DD_DEF_PORT): if ssid is None: raise Exception('SSID not provided') super().__init__(port) diff --git a/dumbdisplay/ddlayer.py b/dumbdisplay/ddlayer.py index a2f3bb8..28c6151 100644 --- a/dumbdisplay/ddlayer.py +++ b/dumbdisplay/ddlayer.py @@ -1,4 +1,3 @@ - def DD_RGB_COLOR(r: int, g: int, b: int): return r * 0x10000 + g * 0x100 + b @@ -38,16 +37,16 @@ def _DD_COLOR_ARG(c): class DDFeedback: - ''' + """ type: can be "click", "doubleclick", "longpress" - ''' - def __init__(self, type, x, y): - self.type = type + """ + def __init__(self, fb_type: str, x: int, y: int): + self.type = fb_type self.x = x self.y = y class DDLayer: - def __init__(self, dd, layer_id: str): + def __init__(self, dd: 'DumbDisplayImpl', layer_id: str): self.dd = dd self.layer_id = layer_id self._feedback_handler = None @@ -55,28 +54,28 @@ def __init__(self, dd, layer_id: str): #self.customData = "" dd._onCreatedLayer(self) def visibility(self, visible: bool): - '''set layer visibility''' + """set layer visibility""" self.dd._sendCommand(self.layer_id, "visible", _DD_BOOL_ARG(visible)) def disabled(self, disabled: bool = True): - '''set layer disabled''' + """set layer disabled""" self.dd._sendCommand(self.layer_id, "disabled", _DD_BOOL_ARG(disabled)) def transparent(self, transparent: bool): self.dd._sendCommand(self.layer_id, "transparent", _DD_BOOL_ARG(transparent)) def opacity(self, opacity: int): - '''set layer opacity percentage -- 0 to 100''' + """set layer opacity percentage -- 0 to 100""" self.dd._sendCommand(self.layer_id, "opacity", str(opacity)) def alpha(self, alpha: int): - '''set layer alpha -- 0 to 255''' + """set layer alpha -- 0 to 255""" self.dd._sendCommand(self.layer_id, "alpha", str(alpha)) def border(self, size: float, color: str, shape: str = "flat", extra_size: float = 0): - ''' + """ :param size: unit depends on the layer type: - LcdLayer; each character is composed of pixels - 7SegmentRowLayer; each 7-segment is composed of fixed 220 x 320 pixels - LedGridLayer; a LED is considered as a pixel :param shape: can be "flat", "round", "raised" or "sunken" :param extra_size just added to size; however if shape is "round", it affects the "roundness" - ''' + """ if _DD_FLOAT_IS_ZERO(extra_size): # was type(extra_size) == int and extra_size == 0 self.dd._sendCommand(self.layer_id, "border", _DD_FLOAT_ARG(size), _DD_COLOR_ARG(color), shape) else: @@ -84,7 +83,7 @@ def border(self, size: float, color: str, shape: str = "flat", extra_size: float def noBorder(self): self.dd._sendCommand(self.layer_id, "border") def padding(self, left: float, top: float = None, right: float = None, bottom: float = None): - '''see border() for size unit''' + """see border() for size unit""" if top is None and right is None and bottom is None: self.dd._sendCommand(self.layer_id, "padding", _DD_FLOAT_ARG(left)) else: @@ -98,7 +97,7 @@ def padding(self, left: float, top: float = None, right: float = None, bottom: f def noPadding(self): self.dd._sendCommand(self.layer_id, "padding") def margin(self, left: float, top: float = None, right: float = None, bottom: float = None): - '''see border() for size unit''' + """see border() for size unit""" if top is None and right is None and bottom is None: self.dd._sendCommand(self.layer_id, "margin", _DD_FLOAT_ARG(left)) else: @@ -112,10 +111,10 @@ def margin(self, left: float, top: float = None, right: float = None, bottom: fl def noMargin(self): self.dd._sendCommand(self.layer_id, "margin") def backgroundColor(self, color, opacity = 100): - ''' + """ :param opacity: background opacity (0 - 100) :return: - ''' + """ if opacity < 100: self.dd._sendCommand(self.layer_id, "bgcolor", _DD_COLOR_ARG(color), str(opacity)) else: @@ -123,7 +122,7 @@ def backgroundColor(self, color, opacity = 100): def noBackgroundColor(self): self.dd._sendCommand(self.layer_id, "nobgcolor") def clear(self): - '''clear the layer''' + """clear the layer""" self.dd._sendCommand(self.layer_id, "clear") def flash(self): self.dd._sendCommand(self.layer_id, "flash") @@ -132,7 +131,7 @@ def flashArea(self, x, y): # def writeComment(self, comment): # self.dd.writeComment(comment) def enableFeedback(self, auto_feedback_method: str = "", feedback_handler = None, allowed_feedback_types: str = ""): - ''' + """ :param auto_feedback_method: . "" -- no auto feedback flash (need explicitly call to flashArea() or flash() when detected feedback) . "f" -- flash the default way (layer + border) @@ -145,19 +144,19 @@ def enableFeedback(self, auto_feedback_method: str = "", feedback_handler = None . type -- "click" . x, y -- the "area" on the layer where was clicked . *args -- for future-proofing - ''' + """ self._feedback_handler = feedback_handler # TODO: have a feedback_handler_ex that will be called with kwargs self._feedbacks = [] self.dd._sendCommand(self.layer_id, "feedback", _DD_BOOL_ARG(True), auto_feedback_method, allowed_feedback_types) def disableFeedback(self): - '''disable feedback''' + """disable feedback""" self.dd._sendCommand(self.layer_id, "feedback", _DD_BOOL_ARG(False)) self._feedback_handler = None def getFeedback(self) -> DDFeedback: - ''' + """ get any feedback as the structure {type, x, y} :return: None if none (or when "handler" set) - ''' + """ self.dd._checkForFeedback() if len(self._feedbacks) > 0: (type, x, y) = self._feedbacks.pop(0) @@ -168,31 +167,33 @@ def getFeedback(self) -> DDFeedback: # self._feedback_handler = feedback_handler # self._shipFeedbacks() def reorder(self, how: str): - ''' + """ recorder the layer :param how: can be "T" for top; or "B" for bottom; "U" for up; or "D" for down - ''' + """ self.dd._reorderLayer(self.layer_id, how) def release(self): - '''release the layer''' + """release the layer""" self.dd._deleteLayer(self.layer_id) self.dd._onDeletedLayer(self.layer_id) self.dd = None - def pinLayer(self, uLeft: int, uTop: int, uWidth: int, uHeight: int, align: str = ""): - self.dd._pinLayer(self.layer_id, uLeft, uTop, uWidth, uHeight, align) + def pinLayer(self, u_left: int, u_top: int, u_width: int, u_height: int, align: str = ""): + #self.dd.pinLayer(self.layer_id, u_left, u_top, u_width, u_height, align) + self.dd.pinLayer(self, u_left, u_top, u_width, u_height, align) def reorderLayer(self, how: str): self.dd._reorderLayer(self.layer_id, how) - def _handleFeedback(self, type, x, y): + def _handleFeedback(self, fb_type: str, x: int, y: int): #print("RAW FB: " + self.layer_id + '.' + type + ':' + str(x) + ',' + str(y)) if self._feedback_handler is not None: - # if False: # TODO: text parameters - # print(self._feedback_handler.__code__.co_argcount) - self._feedback_handler(self, type, x, y) + self._feedback_handler(self, fb_type, x, y) else: - self._feedbacks.append((type, x, y)) + self._feedbacks.append((fb_type, x, y)) # self._shipFeedbacks() + def _handleAck(self, x: int, y: int, text: str): + pass + diff --git a/dumbdisplay/ddlayer_7segrow.py b/dumbdisplay/ddlayer_7segrow.py index 0e710aa..05be562 100644 --- a/dumbdisplay/ddlayer_7segrow.py +++ b/dumbdisplay/ddlayer_7segrow.py @@ -1,48 +1,51 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer from .ddlayer import _DD_COLOR_ARG class DDLayer7SegmentRow(DDLayer): - '''A row of 7 Segments''' - def __init__(self, dd, digit_count = 1): - ''' + """A row of 7 Segments""" + def __init__(self, dd: DumbDisplayImpl, digit_count = 1): + """ :param dd: DumbDisplay object :param digit_count: number of digits / # rows - ''' + """ layer_id = dd._createLayer("7segrow", str(digit_count)) super().__init__(dd, layer_id) def segmentColor(self, color): - '''set segment color''' + """set segment color""" self.dd._sendCommand(self.layer_id, "segcolor", _DD_COLOR_ARG(color)) def turnOn(self, segments, digit_idx = 0): - ''' + """ turn on one or more segments :param segments: each character represents a segment to turn off - 'a', 'b', 'c', 'd', 'e', 'f', 'g', '.' - ''' + """ self.dd._sendCommand(self.layer_id, "segon", segments, str(digit_idx)) def turnOff(self, segments, digit_idx = 0): - '''turn off one or more segments -- see turnOn()''' + """turn off one or more segments -- see turnOn()""" self.dd._sendCommand(self.layer_id, "segoff", segments, str(digit_idx)) def setOn(self, segments = "", digit_idx = 0): - '''like turnOn(), exception that the digit will be cleared first;''' - '''empty segments basically means turn all segments of the digit off''' + """ + like turnOn(), except that the digit will be cleared first; + empty segments basically means turn all segments of the digit off + """ self.dd._sendCommand(self.layer_id, "setsegon", segments, str(digit_idx)) def showDigit(self, digit: int, digit_idx: int = 0): - ''' + """ show a digit - ''' + """ self.dd._sendCommand(self.layer_id, "showdigit", str(digit) , str(digit_idx)) def showNumber(self, number): - '''show number''' + """show number""" self.dd._sendCommand(self.layer_id, "shownumber", str(number)) def showHexNumber(self, number): - '''show HEX number''' + """show HEX number""" self.dd._sendCommand(self.layer_id, "showhex", str(number)) def showFormatted(self, formatted): - ''' + """ show formatted number (even number with hex digits) -- e.g. "12.00", "00.34", "-.12", "0ff" - ''' + """ self.dd._sendCommand(self.layer_id, "showformatted", str(formatted)) diff --git a/dumbdisplay/ddlayer_graphical.py b/dumbdisplay/ddlayer_graphical.py index 0af0f1e..c808113 100644 --- a/dumbdisplay/ddlayer_graphical.py +++ b/dumbdisplay/ddlayer_graphical.py @@ -1,26 +1,21 @@ #from ._ddlayer import DDLayer +from .ddimpl import DumbDisplayImpl from .ddlayer_multilevel import DDLayerMultiLevel from .ddlayer import _DD_COLOR_ARG, _DD_BOOL_ARG, _DD_INT_ARG -class DDLayerGraphical(DDLayerMultiLevel): - '''Graphical LCD''' - def __init__(self, dd, width, height): - ''' - :param dd: DumbDisplay object - :param width: width - :param height: height - ''' - layer_id = dd._createLayer("graphical", _DD_INT_ARG(width), _DD_INT_ARG(height)) + +class DDLayerGraphicalBase(DDLayerMultiLevel): + def __init__(self, dd: DumbDisplayImpl, layer_id: str): super().__init__(dd, layer_id) def setCursor(self, x, y): self.dd._sendCommand(self.layer_id, "setcursor", str(x), str(y)) def moveCursorBy(self, by_x, by_y): self.dd._sendCommand(self.layer_id, "movecursorby", str(by_x), str(by_y)) def setTextColor(self, color, bg_color = ""): - ''' + """ set the text color (i.e. pen color) - :param bg_color: "" means means default - ''' + :param bg_color: "" means default + """ self.dd._sendCommand(self.layer_id, "textcolor", _DD_COLOR_ARG(color), _DD_COLOR_ARG(bg_color)) def setTextSize(self, size): self.dd._sendCommand(self.layer_id, "textsize", str(size)) @@ -38,23 +33,23 @@ def println(self, text = ""): def fillScreen(self, color): self.dd._sendCommand(self.layer_id, "fillscreen", _DD_COLOR_ARG(color)) def drawChar(self, x, y, char, color, bg_color = "", size = 0): - ''' + """ :param char: char to draw :param bg_color: "" means default :param size: 0 means defajult - ''' + """ self.dd._sendCommand(self.layer_id, "drawchar", str(x), str(y), _DD_COLOR_ARG(color), _DD_COLOR_ARG(bg_color), str(size), char) def drawStr(self, x, y, string, color, bg_color = "", size = 0): - ''' + """ :param bg_color: "" means default :param size: 0 means default - ''' + """ self.dd._sendCommand(self.layer_id, "drawstr", str(x), str(y), _DD_COLOR_ARG(color), _DD_COLOR_ARG(bg_color), str(size), string) def drawTextLine(self, text: str, y: int, align: str = "L", color: str = "", bgColor: str = "", size: int = 0): - ''' + """ similar to drawStr(), but draw string as a text line at (0, y) with alignment option :param align 'L', 'C', or 'R' - ''' + """ self.dd._sendCommand(self.layer_id, "drawtextline", _DD_INT_ARG(y), align, color, bgColor, _DD_INT_ARG(size), text) def drawPixel(self, x, y, color): self.dd._sendCommand(self.layer_id, "drawpixel", str(x), str(y), _DD_COLOR_ARG(color)) @@ -70,11 +65,11 @@ def drawRoundRect(self, x, y, w, h, r, color, filled = False): self.dd._sendCommand(self.layer_id, "drawroundrect", str(x), str(y), str(w), str(h), str(r), _DD_COLOR_ARG(color), _DD_BOOL_ARG(filled)) def drawImageFile(self, imageFileName: str, x: int = 0, y: int = 0, w: int = 0, h: int = 0, options = ""): - ''' + """ draw image file in cache (if not already loaded to cache, load it) :param x,y: position of the left-top corner :param w,h: image size to scale to; if both 0, will not scale, if any 0, will scale keeping aspect ratio - ''' + """ if x == 0 and y == 0 and w == 0 and h == 0: self.dd._sendCommand(self.layer_id, "drawimagefile", imageFileName, options) elif x == 0 and y == 0: @@ -88,18 +83,18 @@ def drawImageFile(self, imageFileName: str, x: int = 0, y: int = 0, w: int = 0, else: self.dd._sendCommand(self.layer_id, "drawimagefile", imageFileName, _DD_INT_ARG(x), _DD_INT_ARG(y), _DD_INT_ARG(w), _DD_INT_ARG(h), options) def drawImageFileFit(self, imageFileName: str, x: int = 0, y: int = 0, w: int = 0, h: int = 0, options: str = ""): - ''' + """ draw image file in cache (if not already loaded to cache, load it) :param x,y,w,h: rect to draw the image; 0 means the default value :param options (e.g. "LB"): left align "L"; right align "R"; top align "T"; bottom align "B"; default to fit centered - ''' + """ if x == 0 and y == 0 and w == 0 and h == 0 and options == "": self.dd._sendCommand(self.layer_id, "drawimagefilefit", imageFileName) else: self.dd._sendCommand(self.layer_id, "drawimagefilefit", imageFileName, _DD_INT_ARG(x), _DD_INT_ARG(y), _DD_INT_ARG(w), _DD_INT_ARG(h), options) def forward(self, distance): - '''draw forward relative to cursor position''' + """draw forward relative to cursor position""" self.dd._sendCommand(self.layer_id, "fd", str(distance)) def leftTurn(self, angle): self.dd._sendCommand(self.layer_id, "lt", str(angle)) @@ -126,20 +121,45 @@ def triangle(self, side1, angle, side2): def isoscelesTriangle(self, side, angle): self.dd._sendCommand(self.layer_id, "trisas", str(side), str(angle)) def polygon(self, side, vertex_count): - '''draw polygon given side and vertex count''' + """draw polygon given side and vertex count""" self.dd._sendCommand(self.layer_id, "poly", str(side), str(vertex_count)) def centeredPolygon(self, radius, vertex_count, inside = False): - ''' + """ draw polygon enclosed in an imaginary centered circle - given circle radius and vertex count - whether inside the imaginary circle or outside of it - ''' + """ self.dd._sendCommand(self.layer_id, "cpolyin" if inside else "cpoly", str(radius), str(vertex_count)) def write(self, text, draw = False): - ''' + """ write text; will not auto wrap :param draw: draw means draw the text (honor the heading direction) - ''' + """ self.dd._sendCommand(self.layer_id, "drawtext" if draw else "write", text) +class DDLayerGraphical(DDLayerGraphicalBase): + def __init__(self, dd: DumbDisplayImpl, width: int, height: int): + """ + :param dd: DumbDisplay object + :param width: width + :param height: height + """ + layer_id = dd._createLayer("graphical", _DD_INT_ARG(width), _DD_INT_ARG(height)) + super().__init__(dd, layer_id) + + +class DDRootLayer(DDLayerGraphicalBase): + """ + it is [the only] root layer of the DumbDisplay, the foundation layer on which all other layers are contained; + it is basically a graphical layer, but without feedback capability + """ + def __init__(self, dd: DumbDisplayImpl, width: int, height: int, contained_alignment: str = ""): + """ + set the root layer of the DumbDisplay; it is basically a graphical layer + :param dd: DumbDisplay object + :param width: width + :param height: height + """ + layer_id = dd._setRootLayer(width, height, contained_alignment) + super().__init__(dd, layer_id) diff --git a/dumbdisplay/ddlayer_joystick.py b/dumbdisplay/ddlayer_joystick.py index b244d1a..2daaf4a 100644 --- a/dumbdisplay/ddlayer_joystick.py +++ b/dumbdisplay/ddlayer_joystick.py @@ -1,36 +1,38 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer from .ddlayer import _DD_COLOR_ARG, _DD_BOOL_ARG, _DD_INT_ARG, _DD_FLOAT_ARG + class DDLayerJoystick(DDLayer): - '''Virtual joystick''' - def __init__(self, dd, maxStickValue: int = 1023, directions: str = "", stickLookScaleFactor: float = 1.0): - ''' + """Virtual joystick""" + def __init__(self, dd: DumbDisplayImpl, max_stick_value: int = 1023, directions: str = "", stickLookScaleFactor: float = 1.0): + """ :param dd: DumbDisplay object - :param maxStickValue: the max value of the stick; e.g. 255 or 1023 (the default); min is 15 + :param max_stick_value: the max value of the stick; e.g. 255 or 1023 (the default); min is 15 :param directions: "lr" or "hori": left-to-right; "tb" or "vert": top-to-bottom; "rl": right-to-left; "bt": bottom-to-top; use "+" combines the above like "lr+tb" to mean both directions; "" the same as "lr+tb" :param stickLookScaleFactor: the scaling factor of the stick (UI); 1 by default - ''' - layer_id = dd._createLayer("joystick", _DD_INT_ARG(maxStickValue), directions, _DD_FLOAT_ARG(stickLookScaleFactor)) + """ + layer_id = dd._createLayer("joystick", _DD_INT_ARG(max_stick_value), directions, _DD_FLOAT_ARG(stickLookScaleFactor)) super().__init__(dd, layer_id) def autoRecenter(self, auto_recenter: bool = True): self.dd._sendCommand(self.layer_id, "autorecenter", _DD_BOOL_ARG(auto_recenter)) def colors(self, stick_color: str = "", stick_outline_color: str = "", socket_color: str = "", socket_outline_color = ""): self.dd._sendCommand(self.layer_id, "colors", _DD_COLOR_ARG(stick_color), _DD_COLOR_ARG(stick_outline_color), _DD_COLOR_ARG(socket_color), _DD_COLOR_ARG(socket_outline_color)) def moveToPos(self, x: int, y: int, send_feedback: bool = False): - ''' + """ move joystick position (if joystick is single directional, will only move in the movable direction) :param bg_color: "" means default :param x: x to move to :param y: y to move to :param send_feedback: if true, will send "feedback" for the move (regardless of the current position) - ''' + """ self.dd._sendCommand(self.layer_id, "movetopos", str(x), str(y), _DD_BOOL_ARG(send_feedback)) def moveToCenter(self, send_feedback: bool = False): - ''' + """ move joystick to the center :param send_feedback: if true, will send "feedback" for the move (regardless of the current position) - ''' + """ self.dd._sendCommand(self.layer_id, "movetocenter", _DD_BOOL_ARG(send_feedback)) diff --git a/dumbdisplay/ddlayer_lcd.py b/dumbdisplay/ddlayer_lcd.py index afc53ad..6f192df 100644 --- a/dumbdisplay/ddlayer_lcd.py +++ b/dumbdisplay/ddlayer_lcd.py @@ -1,20 +1,22 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer from .ddlayer import _DD_BOOL_ARG from .ddlayer import _DD_COLOR_ARG + class DDLayerLcd(DDLayer): - '''LCD''' - def __init__(self, dd, col_count: int = 16, row_count: int = 2, char_height: int = 0, font_name: str = ''): - ''' + """LCD""" + def __init__(self, dd: DumbDisplayImpl, col_count: int = 16, row_count: int = 2, char_height: int = 0, font_name: str = ''): + """ :param dd: DumbDisplay object :param col_count: number of columns :param row_count: numer of rows :param char_height: char height :param font_name: font name - ''' + """ layer_id = dd._createLayer("lcd", str(col_count), str(row_count), str(char_height), font_name) super().__init__(dd, layer_id) - def print(self, text): + def print(self, text: str): self.dd._sendCommand(self.layer_id, "print", str(text)) def home(self): self.dd._sendCommand(self.layer_id, "home") @@ -36,20 +38,24 @@ def scrollDisplayLeft(self): self.dd._sendCommand(self.layer_id, "scrollleft") def scrollDisplayRight(self): self.dd._sendCommand(self.layer_id, "scrollright") - def writeLine(self, text, y = 0, align = "L"): - '''write text as a line, with alignment "L", "C", or "R"''' + def writeLine(self, text: str, y: int = 0, align: str = "L"): + """ + write text as a line, with alignment 'L', 'C', or 'R' + """ self.dd._sendCommand(self.layer_id, "writeline", str(y), align, str(text)) - def writeCenteredLine(self, text, y = 0): - '''write text as a line, with align "centered"''' + def writeCenteredLine(self, text: str, y: int = 0): + """ + write text as a line, with align "centered" + """ self.dd._sendCommand(self.layer_id, "writeline", str(y), "C", str(text)) def pixelColor(self, color): - '''set pixel color''' + """set pixel color""" self.dd._sendCommand(self.layer_id, "pixelcolor", _DD_COLOR_ARG(color)) def bgPixelColor(self, color): - '''set "background" (off) pixel color''' + """set "background" (off) pixel color""" self.dd._sendCommand(self.layer_id, "bgpixelcolor", _DD_COLOR_ARG(color)) def noBgPixelColor(self): - '''set no "background" (off) pixel color''' + """set no "background" (off) pixel color""" self.dd._sendCommand(self.layer_id, "bgpixelcolor") diff --git a/dumbdisplay/ddlayer_ledgrid.py b/dumbdisplay/ddlayer_ledgrid.py index ebf5f40..d8ffe2c 100644 --- a/dumbdisplay/ddlayer_ledgrid.py +++ b/dumbdisplay/ddlayer_ledgrid.py @@ -1,42 +1,47 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer from .ddlayer import _DD_COLOR_ARG class DDLayerLedGrid(DDLayer): - '''Grid of LEDs''' - def __init__(self, dd, col_count = 1, row_count = 1, sub_col_count = 1, sub_row_count = 1): - ''' + """Grid of LEDs""" + def __init__(self, dd: DumbDisplayImpl, col_count: int = 1, row_count: int = 1, sub_col_count: int = 1, sub_row_count: int = 1): + """ :param dd: DumbDisplay object :param col_count: grid # columns :param row_count: grid # rows :param sub_col_count: # sub columns of each cell :param sub_row_count: # sub rows of each cell - ''' + """ layer_id = dd._createLayer("ledgrid", str(col_count), str(row_count), str(sub_col_count), str(sub_row_count)) super().__init__(dd, layer_id) def turnOn(self, x = 0, y = 0): - '''turn on LED @ (x, y)''' + """turn on LED @ (x, y)""" self.dd._sendCommand(self.layer_id, "ledon", str(x), str(y)) - def turnOff(self, x = 0, y = 0): - '''turn off LED @ (x, y)''' + def turnOff(self, x: int = 0, y: int = 0): + """turn off LED @ (x, y)""" self.dd._sendCommand(self.layer_id, "ledoff", str(x), str(y)) def toggle(self, x = 0, y = 0): - '''toggle LED @ (x, y)''' + """toggle LED @ (x, y)""" self.dd._sendCommand(self.layer_id, "ledtoggle", str(x), str(y)) - def turnOnEx(self, x = 0, y = 0, onColor: str = ""): - self.dd._sendCommand(self.layer_id, "ledonex", str(x), str(y), _DD_COLOR_ARG(onColor)) - def horizontalBar(self, count: int, rightToLeft = False): - '''turn on LEDs to form a horizontal "bar"''' - self.dd._sendCommand(self.layer_id, "ledhoribar", str(count), str(rightToLeft)) - def verticalBar(self, count: int, bottomToTop = True): - '''turn on LEDs to form a vertical "bar"''' - self.dd._sendCommand(self.layer_id, "ledvertbar", str(count), str(bottomToTop)) + def turnOnEx(self, x: int = 0, y: int = 0, on_color =""): + self.dd._sendCommand(self.layer_id, "ledonex", str(x), str(y), _DD_COLOR_ARG(on_color)) + def horizontalBar(self, count: int, right_to_left: bool = False): + """ + turn on LEDs to form a horizontal "bar" + """ + self.dd._sendCommand(self.layer_id, "ledhoribar", str(count), str(right_to_left)) + def verticalBar(self, count: int, bottom_to_top: bool = True): + """ + turn on LEDs to form a vertical "bar" + """ + self.dd._sendCommand(self.layer_id, "ledvertbar", str(count), str(bottom_to_top)) def onColor(self, color): - '''set LED on color''' + """set LED on color""" self.dd._sendCommand(self.layer_id, "ledoncolor", _DD_COLOR_ARG(color)) def offColor(self, color): - '''set LED off color''' + """set LED off color""" self.dd._sendCommand(self.layer_id, "ledoffcolor", _DD_COLOR_ARG(color)) def noOffColor(self): - '''/* set no LED off color */''' + """ set no LED off color """ self.dd._sendCommand(self.layer_id, "ledoffcolor") diff --git a/dumbdisplay/ddlayer_multilevel.py b/dumbdisplay/ddlayer_multilevel.py index 7d4fbb3..064c929 100644 --- a/dumbdisplay/ddlayer_multilevel.py +++ b/dumbdisplay/ddlayer_multilevel.py @@ -1,17 +1,19 @@ +from dumbdisplay.ddcmds import DDC_setlevelanchor, DDC_movelevelanchorby, DDC_setlevelrotate +from dumbdisplay.ddimpl import DumbDisplayImpl from dumbdisplay.ddlayer import DDLayer, _DD_BOOL_ARG, _DD_FLOAT_ARG, _DD_INT_ARG, _DD_FLOAT_IS_ZERO DD_DEF_LAYER_LEVEL_ID = "_" class DDLayerMultiLevel(DDLayer): - def __init__(self, dd, layer_id): + def __init__(self, dd: DumbDisplayImpl, layer_id: str): super().__init__(dd, layer_id) def addLevel(self, level_id: str, width: float = 0, height: float = 0, switch_to_it: bool = False): - ''' + """ add a level, optionally change its "opening" size :param level_id: level ID; cannot be DD_DEF_LAYER_LEVEL_ID - :param width: width width of the level "opening"; 0 means the maximum width (the width of the layer) - :param height: height height of the level "opening"; 0 means the maximum height (the height of the layer) - ''' + :param width: width of the level "opening"; 0 means the maximum width (the width of the layer) + :param height: height of the level "opening"; 0 means the maximum height (the height of the layer) + """ if _DD_FLOAT_IS_ZERO(width) and _DD_FLOAT_IS_ZERO(height): if switch_to_it: self.dd._sendCommand(self.layer_id, "addlevel", level_id, _DD_BOOL_ARG(switch_to_it)) @@ -19,135 +21,156 @@ def addLevel(self, level_id: str, width: float = 0, height: float = 0, switch_to self.dd._sendCommand(self.layer_id, "addlevel", level_id) else: self.dd._sendCommand(self.layer_id, "addlevel", level_id, _DD_FLOAT_ARG(width), _DD_FLOAT_ARG(height), _DD_BOOL_ARG(switch_to_it)) - def addTopLevel(self, level_id: str, width: float = 0, height:float = 0, switchToIt:bool = False): - ''' + def addTopLevel(self, level_id: str, width: float = 0, height:float = 0, switch_to_it:bool = False): + """ add top level -- like addLevel() but add to the top (i.e. will be drawn last) :param level_id: level ID; cannot be DD_DEF_LAYER_LEVEL_ID - :param width: width width of the level "opening"; 0 means the maximum width (the width of the layer) - :param height: height height of the level "opening"; 0 means the maximum height (the height of the layer) - ''' + :param width: width of the level "opening"; 0 means the maximum width (the width of the layer) + :param height: height of the level "opening"; 0 means the maximum height (the height of the layer) + """ if _DD_FLOAT_IS_ZERO(width) and _DD_FLOAT_IS_ZERO(height): - if switchToIt: - self.dd._sendCommand(self.layer_id, "addtoplevel", level_id, _DD_BOOL_ARG(switchToIt)) + if switch_to_it: + self.dd._sendCommand(self.layer_id, "addtoplevel", level_id, _DD_BOOL_ARG(switch_to_it)) else: self.dd._sendCommand(self.layer_id, "addtoplevel", level_id) else: - self.dd._sendCommand(self.layer_id, "aaddtoplevel", level_id, _DD_FLOAT_ARG(width), _DD_FLOAT_ARG(height), _DD_BOOL_ARG(switchToIt)) + self.dd._sendCommand(self.layer_id, "aaddtoplevel", level_id, _DD_FLOAT_ARG(width), _DD_FLOAT_ARG(height), _DD_BOOL_ARG(switch_to_it)) def switchLevel(self, level_id: str, add_if_missing: bool = True): - ''' + """ switch to a different level (which is like a sub-layer), making it the current level :param add_if_missing: if true, add the level if it is missing - ''' + """ self.dd._sendCommand(self.layer_id, "switchlevel", level_id, _DD_BOOL_ARG(add_if_missing)) def pushLevel(self): - ''' + """ push the current level onto the level stack, to be pop with popLevel() - ''' + """ self.dd._sendCommand(self.layer_id, "pushlevel") def pushLevelAndSwitchTo(self, switch_tolevel_id: str, add_if_missing: bool = True): - ''' + """ push the current level onto the level stack, to be pop with popLevel() :param switch_tolevel_id switch to level ID (after pushing current level) - ''' + """ self.dd._sendCommand2(self.layer_id, "pushlevel", switch_tolevel_id, _DD_BOOL_ARG(add_if_missing)) def popLevel(self): - ''' + """ pop a level from the level stack and make it the current level - ''' + """ self.dd._sendCommand2(self.layer_id, "poplevel") def levelOpacity(self, opacity: int): - ''' + """ set the opacity of the current level :param opacity background opacity (0 - 100) - ''' + """ self.dd._sendCommand(self.layer_id, "levelopacity", _DD_INT_ARG(opacity)) def levelTransparent(self, transparent: bool): - ''' + """ set whether level is transparent - ''' + """ self.dd._sendCommand(self.layer_id, "leveltransparent", _DD_INT_ARG(transparent)) def setLevelAnchor(self, x: float, y: float, reach_in_millis: int = 0): - ''' + """ set the anchor of the level; note that level anchor is the top-left corner of the level "opening" - ''' + """ + #command = "setlevelanchor" if self.dd._compatibility < 15 else "SLA" if reach_in_millis > 0: - self.dd._sendCommand(self.layer_id, "setlevelanchor", _DD_FLOAT_ARG(x), _DD_FLOAT_ARG(y), _DD_INT_ARG(reach_in_millis)) + self.dd._sendCommand(self.layer_id, DDC_setlevelanchor, _DD_FLOAT_ARG(x), _DD_FLOAT_ARG(y), _DD_INT_ARG(reach_in_millis)) else: - self.dd._sendCommand(self.layer_id, "setlevelanchor", _DD_FLOAT_ARG(x), _DD_FLOAT_ARG(y)) + self.dd._sendCommand(self.layer_id, DDC_setlevelanchor, _DD_FLOAT_ARG(x), _DD_FLOAT_ARG(y)) def moveLevelAnchorBy(self, by_x: float, by_y: float, reach_in_millis: int = 0): - ''' + """ move the level anchor - ''' + """ + #command = "movelevelanchorby" if self.dd._compatibility < 15 else "MLAB" + if reach_in_millis > 0: + self.dd._sendCommand(self.layer_id, DDC_movelevelanchorby, _DD_FLOAT_ARG(by_x), _DD_FLOAT_ARG(by_y), _DD_INT_ARG(reach_in_millis)); + else: + self.dd._sendCommand(self.layer_id, DDC_movelevelanchorby, _DD_FLOAT_ARG(by_x), _DD_FLOAT_ARG(by_y)) + def setLevelRotation(self, angle: float, pivot_x: float = 0, pivot_y: float = 0, reach_in_millis: int = 0): + """ + :param angle: rotation angle in degree; positive is clockwise + :param pivot_x: x coordinate of the pivot point (relative to the level anchor) + :param pivot_y: y coordinate of the pivot point (relative to the level anchor) + """ if reach_in_millis > 0: - self.dd._sendCommand(self.layer_id, "movelevelanchorby", _DD_FLOAT_ARG(by_x), _DD_FLOAT_ARG(by_y), _DD_INT_ARG(reach_in_millis)); + self.dd._sendCommand(self.layer_id, DDC_setlevelrotate, _DD_FLOAT_ARG(angle), _DD_FLOAT_ARG(pivot_x), _DD_FLOAT_ARG(pivot_y), _DD_INT_ARG(reach_in_millis)) else: - self.dd._sendCommand(self.layer_id, "movelevelanchorby", _DD_FLOAT_ARG(by_x), _DD_FLOAT_ARG(by_y)); + angle_is_zero = _DD_FLOAT_IS_ZERO(angle) + pivot_x_is_zero = _DD_FLOAT_IS_ZERO(pivot_x) + pivot_y_is_zero = _DD_FLOAT_IS_ZERO(pivot_y) + if angle_is_zero: + self.dd._sendCommand(self.layer_id, DDC_setlevelrotate) + elif pivot_x_is_zero and pivot_y_is_zero: + self.dd._sendCommand(self.layer_id, DDC_setlevelrotate, _DD_FLOAT_ARG(angle)) + elif pivot_y_is_zero: + self.dd._sendCommand(self.layer_id, DDC_setlevelrotate, _DD_FLOAT_ARG(angle), _DD_FLOAT_ARG(pivot_x)) + else: + self.dd._sendCommand(self.layer_id, DDC_setlevelrotate, _DD_FLOAT_ARG(angle), _DD_FLOAT_ARG(pivot_x), _DD_FLOAT_ARG(pivot_y)) def registerLevelBackground(self, background_id: str, background_image_name: str, draw_background_options: str = ""): - ''' + """ register an image for setting as level's background :param background_id id to identify the background -- see setLevelBackground() :param background_image_name name of the image can be a series of images like dumbdisplay_##0-7##.png (for dumbdisplay_0.png to dumbdisplay_7.png) which can be used for animation with animateLevelBackground() :param drawBackgroundOptions options for drawing the background; same means as the option param of GraphicalDDLayer::drawImageFiler() - ''' + """ self.dd._sendCommand(self.layer_id, "reglevelbg", background_id, background_image_name, draw_background_options) def exportLevelAsRegisteredBackground(self, background_id: str, replace: bool = True): - ''' + """ experimental: export the current level as a registered background image -- see exportLevelsAsImage() and registerLevelBackground() :param background_id id to identify the background -- see setLevelBackground() :param replace if true (default), replace the existing registered background image with the same id; if false, will add as an item of background image series that can be used for animation with animateLevelBackground() - ''' + """ self.dd.sendCommand(self.layer_id, "explevelasregbg", background_id, _DD_BOOL_ARG(replace)); def setLevelBackground(self, background_id: str, background_image_name: str = "", draw_background_options: str = ""): - ''' + """ set a registered background image as the current level's background :param background_id :param background_image_name if not registered, the name of the image to register; can be a series of images like dumbdisplay_##0-7##.png (for dumbdisplay_0.png to dumbdisplay_7.png) which can be used for animation with animateLevelBackground() :param draw_background_options if not registered, the options for drawing the background - ''' + """ if background_image_name == "": self.dd._sendCommand(self.layer_id, "setlevelbg", background_id) else: self.dd._sendCommand(self.layer_id, "setlevelbg", background_id, background_image_name, draw_background_options) def setLevelNoBackground(self): - ''' + """ set that the current level uses no background image - ''' + """ self.dd._sendCommand(self.layer_id, "setlevelnobg") def animateLevelBackground(self, fps: float, reset: bool = True, options: str = ""): - ''' + """ start animate level background (if level background has a series of images) :param fps frames per second which is used to calculate the interval between the series of images - :param reset reset to the first image in the series (before start animation) + :param reset to the first image in the series (before start animation) param options can be "r" to reverse the order of the series of images - ''' + """ self.dd._sendCommand(self.layer_id, "anilevelbg", _DD_FLOAT_ARG(fps), _DD_BOOL_ARG(reset), options) def stopAnimateLevelBackground(self, reset: bool = True): - ''' + """ stop animate level background :param reset reset to the first image in the series - ''' + """ self.dd._sendCommand(self.layer_id, "stopanilevelbg", _DD_FLOAT_ARG(reset)) def reorderLevel(self, level_id: str, how: str): - ''' + """ reorder the specified level (by moving it in the z-order plane) :param how can be "T" for top; or "B" for bottom; "U" for up; or "D" for down - ''' + """ self.dd._sendCommand(self.layer_id, "reordlevel", level_id, how) def exportLevelsAsImage(self, image_file_name: str, cache_it_not_save: bool = False): - ''' + """ export (and save) the levels as an image (without the decorations of the layer like border) - ''' + """ self.dd._sendCommand(self.layer_id, "explevelsasimg", image_file_name, _DD_BOOL_ARG(cache_it_not_save)) def deleteLevel(self, level_id: str): - ''' + """ delete the specified level - ''' + """ self.dd._sendCommand(self.layer_id, "dellevel", level_id) -# TODO: add more diff --git a/dumbdisplay/ddlayer_plotter.py b/dumbdisplay/ddlayer_plotter.py index f8f4b06..884e4d6 100644 --- a/dumbdisplay/ddlayer_plotter.py +++ b/dumbdisplay/ddlayer_plotter.py @@ -1,26 +1,26 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer -from .ddlayer import _DD_BOOL_ARG -from .ddlayer import _DD_COLOR_ARG + class DDLayerPlotter(DDLayer): - '''Plotter''' - def __init__(self, dd, width, height, pixels_per_second = 10): - ''' + """Plotter""" + def __init__(self, dd: DumbDisplayImpl, width: int, height: int, pixels_per_second: int = 10): + """ :param dd: DumbDisplay object :param width: width :param height: height :param pixels_per_second: # pixel to scroll per second - ''' + """ layer_id = dd._createLayer("plotterview", str(width), str(height), str(pixels_per_second)) super().__init__(dd, layer_id) def label(self, no_key_label = None, **key_label_pairs): - '''set labels of keys; if key has no label, the key will be the label''' + """set labels of keys; if key has no label, the key will be the label""" if no_key_label is not None: self.dd._sendCommand(self.layer_id, "label", "", str(no_key_label)) for (key, lab) in key_label_pairs.items(): self.dd._sendCommand(self.layer_id, "label", key, str(lab)) def set(self, no_key_value = None, **key_value_pairs): - '''set values with multiple key value pairs; value should be numeric''' + """set values with multiple key value pairs; value should be numeric""" params = [] if no_key_value is not None: params.append("") diff --git a/dumbdisplay/ddlayer_selection.py b/dumbdisplay/ddlayer_selection.py index 2b09edf..1a45727 100644 --- a/dumbdisplay/ddlayer_selection.py +++ b/dumbdisplay/ddlayer_selection.py @@ -1,10 +1,11 @@ +from .ddimpl import DumbDisplayImpl from .ddlayer import DDLayer, _DD_INT_ARG, _DD_FLOAT_ARG from .ddlayer import _DD_BOOL_ARG -from .ddlayer import _DD_COLOR_ARG + class DDLayerSelection(DDLayer): - '''Selection''' - def __init__(self, dd, + """Selection""" + def __init__(self, dd: DumbDisplayImpl, col_count: int = 16, row_count: int = 2, hori_selection_count: int = 1, vert_selection_count: int = 1, char_height: int = 0, font_name: str = "", @@ -15,54 +16,54 @@ def __init__(self, dd, _DD_BOOL_ARG(can_draw_dots), _DD_FLOAT_ARG(selection_border_size_char_height_factor)) super().__init__(dd, layer_id) def text(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0, align: str = "L"): - ''' + """ set a "selection" unit text (of y-th row) :param align 'L', 'C', or 'R' - ''' + """ self.dd._sendCommand(self.layer_id, "text", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), align, text) def textCentered(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0): - ''' + """ set a "selection" unit centered text (of y-th row) - ''' + """ self.dd._sendCommand(self.layer_id, "text", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), "C", text) def textRightAligned(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0): - ''' + """ set a "selection" unit right-aligned text (of y-th row) - ''' + """ self.dd._sendCommand(self.layer_id, "text", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), "R", text) def unselectedText(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0, align: str = "L"): - ''' + """ set a "selection" unit text (of y-th row) when unselected (it defaults to the same text as selected) :param align 'L', 'C', or 'R' - ''' + """ self.dd._sendCommand(self.layer_id, "unselectedtext", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), align, text) def unselectedTextCentered(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0): - ''' + """ set a "selection" unit centered text (of y-th row) when unselected (it defaults to the same text as selected) - ''' + """ self.dd._sendCommand(self.layer_id, "unselectedtext", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), "C", text) def unselectedTextRightAligned(self, text: str, y: int = 0, hori_selection_idx: int = 0, vert_selection_idx: int = 0): - ''' + """ set a "selection" unit right-aligned text (of y-th row) when unselected (it defaults to the same text as selected) - ''' + """ self.dd._sendCommand(self.layer_id, "unselectedtext", _DD_INT_ARG(y), _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), "R", text) def select(self, hori_selection_idx: int = 0, vert_selection_idx: int = 0, deselect_the_others: bool = True): - ''' + """ select a "selection" unit :param deselect_the_others: if True, deselect the others - ''' + """ self.dd._sendCommand(self.layer_id, "select", _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), _DD_BOOL_ARG(deselect_the_others)) def deselect(self, hori_selection_idx: int = 0, vert_selection_idx: int = 0, select_the_others: bool = False): - ''' + """ deselect a "selection" unit :param select_the_others: if True, select the others - ''' + """ self.dd._sendCommand(self.layer_id, "deselect", _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), _DD_BOOL_ARG(select_the_others)) def selected(self, selected: bool, hori_selection_idx: int = 0, vert_selection_idx: int = 0, reverse_the_others: bool = False): - ''' + """ set a "selection" unit selected or not (combination of select() and deselect()) :param reverse_the_others: if True, reverse the others - ''' + """ if selected: self.dd._sendCommand(self.layer_id, "select", _DD_INT_ARG(hori_selection_idx), _DD_INT_ARG(vert_selection_idx), _DD_BOOL_ARG(reverse_the_others)) else: diff --git a/dumbdisplay/ddlayer_turtle.py b/dumbdisplay/ddlayer_turtle.py new file mode 100644 index 0000000..d0239e0 --- /dev/null +++ b/dumbdisplay/ddlayer_turtle.py @@ -0,0 +1,186 @@ +#from ._ddlayer import DDLayer +from .ddlayer_multilevel import DDLayerMultiLevel +from .ddlayer import _DD_COLOR_ARG, _DD_BOOL_ARG, _DD_INT_ARG +from .ddimpl import _INIT_ACK_SEQ, _NEXT_ACK_SEQ, _ACK_STR_TO_ACK_SEQ, DumbDisplayImpl + + +class DDLayerTurtle(DDLayerMultiLevel): + """Turtle-like Layer""" + def __init__(self, dd: DumbDisplayImpl, width: int, height: int): + """ + :param dd: DumbDisplay object + :param width: width + :param height: height + """ + layer_id = dd._createLayer("turtle", _DD_INT_ARG(width), _DD_INT_ARG(height)) + super().__init__(dd, layer_id) + def forward(self, distance: int, with_pen: bool = True): + """ + forward; with pen or not + """ + self._sendCommandTracked("fd" if with_pen else "jfd", _DD_INT_ARG(distance)) + def backward(self, distance: int, with_pen: bool = True): + """ + backward; with pen or not + """ + self._sendCommandTracked("bk" if with_pen else "jbk", _DD_INT_ARG(distance)) + def leftTurn(self, angle: int): + """ + left turn + """ + self.dd._sendCommand(self.layer_id, "lt", _DD_INT_ARG(angle)) + def rightTurn(self, angle: int): + """ + right turn + """ + self.dd._sendCommand(self.layer_id, "rt", _DD_INT_ARG(angle)) + def home(self, with_pen: bool = True): + """ + go home (0, 0); with pen or not + """ + self._sendCommandTracked("home" if with_pen else "jhome") + def goTo(self, x: int, y: int, with_pen: bool = True): + """ + go to (x, y); with pen or not + """ + self._sendCommandTracked("goto" if with_pen else "jto", _DD_INT_ARG(x), _DD_INT_ARG(y)) + def goBy(self, by_x: int, by_y: int, with_pen: bool = True): + """ + go by (by_x, by_y); with pen or not + """ + self._sendCommandTracked("goby" if with_pen else "jby", _DD_INT_ARG(by_x), _DD_INT_ARG(by_y)) + def setHeading(self, angle: int): + """ + set heading angle (degree) + """ + self.dd._sendCommand(self.layer_id, "seth", _DD_INT_ARG(angle)) + def penSize(self, size: int): + """ + set pen size + """ + self.dd._sendCommand(self.layer_id, "pensize", _DD_INT_ARG(size)) + def penColor(self, color: str): + """ + set pen color + """ + self.dd._sendCommand(self.layer_id, "pencolor", _DD_COLOR_ARG(color)) + def fillColor(self, color: str): + """ + set fill color + """ + self.dd._sendCommand(self.layer_id, "fillcolor", _DD_COLOR_ARG(color)) + def noColor(self): + """ + set no fill color + """ + self.dd._sendCommand(self.layer_id, "nofillcolor") + def penFilled(self, filled: bool = True): + """ + set pen filled or not + """ + self.dd._sendCommand(self.layer_id, "pfilled", _DD_BOOL_ARG(filled)) + def setTextSize(self, size: int): + """ + set text size + """ + self.dd._sendCommand(self.layer_id, "ptextsize", _DD_INT_ARG(size)) + def setTextFont(self, font_name = "", text_size = 0): + """ + set font + @param font_name: empty means default + @param text_size: 0 means default + """ + self.dd._sendCommand(self.layer_id, "ptextfont", font_name, _DD_INT_ARG(text_size)) + def penUp(self): + """pen up""" + self.dd._sendCommand(self.layer_id, "pu") + def penDown(self): + """pen down""" + self.dd._sendCommand(self.layer_id, "pd") + def beginFill(self): + """begin fill""" + self.dd._sendCommand(self.layer_id, "begin_fill") + def endFill(self): + """end fill""" + self.dd._sendCommand(self.layer_id, "end_fill") + def dot(self, size: int, color: str): + """draw a dot""" + self.dd._sendCommand(self.layer_id, "dot", _DD_INT_ARG(size), _DD_COLOR_ARG(color)) + def circle(self, radius: int, centered: bool = False): + """draw circle; centered or not""" + self.dd._sendCommand(self.layer_id, "ccircle" if centered else "circle", _DD_INT_ARG(radius)) + def oval(self, width: int, height: int, centered: bool = False): + """draw oval; centered or not""" + self.dd._sendCommand(self.layer_id, "coval" if centered else "oval", _DD_INT_ARG(width), _DD_INT_ARG(height)) + def arc(self, width: int, height: int, start_angle: int, sweep_angle: int, centered: bool = False): + """draw arc; centered or not""" + self.dd._sendCommand(self.layer_id, "carc" if centered else "arc", _DD_INT_ARG(width), _DD_INT_ARG(height), _DD_INT_ARG(start_angle), _DD_INT_ARG(sweep_angle)) + def triangle(self, side1: int, angle: int, side2: int): + """draw triangle (SAS)""" + self.dd._sendCommand(self.layer_id, "trisas", _DD_INT_ARG(side1), _DD_INT_ARG(angle), _DD_INT_ARG(side2)) + def isoscelesTriangle(self, side: int, angle: int): + """draw isosceles triangle; given size and angle""" + self.dd._sendCommand(self.layer_id, "trisas", _DD_INT_ARG(side), _DD_INT_ARG(angle)) + def rectangle(self, width: int, height: int, centered: bool = False): + """draw rectangle; centered or not""" + self.dd._sendCommand(self.layer_id, "crect" if centered else "rect", _DD_INT_ARG(width), _DD_INT_ARG(height)) + def polygon(self, side: int, vertex_count: int): + """draw polygon given side and vertex count""" + self.dd._sendCommand(self.layer_id, "poly", _DD_INT_ARG(side), _DD_INT_ARG(vertex_count)) + def centeredPolygon(self, radius: int, vertex_count: int, inside: bool = False): + """ + draw polygon enclosed in an imaginary centered circle + - given circle radius and vertex count + - whether inside the imaginary circle or outside of it + """ + self.dd._sendCommand(self.layer_id, "cpolyin" if inside else "cpoly", _DD_INT_ARG(radius), _DD_INT_ARG(vertex_count)) + def write(self, text: str, align: str = "L"): + """write text, with alignment 'L', 'C', or 'R'""" + if align == "" or align == "L": + self._sendCommandTracked("write", text) + else: + self._sendCommandTracked("writetext", str(align), text) + def drawText(self, text: str, draw: bool = False): + """draw the text (honor heading)""" + self._sendCommandTracked("drawtext" if draw else "write", text) + def _sendCommandTracked(self, command: str, *params): + self.dd._sendCommand(self.layer_id, command, *params) + + +class DDLayerTurtleTracked(DDLayerTurtle): # TODO: working on DDLayerTurtleTracked + """ + EXPERIMENTAL: Turtle-like Layer, with position tracking + """ + def __init__(self, dd, width, height): + """ + :param dd: DumbDisplay object + :param width: width + :param height: height + """ + super().__init__(dd, width, height) + self._x: int = 0 + self._y: int = 0 + self._next_ack_seq: int = _INIT_ACK_SEQ + def pos(self, sync: bool = True) -> (int, int): + while self._pending_ack_seq is not None: + self.dd.timeslice() + if not sync or self.dd._is_reconnecting(): + break + return (self._x, self._y) + # def xcor(self) -> int: + # self.dd.timeslice() + # return self._x + # def ycor(self) -> int: + # self.dd.timeslice() + # return self._y + def _sendCommandTracked(self, command: str, *params): + ack_seq = self._next_ack_seq + self._next_ack_seq = _NEXT_ACK_SEQ(self._next_ack_seq) + self._pending_ack_seq = ack_seq + self.dd._sendCommand(self.layer_id, command, *params, ack_seq=ack_seq) + def _handleAck(self, ack_seq: str, x: int, y: int, text: str): + ack_seq = _ACK_STR_TO_ACK_SEQ(ack_seq) + if ack_seq == self._pending_ack_seq: + self._pending_ack_seq = None + self._x = x + self._y = y diff --git a/dumbdisplay/dumbdisplay.py b/dumbdisplay/dumbdisplay.py index 3c97832..ee2bcbc 100644 --- a/dumbdisplay/dumbdisplay.py +++ b/dumbdisplay/dumbdisplay.py @@ -1,7 +1,6 @@ -from dumbdisplay.ddimpl import DumbDisplayImpl -from dumbdisplay.ddiobase import DDInputOutput -from .ddlayer import _DD_INT_ARG, _DD_BOOL_ARG - +from .ddimpl import DumbDisplayImpl +from .ddiobase import DDInputOutput +from .ddlayer import _DD_INT_ARG, _DD_BOOL_ARG, DDLayer import sys @@ -14,18 +13,18 @@ class DDAutoPin: def __init__(self, orientation: str, *layers): - ''' + """ :param orientation: H or V or S :param layers: layer or "pinner" - ''' + """ self.orientation = orientation self.layers = layers - def build(self): + def build(self) -> str: return self._build_layout() def pin(self, dd): layout_spec = self._build_layout() dd.configAutoPin(layout_spec) - def _build_layout(self): + def _build_layout(self) -> str: layout_spec = None for layer in self.layers: if layout_spec is None: @@ -49,7 +48,7 @@ def __init__(self, orientation: str, left: int, top: int, right: int, bottom: in self.right = right self.bottom = bottom super().__init__(orientation, *layers) - def _build_layout(self): + def _build_layout(self) -> str: layout_spec = super()._build_layout() return f"S/{self.left}-{self.top}-{self.right}-{self.bottom}({layout_spec})" @@ -66,17 +65,17 @@ def __init__(self, io: DDInputOutput = None, reset_machine_when_failed_to_send_c self.passive_state = None # None; "cing" (connecting); "c" (connected); "nc" (not connected) # def debugSetup(self, debug_led_pin): - # '''setup debug use flashing LED pin number''' + # """setup debug use flashing LED pin number""" # if _DD_HAS_LED: # self.debug_led = Pin(debug_led_pin, Pin.OUT) def connect(self): - '''explicit connect''' + """explicit connect""" self._connect() def connectPassive(self) -> (bool, bool): - ''' + """ will use a thread to connect :return: (connected, reconnecting) ... reconnecting is True when connected but detected connection loss - ''' + """ if self.passive_state is None: self.passive_state = "cing" try: @@ -101,29 +100,47 @@ def isReconnecting(self) -> bool: iop = self._connected_iop return iop is not None and iop.reconnecting def autoPin(self, orientation: str = 'V'): - ''' + """ auto pin layers :param orientation: H or V - ''' + """ layout_spec = str(orientation) + '(*)' self.configAutoPin(layout_spec) def configAutoPin(self, layout_spec: str = "V(*)"): - ''' + """ configure "auto pinning of layers" with the layer spec provided - horizontal: H(*) - vertical: V(*) - or nested, like H(0+V(1+2)+3) - where 0/1/2/3 are the layer ids - ''' + """ self._connect() self._sendCommand(None, "CFGAP", layout_spec) + def addRemainingAutoPinConfig(self, rest_layout_spec: str): + """ + add REST "auto pinning" spec for layers not already associated into existing "auto pinning" config + """ + self._sendCommand(None, "ADDRESTAP", rest_layout_spec) + def deleteAllRemainingAutoPinConfigs(self, rest_layout_spec: str): + """ + delete all REST "auto pinning" specs + """ + self._sendCommand(None, "ADDRESTAP", rest_layout_spec) def configPinFrame(self, x_unit_count: int, y_unit_count: int): self._connect() self._sendCommand(None, "CFGPF", _DD_INT_ARG(x_unit_count), _DD_INT_ARG(y_unit_count)) - def _pinLayer(self, layer_id: str, uLeft: int, uTop: int, uWidth: int, uHeight: int, align: str = ""): - self._sendCommand(layer_id, "PIN", _DD_INT_ARG(uLeft), _DD_INT_ARG(uTop), _DD_INT_ARG(uWidth), _DD_INT_ARG(uHeight), align) + # def pinLayer(self, layer_id: str, u_left: int, u_top: int, u_width: int, u_height: int, align: str = ""): + # self._sendCommand(layer_id, "PIN", _DD_INT_ARG(u_left), _DD_INT_ARG(u_top), _DD_INT_ARG(u_width), _DD_INT_ARG(u_height), align) + def pinLayer(self, layer: DDLayer, u_left: int, u_top: int, u_width: int, u_height: int, align: str = ""): + self._sendCommand(layer.layer_id, "PIN", _DD_INT_ARG(u_left), _DD_INT_ARG(u_top), _DD_INT_ARG(u_width), _DD_INT_ARG(u_height), align) def pinAutoPinLayers(self, layout_spec: str, u_left: int, u_top: int, u_width: int, u_height: int, align: str = ""): self._sendCommand(None, "PINAP", layout_spec, _DD_INT_ARG(u_left), _DD_INT_ARG(u_top), _DD_INT_ARG(u_width), _DD_INT_ARG(u_height), align) + def freezeDrawing(self): + self._connect() + self._sendCommand(None, "FRZ") + def unfreezeDrawing(self, refreeze_after_draw: bool = False): + self._connect() + self._sendCommand(None, "UNFRZ", _DD_BOOL_ARG(refreeze_after_draw)) def recordLayerSetupCommands(self): self._connect() self._sendCommand(None, "RECC") @@ -137,16 +154,16 @@ def recordLayerCommands(self): def playbackLayerCommands(self): self._sendCommand(None, "PLAYC") def backgroundColor(self, color: str): - '''set DD background color with common "color name"''' + """set DD background color with common "color name""" self._connect() self._sendCommand(None, "BGC", color) def writeComment(self, comment: str): - '''write out a comment to DD''' + """write out a comment to DD""" self._connect() self._sendCommand(None, '// ' + comment) print("# " + comment) def log(self, log_msg: str, is_error: bool = False): - '''log to DD''' + """log to DD""" if is_error: print("X " + log_msg) else: @@ -157,16 +174,16 @@ def log(self, log_msg: str, is_error: bool = False): else: self._sendCommand(None, '// ' + log_msg) def recordLayerCommands(self): - ''' + """ start recording layer commands (of any layers) and sort of freeze the display, until playback - ''' + """ self._sendCommand(None, "RECC") def playbackLayerCommands(self): - '''playback recorded commands (unfreeze the display)''' + """playback recorded commands (unfreeze the display)""" self._sendCommand(None, "PLAYC") def release(self): - '''release it''' + """release it""" super().release() def tone(self, freq: int, duration: int): @@ -174,6 +191,14 @@ def tone(self, freq: int, duration: int): def notone(self): self._sendCommand(None, "NOTONE") + # def setRootLayer(self, width: int, height: int, contained_alignment: str = "") -> DDLayerGraphical: + # /// note that the "root" will always be placed as the container, and hence don't need be pined; + # /// @param containedAlignment the alignment of the contained layers; "L" / "T" / "LT"; "" means centered + # /// currently, "container" layer does not support "feedback" + # /// @since v0.9.9-r50 + # GraphicalDDLayer* setRootLayer(int width, int height, const String& containedAlignment = ""); + # + def onDetectedDisconnect(self, for_ms: int): diff --git a/dumbdisplay/full.py b/dumbdisplay/full.py index 92a8ff6..2d8470c 100644 --- a/dumbdisplay/full.py +++ b/dumbdisplay/full.py @@ -10,5 +10,6 @@ from .layer_ledgrid import * from .layer_plotter import * from .layer_selection import * +from .layer_turtle import * from ._exp_tunnel_basic import * \ No newline at end of file diff --git a/dumbdisplay/layer_graphical.py b/dumbdisplay/layer_graphical.py index 4d9b771..981a765 100644 --- a/dumbdisplay/layer_graphical.py +++ b/dumbdisplay/layer_graphical.py @@ -1 +1,2 @@ from .ddlayer_graphical import DDLayerGraphical as LayerGraphical +from .ddlayer_graphical import DDRootLayer diff --git a/dumbdisplay/layer_turtle.py b/dumbdisplay/layer_turtle.py new file mode 100644 index 0000000..c7dd6f2 --- /dev/null +++ b/dumbdisplay/layer_turtle.py @@ -0,0 +1,2 @@ +from .ddlayer_turtle import DDLayerTurtle as LayerTurtle +from .ddlayer_turtle import DDLayerTurtleTracked as LayerTurtleTracked diff --git a/dumbdisplay_examples/OLD/tetris_classic/tetris_classic.py b/dumbdisplay_examples/OLD/tetris_classic/tetris_classic.py new file mode 100644 index 0000000..680b45a --- /dev/null +++ b/dumbdisplay_examples/OLD/tetris_classic/tetris_classic.py @@ -0,0 +1,393 @@ +# *** +# *** Adapted from TETRIS CLASSIC\tetris_turtle.py of https://github.com/DimaGutierrez/Python-Games +# *** - modified from dumbdisplay_examples/tetris_classic +# *** +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + + +# TODO: two issues +# . sometimes block can fall through the others +# . when rotate / move left / right, the block can go into others (or the grid) + +_delay = 0.3 + +class GridRow: + def __init__(self, grid, grid_dirty): + self.grid = grid + self.grid_dirty = grid_dirty + self.row_idx = 0 + def __len__(self): + return len(self.grid[self.row_idx]) + def __getitem__(self, col_idx): + return self.grid[self.row_idx][col_idx] + def __setitem__(self, col_idx, value): + self.grid[self.row_idx][col_idx] = value + self.grid_dirty[self.row_idx][col_idx] = True + +class Grid: + def __init__(self): + self.grid = [[0 for _ in range(12)] for _ in range(24)] + self.grid_dirty = [[False for _ in range(12)] for _ in range(24)] + self.grid_row = GridRow(self.grid, self.grid_dirty) + def __len__(self): + return len(self.grid) + def __getitem__(self, row_idx): + self.grid_row.row_idx = row_idx + return self.grid_row + def check_reset_need_redraw(self, row_idx, col_idx): + dirty = self.grid_dirty[row_idx][col_idx] + if not dirty: + return False + self.grid_dirty[row_idx][col_idx] = False + return True + + +class Shape: + def __init__(self): + self.grid = Grid() + self.score_count = 0 + self.reset(for_init=True) + self.x = None + self.y = None + self.color = None + self.reset(for_init=True) + + def reset(self, for_init=False): + self.x = 5 + self.y = 0 + self.color = random.randint(1, 7) + + # Block Shape + square = [[1,1], + [1,1]] + + horizontal_line = [[1,1,1,1]] + + vertical_line = [[1], + [1], + [1], + [1]] + + left_l = [[1,0,0,0], + [1,1,1,1]] + + right_l = [[0,0,0,1], + [1,1,1,1]] + + left_s = [[1,1,0], + [0,1,1]] + + right_s = [[0,1,1], + [1,1,0]] + + t = [[0,1,0], + [1,1,1]] + + shapes = [square, horizontal_line, vertical_line, left_l, right_l, left_s, right_s, t] + + # Choose a random shape each time + self.shape = random.choice(shapes) + + + self.height = len(self.shape) + self.width = len(self.shape[0]) + + self.draw_shape() + + # print(self.height, self.width) + + def move_left(self): + if self.x > 0: + if self.grid[self.y][self.x - 1] == 0: + self.erase_shape() + self.x -= 1 + self.draw_shape() + + def move_right(self): + if self.x < 12 - self.width: + if self.grid[self.y][self.x + self.width] == 0: + self.erase_shape() + self.x += 1 + self.draw_shape() + + def draw_shape(self): + for y in range(self.height): + for x in range(self.width): + if(self.shape[y][x]==1): + self.grid[self.y + y][self.x + x] = self.color + + def erase_shape(self): + for y in range(self.height): + for x in range(self.width): + if(self.shape[y][x]==1): + self.grid[self.y + y][self.x + x] = 0 + + def can_move(self): + result = True + for x in range(self.width): + # Check if bottom is a 1 + if(self.shape[self.height-1][x] == 1): + if(self.grid[self.y + self.height][self.x + x] != 0): + result = False + return result + + def rotate(self): + # First erase_shape + self.erase_shape() + rotated_shape = [] + for x in range(len(self.shape[0])): + new_row = [] + for y in range(len(self.shape)-1, -1, -1): + new_row.append(self.shape[y][x]) + rotated_shape.append(new_row) + + right_side = self.x + len(rotated_shape[0]) + if right_side < len(self.grid[0]): + self.shape = rotated_shape + # Update the height and width + self.height = len(self.shape) + self.width = len(self.shape[0]) + + self.draw_shape() + +def draw_grid(shape: Shape, pen: LayerTurtle): + grid = shape.grid + + #pen.clear() + top = 230 + left = -110 + + colors = ["black", "lightblue", "blue", "orange", "yellow", "green", "purple", "red"] + + for y in range(len(grid)): + for x in range(len(grid[0])): + if not grid.check_reset_need_redraw(y, x): + continue + screen_x = left + (x * 20) + screen_y = top - (y * 20) + color_number = grid[y][x] + color = colors[color_number] + pen.penColor(color) + pen.goTo(screen_x, screen_y, with_pen=False) + #pen.stamp() + pen.rectangle(18, 18, centered=True) + + +def check_grid(shape: Shape, score: LayerTurtle): + grid = shape.grid + + # Check if each row is full + y = 23 + while y > 0: + is_full = True + for x in range(0, 12): + if grid[y][x] == 0: + is_full = False + y -= 1 + break + if is_full: + shape.score_count += 10 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for copy_y in range(y, 0, -1): + for copy_x in range(0, 12): + grid[copy_y][copy_x] = grid[copy_y-1][copy_x] + +# def draw_score(pen: LayerTurtle): +# pen.penColor("blue") +# #pen.hideturtle() +# pen.goTo(-75, 350, with_pen=False) +# #pen.write("Score: {}".format(score), move=False, align="left", font=("Arial", 24, "normal")) +# pen.write("Score: {}".format(score), align="L") + + + +class TetrisClassicApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + # self.root: DDRootLayer = None + self.score: LayerTurtle = None + self.pen: LayerTurtle = None + # self.left_button: LayerLcd = None + # self.right_button: LayerLcd = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + width = 400 + height = 700 + + root = DDRootLayer(self.dd, width, height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + score = LayerTurtle(self.dd, width, height) + score.penColor('red') + score.penUp() + #score.hideturtle() + score.goTo(60, -300) + #score.write('Score: 0', align='center', font=('Courier', 24, 'normal')) + score.setTextFont("Courier", 24) + score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, width, height) + border.penSize(10) + border.penUp() + #border.hideturtle() + border.goTo(-130, 240) + border.penDown() + border.penColor('white') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + #border.write("TETRIS", align='center', font=('Courier', 36, 'normal')) + border.setTextFont("Courier", 36) + border.write("TETRIS", "C") + + pen = LayerTurtle(self.dd, width, height) + #pen.up() + # pen.speed(0) + # pen.shape('square') + # pen.shapesize(0.9, 0.9) + # pen.setundobuffer(None) + pen.penFilled() + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveShapeLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveShapeRight()) + + rotate_button = LayerLcd(self.dd, 2, 1, char_height=28) + rotate_button.noBackgroundColor() + rotate_button.writeLine("🔄") + rotate_button.enableFeedback("f", lambda *args: self.shapeRotate()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, rotate_button, right_button)).pin(self.dd) + + # self.root = root + self.score = score + self.pen = pen + # self.left_button = left_button + # self.right_button = right_button + + # Create the basic shape for the start of the game + shape = Shape() + + # # Put the shape in the grid + # grid[shape.y][shape.x] = shape.color + + self.shape = shape + + # wn.listen() + # wn.onkeypress(lambda: shape.move_left(grid), "a") + # wn.onkeypress(lambda: shape.move_right(grid), "d") + # wn.onkeypress(lambda: shape.rotate(grid), "space") + + def updateDD(self): + #global _dirty + now = time.time() + need_update = self.last_update_time is None or (now - self.last_update_time) >= _delay + if need_update: + self.update() + self.last_update_time = now + #_dirty = False + # else: + # if _dirty: + # self.drawGrid() + # _dirty = False + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + # Move the shape + # Open Row + # Check for the bottom + if self.shape.y == 23 - self.shape.height + 1: + if True: + self.checkGrid() + if self.shape.y == 23: + self.shape = None + else: + self.shape.reset() + # self.shape.x = 5 + # self.shape.y = 0 + else: + self.shape = Shape() + self.checkGrid() + # Check for collision with next row + elif self.shape.can_move(): + # Erase the current shape + self.shape.erase_shape() + + # Move the shape by 1 + self.shape.y +=1 + + # Draw the shape again + self.shape.draw_shape() + + else: + if True: + self.checkGrid() + if self.shape.y > 0: + self.shape.reset() + # self.shape.x = 5 + # self.shape.y = 0 + else: + self.shape = None + print("*** GAME OVER ***") + else: + self.shape = Shape() + self.checkGrid() + + # Draw the screen + if self.shape is not None: + self.drawGrid() + + def drawGrid(self): + self.dd.freezeDrawing() + draw_grid(shape=self.shape, pen=self.pen) + self.dd.unfreezeDrawing() + + def checkGrid(self): + check_grid(shape=self.shape, score=self.score) + + def moveShapeLeft(self): + self.shape.move_left() + self.drawGrid() + + def moveShapeRight(self): + self.shape.move_right() + self.drawGrid() + + def shapeRotate(self): + self.shape.rotate() + self.drawGrid() + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisClassicApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/OLD/tetris_classic/tetris_classic_OLD_0.py b/dumbdisplay_examples/OLD/tetris_classic/tetris_classic_OLD_0.py new file mode 100644 index 0000000..59aa8fc --- /dev/null +++ b/dumbdisplay_examples/OLD/tetris_classic/tetris_classic_OLD_0.py @@ -0,0 +1,389 @@ +# *** +# *** Adapted from TETRIS CLASSIC\tetris_turtle.py of https://github.com/DimaGutierrez/Python-Games +# *** - modified from dumbdisplay_examples/tetris_classic +# *** +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + + +# TODO: two issues +# . sometimes block can fall through the others +# . when rotate / move left / right, the block can go into others (or the grid) + +_delay = 0.3 + +class GridRow: + def __init__(self, grid, grid_dirty): + self.grid = grid + self.grid_dirty = grid_dirty + self.row_idx = 0 + def __len__(self): + return len(self.grid[self.row_idx]) + def __getitem__(self, col_idx): + return self.grid[self.row_idx][col_idx] + def __setitem__(self, col_idx, value): + self.grid[self.row_idx][col_idx] = value + self.grid_dirty[self.row_idx][col_idx] = True + +class Grid: + def __init__(self): + self.grid = [[0 for _ in range(12)] for _ in range(24)] + self.grid_dirty = [[False for _ in range(12)] for _ in range(24)] + self.grid_row = GridRow(self.grid, self.grid_dirty) + def __len__(self): + return len(self.grid) + def __getitem__(self, row_idx): + self.grid_row.row_idx = row_idx + return self.grid_row + def check_reset_need_redraw(self, row_idx, col_idx): + dirty = self.grid_dirty[row_idx][col_idx] + if not dirty: + return False + self.grid_dirty[row_idx][col_idx] = False + return True + + +class Shape: + def __init__(self): + self.grid = Grid() + self.score_count = 0 + self.reset(for_init=True) + + def reset(self, for_init=False): + self.x = 5 + self.y = 0 + self.color = random.randint(1, 7) + + # Block Shape + square = [[1,1], + [1,1]] + + horizontal_line = [[1,1,1,1]] + + vertical_line = [[1], + [1], + [1], + [1]] + + left_l = [[1,0,0,0], + [1,1,1,1]] + + right_l = [[0,0,0,1], + [1,1,1,1]] + + left_s = [[1,1,0], + [0,1,1]] + + right_s = [[0,1,1], + [1,1,0]] + + t = [[0,1,0], + [1,1,1]] + + shapes = [square, horizontal_line, vertical_line, left_l, right_l, left_s, right_s, t] + + # Choose a random shape each time + self.shape = random.choice(shapes) + + + self.height = len(self.shape) + self.width = len(self.shape[0]) + + self.draw_shape() + + # print(self.height, self.width) + + def move_left(self): + if self.x > 0: + if self.grid[self.y][self.x - 1] == 0: + self.erase_shape() + self.x -= 1 + self.draw_shape() + + def move_right(self): + if self.x < 12 - self.width: + if self.grid[self.y][self.x + self.width] == 0: + self.erase_shape() + self.x += 1 + self.draw_shape() + + def draw_shape(self): + for y in range(self.height): + for x in range(self.width): + if(self.shape[y][x]==1): + self.grid[self.y + y][self.x + x] = self.color + + def erase_shape(self): + for y in range(self.height): + for x in range(self.width): + if(self.shape[y][x]==1): + self.grid[self.y + y][self.x + x] = 0 + + def can_move(self): + result = True + for x in range(self.width): + # Check if bottom is a 1 + if(self.shape[self.height-1][x] == 1): + if(self.grid[self.y + self.height][self.x + x] != 0): + result = False + return result + + def rotate(self): + # First erase_shape + self.erase_shape() + rotated_shape = [] + for x in range(len(self.shape[0])): + new_row = [] + for y in range(len(self.shape)-1, -1, -1): + new_row.append(self.shape[y][x]) + rotated_shape.append(new_row) + + right_side = self.x + len(rotated_shape[0]) + if right_side < len(self.grid[0]): + self.shape = rotated_shape + # Update the height and width + self.height = len(self.shape) + self.width = len(self.shape[0]) + + self.draw_shape() + +def draw_grid(shape: Shape, pen: LayerTurtle): + grid = shape.grid + + #pen.clear() + top = 230 + left = -110 + + colors = ["black", "lightblue", "blue", "orange", "yellow", "green", "purple", "red"] + + for y in range(len(grid)): + for x in range(len(grid[0])): + if not grid.check_reset_need_redraw(y, x): + continue + screen_x = left + (x * 20) + screen_y = top - (y * 20) + color_number = grid[y][x] + color = colors[color_number] + pen.penColor(color) + pen.goTo(screen_x, screen_y, with_pen=False) + #pen.stamp() + pen.rectangle(18, 18, centered=True) + + +def check_grid(shape: Shape, score: LayerTurtle): + grid = shape.grid + + # Check if each row is full + y = 23 + while y > 0: + is_full = True + for x in range(0, 12): + if grid[y][x] == 0: + is_full = False + y -= 1 + break + if is_full: + shape.score_count += 10 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for copy_y in range(y, 0, -1): + for copy_x in range(0, 12): + grid[copy_y][copy_x] = grid[copy_y-1][copy_x] + +# def draw_score(pen: LayerTurtle): +# pen.penColor("blue") +# #pen.hideturtle() +# pen.goTo(-75, 350, with_pen=False) +# #pen.write("Score: {}".format(score), move=False, align="left", font=("Arial", 24, "normal")) +# pen.write("Score: {}".format(score), align="L") + + + +class TetrisClassicApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + # self.root: DDRootLayer = None + self.score: LayerTurtle = None + self.pen: LayerTurtle = None + # self.left_button: LayerLcd = None + # self.right_button: LayerLcd = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + width = 400 + height = 700 + + root = DDRootLayer(self.dd, width, height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + score = LayerTurtle(self.dd, width, height) + score.penColor('red') + score.penUp() + #score.hideturtle() + score.goTo(60, -300) + #score.write('Score: 0', align='center', font=('Courier', 24, 'normal')) + score.setTextFont("Courier", 24) + score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, width, height) + border.penSize(10) + border.penUp() + #border.hideturtle() + border.goTo(-130, 240) + border.penDown() + border.penColor('white') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + #border.write("TETRIS", align='center', font=('Courier', 36, 'normal')) + border.setTextFont("Courier", 36) + border.write("TETRIS", "C") + + pen = LayerTurtle(self.dd, width, height) + #pen.up() + # pen.speed(0) + # pen.shape('square') + # pen.shapesize(0.9, 0.9) + # pen.setundobuffer(None) + pen.penFilled() + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveShapeLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveShapeRight()) + + rotate_button = LayerLcd(self.dd, 2, 1, char_height=28) + rotate_button.noBackgroundColor() + rotate_button.writeLine("🔄") + rotate_button.enableFeedback("f", lambda *args: self.shapeRotate()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, rotate_button, right_button)).pin(self.dd) + + # self.root = root + self.score = score + self.pen = pen + # self.left_button = left_button + # self.right_button = right_button + + # Create the basic shape for the start of the game + shape = Shape() + + # # Put the shape in the grid + # grid[shape.y][shape.x] = shape.color + + self.shape = shape + + # wn.listen() + # wn.onkeypress(lambda: shape.move_left(grid), "a") + # wn.onkeypress(lambda: shape.move_right(grid), "d") + # wn.onkeypress(lambda: shape.rotate(grid), "space") + + def updateDD(self): + #global _dirty + now = time.time() + need_update = self.last_update_time is None or (now - self.last_update_time) >= _delay + if need_update: + self.update() + self.last_update_time = now + #_dirty = False + # else: + # if _dirty: + # self.drawGrid() + # _dirty = False + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + # Move the shape + # Open Row + # Check for the bottom + if self.shape.y == 23 - self.shape.height + 1: + if True: + self.checkGrid() + if self.shape.y == 23: + self.shape = None + else: + self.shape.reset() + # self.shape.x = 5 + # self.shape.y = 0 + else: + self.shape = Shape() + self.checkGrid() + # Check for collision with next row + elif self.shape.can_move(): + # Erase the current shape + self.shape.erase_shape() + + # Move the shape by 1 + self.shape.y +=1 + + # Draw the shape again + self.shape.draw_shape() + + else: + if True: + self.checkGrid() + if self.shape.y > 0: + self.shape.reset() + # self.shape.x = 5 + # self.shape.y = 0 + else: + self.shape = None + print("*** GAME OVER ***") + else: + self.shape = Shape() + self.checkGrid() + + # Draw the screen + if self.shape is not None: + self.drawGrid() + + def drawGrid(self): + self.dd.freezeDrawing() + draw_grid(shape=self.shape, pen=self.pen) + self.dd.unfreezeDrawing() + + def checkGrid(self): + check_grid(shape=self.shape, score=self.score) + + def moveShapeLeft(self): + self.shape.move_left() + self.drawGrid() + + def moveShapeRight(self): + self.shape.move_right() + self.drawGrid() + + def shapeRotate(self): + self.shape.rotate() + self.drawGrid() + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisClassicApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/OLD/tetris_one_block/main.py b/dumbdisplay_examples/OLD/tetris_one_block/main.py new file mode 100644 index 0000000..7576fbe --- /dev/null +++ b/dumbdisplay_examples/OLD/tetris_one_block/main.py @@ -0,0 +1,14 @@ +### +# this is just a sample entrypoint +### + +def sample_run(): + import random + from dumbdisplay_examples.tetris_one_block.tetris_one_block import TetrisOneBlockApp + print(f"*** Sample Run of TetrisOneBlockApp ***") + app = TetrisOneBlockApp() + app.run() + + +if __name__ == "__main__": + sample_run() \ No newline at end of file diff --git a/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block.py b/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block.py new file mode 100644 index 0000000..d5f40d1 --- /dev/null +++ b/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block.py @@ -0,0 +1,437 @@ +# *** +# *** Adapted from TETRIS ONE BLOCK\tetris_one_block.py of https://github.com/DimaGutierrez/Python-Games +# *** +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + +_USE_LEVEL_ANCHOR_FOR_BLOCK = True +_INIT_BLOCK_X = 5 +_RANDOMIZE_ROW_COUNT = 4 + + + +_width = 400 +_height = 700 +_top = 230 +_left = -110 +_block_unit_width = 20 +#_colors = ['black', 'red', 'lightblue', 'blue', 'orange', 'yellow', 'green', 'purple'] +_colors = ['black', 'crimson', 'cyan', 'ivory', 'coral', 'gold', 'lime', 'magenta'] + + +_delay = 0.3 # For time/sleep +_grid = [ # 12x24 + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [4,3,3,1,0,1,0,3,3,4,0,1], + [5,1,6,5,3,2,1,0,5,5,0,3], + [1,2,3,4,5,2,0,4,2,3,0,4], + [1,3,1,2,4,2,0,3,1,2,4,3], + [1,1,2,3,0,2,2,2,0,3,0,1], + [0,1,1,2,3,0,0,0,4,0,2,0], + [2,0,1,2,3,0,6,5,5,5,0,2] +] +_grid_n_cols = len(_grid[0]) # should be 12 +_grid_n_rows = len(_grid) # should be 24 + + +class Grid: + def __init__(self): + if _RANDOMIZE_ROW_COUNT >= 0: + grid = [] + for y in range(_grid_n_rows): + grid_row = [] + for x in range(_grid_n_cols): + if y >= (_grid_n_rows - _RANDOMIZE_ROW_COUNT) and random.random() < 0.7: + color = random.randint(1, len(_colors) - 1) + else: + color = 0 + grid_row.append(color) + grid.append(grid_row) + self.grid = grid + else: + self.grid = [] + for grid_row in _grid: + self.grid.append(grid_row.copy()) + self.grid_dirty = [] + for grid_row in self.grid: + grid_dirty_row = [] + for cell in grid_row: + dirty = True if cell != 0 else False + grid_dirty_row.append(dirty) + self.grid_dirty.append(grid_dirty_row) + self.n_cols = _grid_n_cols + self.n_rows = _grid_n_rows + + def check_reset_need_redraw(self, row_idx, col_idx): + dirty = self.grid_dirty[row_idx][col_idx] + if not dirty: + return False + self.grid_dirty[row_idx][col_idx] = False + return True + + def get_value(self, row_idx, col_idx): + return self.grid[row_idx][col_idx] + + def set_value(self, row_idx, col_idx, value): + if self.grid[row_idx][col_idx] != value: + self.grid[row_idx][col_idx] = value + self.grid_dirty[row_idx][col_idx] = True + # old_value = self.grid[row_idx][col_idx] + # self.grid[row_idx][col_idx] = value + # self.grid_dirty[row_idx][col_idx] = old_value != value + + + + +# def _calc_screen_position(x: int, y : int) -> (int, int): +# screen_x = _left + (x * 20) # each turtle 20x20 pixels +# screen_y = _top - (y * 20) +# return (screen_x, screen_y) + + +def _draw(x, y, color_number, pen: LayerTurtle): + screen_x = _left + (x * _block_unit_width) # each turtle 20x20 pixels + screen_y = _top - (y * _block_unit_width) + # (screen_x, screen_y) = _calc_screen_position(x, y) + color = _colors[color_number] + pen.penColor(color) + pen.goTo(screen_x, screen_y, with_pen=False) + pen.rectangle(_block_unit_width - 2, _block_unit_width - 2, centered=True) + +# def _draw_block(block: 'Block', block_pen: LayerTurtle): +# block_pen.clear() +# _draw(block.x, block.y, block.color, block_pen) + +def _draw_grid(grid: Grid, pen: LayerTurtle): + for y in range(grid.n_rows): + for x in range(grid.n_cols): + if not grid.check_reset_need_redraw(y, x): + continue + color_number = grid.get_value(y, x) + _draw(x, y, color_number, pen) + + + +class Block: + def __init__(self, x: int, y: int, block_pen: LayerTurtle): + self.x = _INIT_BLOCK_X + self.y = 0 + self.x = x + self.y = y + self.color = random.randint(1, len(_colors) - 1) + self.block_pen = block_pen + if _USE_LEVEL_ANCHOR_FOR_BLOCK: + self.block_pen.clear() + _draw(self.x, self.y, self.color, self.block_pen) + self.sync_image() + + def commit(self, grid: Grid): + grid.set_value(self.y, self.x, self.color) + if True: + self.block_pen.clear() + + def move_down(self, grid: Grid) -> bool: + if self.y < 23 and grid.get_value(self.y + 1, self.x) == 0: + self.y += 1 + self.sync_image() + return True + return False + + def move_right(self, grid: Grid) -> bool: + if self.x < 11: + if grid.get_value(self.y, self.x + 1) == 0: + self.x += 1 + self.sync_image() + return True + return False + + def move_left(self, grid: Grid) -> bool: + if self.x > 0: + if grid.get_value(self.y, self.x - 1) == 0: + self.x -= 1 + self.sync_image() + return True + return False + + def sync_image(self): + if _USE_LEVEL_ANCHOR_FOR_BLOCK: + anchor_x = (self.x - _INIT_BLOCK_X) * _block_unit_width + anchor_y = self.y * _block_unit_width + #(screen_x, screen_y) = _calc_screen_position(x, y) + self.block_pen.setLevelAnchor(anchor_x, anchor_y) + else: + self.block_pen.clear() + _draw(self.x, self.y, self.color, self.block_pen) + + + +def _check_grid(shape: 'Shape', score: LayerTurtle) -> (bool, int): + grid = shape.grid + block = shape.block + + # Check if each row is full: + empty_count = 23 + deleted_count = 0 + for y in range(0,24): + is_full = True + is_empty = True + y_erase = y + for x in range(0,12): + if grid.get_value(y, x) == 0: + is_full = False + else: + is_empty = False + if not is_empty and not is_full: + empty_count -= 1 + break + # Remove row and shift down + if is_full: + shape.score_count += 1 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for y in range(y_erase-1, -1, -1): + for x in range(0,12): + grid.set_value(y + 1, x, grid.get_value(y, x)) + + deleted_count += 1 + + return (empty_count == 23, deleted_count) + + +class Shape: + def __init__(self, pen: LayerTurtle, block_pen: LayerTurtle): + self.grid = Grid() + self.score_count = 0 + self.block: Block = None + self.pen = pen + self.block_pen = block_pen + if not self.reset_block(): + raise Exception("Failed to create initial block") + + def check_grid(self, score: LayerTurtle) -> bool: + (all_empty, delete_count) = _check_grid(self, score) + if delete_count > 0: + self.sync_image() + return all_empty and self.block is None + + def reset_block(self) -> bool: + x = 5 + y = 0 + if self.grid.get_value(y, x) != 0: + #self.sync_image() + return False + self.block = Block(x, y, self.block_pen) + self.sync_image() + return True + + def commit_block(self): + self.block.commit(self.grid) + self.sync_image() + self.block = None + + def move_block_down(self) -> bool: + return self.block.move_down(self.grid) + + def move_block_right(self) -> bool: + return self.block.move_right(self.grid) + + def move_block_left(self) -> bool: + return self.block.move_left(self.grid) + + def sync_image(self): + #self.block.sync_image() + _draw_grid(self.grid, self.pen) + + +class TetrisOneBlockApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + self.score: LayerTurtle = None + self.block_pen: LayerTurtle = None + self.pen: LayerTurtle = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + + root = DDRootLayer(self.dd, _width, _height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + block_pen = LayerTurtle(self.dd, _width, _height) + block_pen.penFilled() + #block_pen.setTextSize(32) + + pen = LayerTurtle(self.dd, _width, _height) + pen.penFilled() + pen.setTextSize(32) + + score = LayerTurtle(self.dd, _width, _height) + score.penColor('red') + score.penUp() + score.goTo(60, -300) + score.setTextFont("Courier", 24) + #score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, _width, _height) + if False: + border.rectangle(260, 490, centered=True) + border.penSize(10) + border.penUp() + border.goTo(-130, 240) + border.penDown() + border.penColor('linen') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + border.setTextFont("Courier", 36) + border.write("One-Block TETRIS", "C") + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveBlockLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveBlockRight()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, right_button)).pin(self.dd) + + self.score = score + self.block_pen = block_pen + self.pen = pen + + self.startGame() + #self.shape = Shape() + #self.resetBlock() + + self.last_update_time = time.time() + + def updateDD(self): + now = time.time() + need_update = (now - self.last_update_time) >= _delay + if need_update: + self.last_update_time = now + self.update() + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + moved_down = self.moveBlockDown() + if not moved_down: + self.shape.commit_block() + won = self.checkGrid() + if won: + self.endGame(won=True) + elif not moved_down: + if not self.resetBlock(): + self.endGame(won=False) + # if self.shape.block.y > 0: + # #self.shape.reset_block() + # self.resetBlock() + # else: + # self.endGame(won=False) + + def startGame(self): + self.score.clear() + self.score.write('Score: 0', 'C') + self.pen.clear() + self.block_pen.clear() + self.shape = Shape(pen=self.pen, block_pen=self.block_pen) + #self.resetBlock() + + + def endGame(self, won: bool): + #self.block_pen.clear() + self.shape = None + if won: + msg = "🥳 YOU WON 🥳" + color = "purple" + else: + msg = "GAME OVER 😔" + color = "darkgray" + self.pen.home(with_pen=False) + self.pen.penColor("white") + self.pen.oval(300, 100, centered=True) + self.pen.penColor(color) + self.pen.write(msg, align='C') + + + def checkGrid(self) -> bool: + return self.shape.check_grid(score=self.score) + # check_result = check_grid(shape=self.shape, score=self.score) + # self.drawGrid() # should only redraw if any lines were cleared + # return check_result + + def resetBlock(self) -> bool: + return self.shape.reset_block() + #self.drawGrid() + #self.drawBlock() + + def moveBlockDown(self) -> bool: + return self.shape.move_block_down() + # if self.shape.move_block_down(): + # self.drawBlock() + # return True + # return False + + def moveBlockLeft(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_left() + # if self.shape.move_block_left(): + # self.drawBlock() + # return True + # return False + + def moveBlockRight(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_right() + # if self.shape.move_block_right(): + # self.drawBlock() + # return True + # return False + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisOneBlockApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block_2.py b/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block_2.py new file mode 100644 index 0000000..7bb5302 --- /dev/null +++ b/dumbdisplay_examples/OLD/tetris_one_block/tetris_one_block_2.py @@ -0,0 +1,428 @@ +# *** +# *** Adapted from TETRIS ONE BLOCK\tetris_one_block.py of https://github.com/DimaGutierrez/Python-Games +# *** + +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + +_RANDOMIZE_ROW_COUNT = 2 + + +_width = 400 +_height = 700 +_top = 230 +_left = -110 +_block_unit_width = 20 +#_colors = ['black', 'red', 'lightblue', 'blue', 'orange', 'yellow', 'green', 'purple'] +_colors = ['black', 'crimson', 'cyan', 'ivory', 'coral', 'gold', 'lime', 'magenta'] +_grid_n_cols = 12 +_grid_n_rows = 24 + + +_delay = 0.3 # For time/sleep + +class Grid: + def __init__(self, grid_cells): + self.grid_cells = grid_cells + self.grid_dirty = [] + for grid_row in self.grid_cells: + grid_dirty_row = [] + for cell in grid_row: + dirty = True if cell != 0 else False + grid_dirty_row.append(dirty) + self.grid_dirty.append(grid_dirty_row) + self.n_cols = len(self.grid_cells[0]) + self.n_rows = len(self.grid_cells) + + def check_reset_need_redraw(self, row_idx, col_idx): + dirty = self.grid_dirty[row_idx][col_idx] + if not dirty: + return False + self.grid_dirty[row_idx][col_idx] = False + return True + + def get_value(self, row_idx, col_idx): + return self.grid_cells[row_idx][col_idx] + + def set_value(self, row_idx, col_idx, value): + if self.grid_cells[row_idx][col_idx] != value: + self.grid_cells[row_idx][col_idx] = value + self.grid_dirty[row_idx][col_idx] = True + + +# def _draw(x, y, color_number, pen: LayerTurtle): +# screen_x = _left + (x * _block_unit_width) # each turtle 20x20 pixels +# screen_y = _top - (y * _block_unit_width) +# # (screen_x, screen_y) = _calc_screen_position(x, y) +# color = _colors[color_number] +# pen.penColor(color) +# pen.goTo(screen_x, screen_y, with_pen=False) +# pen.rectangle(_block_unit_width - 2, _block_unit_width - 2, centered=True) + + +def _randomize_block_grid() -> Grid: + color = random.randint(1, len(_colors) - 1) + if True: + n_rows = random.randint(1, 2) + n_cols = random.randint(1, 2) + block_grid = [] + for y in range(n_rows): + block_grid_row = [] + for x in range(n_cols): + block_grid_row.append(color) + block_grid.append(block_grid_row) + else: + block_grid = [[color, color]] + return Grid(grid_cells=block_grid) + +def _randomize_grid() -> Grid: + grid_cells = [] + for y in range(_grid_n_rows): + grid_row = [] + for x in range(_grid_n_cols): + if y >= (_grid_n_rows - _RANDOMIZE_ROW_COUNT) and random.random() < 0.7: + color = random.randint(1, len(_colors) - 1) + else: + color = 0 + grid_row.append(color) + grid_cells.append(grid_row) + return Grid(grid_cells=grid_cells) + +def _check_can_place_block_grid(block_grid: Grid, block_grid_x_off: int, block_grid_y_offset: int, grid: Grid) -> bool: + for y in range(block_grid.n_rows): + for x in range(block_grid.n_cols): + if block_grid.get_value(y, x) != 0: + row_idx = y + block_grid_y_offset + col_idx = x + block_grid_x_off + if row_idx < 0 or row_idx >= grid.n_rows: + return True + if col_idx < 0 or col_idx >= grid.n_cols: + return True + if grid.get_value(row_idx, col_idx) != 0: + return True + return False + + +def _commit_block_grid(block_grid: Grid, block_grid_x_off: int, block_grid_y_offset: int, grid: Grid): + for y in range(block_grid.n_rows): + for x in range(block_grid.n_cols): + color = block_grid.get_value(y, x) + if color != 0: + grid.set_value(y + block_grid_y_offset, x + block_grid_x_off, color) + + +def _draw(x, y, color_number, pen: LayerTurtle): + screen_x = _left + (x * _block_unit_width) # each turtle 20x20 pixels + screen_y = _top - (y * _block_unit_width) + # (screen_x, screen_y) = _calc_screen_position(x, y) + color = _colors[color_number] + pen.penColor(color) + pen.goTo(screen_x, screen_y, with_pen=False) + pen.rectangle(_block_unit_width - 2, _block_unit_width - 2, centered=True) + +def _draw_grid(grid: Grid, pen: LayerTurtle): + for y in range(grid.n_rows): + for x in range(grid.n_cols): + if not grid.check_reset_need_redraw(y, x): + continue + color_number = grid.get_value(y, x) + _draw(x, y, color_number, pen) + + + +class Block: + def __init__(self, x: int, y: int, block_grid: Grid, block_pen: LayerTurtle): + self.x = x + self.y = y + self.block_grid = block_grid + self.block_pen = block_pen + block_pen.clear() + self.sync_image() + _draw_grid(block_grid, block_pen) + + def move_down(self, grid: Grid) -> bool: + if _check_can_place_block_grid(self.block_grid, self.x, self.y + 1, grid=grid): + return False + self.y += 1 + self.sync_image() + return True + + def move_right(self, grid: Grid) -> bool: + if _check_can_place_block_grid(self.block_grid, self.x + 1, self.y, grid=grid): + return False + self.x += 1 + self.sync_image() + return True + + def move_left(self, grid: Grid) -> bool: + if _check_can_place_block_grid(self.block_grid, self.x - 1, self.y, grid=grid): + return False + self.x -= 1 + self.sync_image() + return True + + def sync_image(self): + #anchor_x = (self.x - _INIT_BLOCK_X) * _block_unit_width + anchor_x = self.x * _block_unit_width + anchor_y = self.y * _block_unit_width + self.block_pen.setLevelAnchor(anchor_x, anchor_y) + + + +def _check_grid(shape: 'Shape', score: LayerTurtle) -> (bool, int): + grid = shape.grid + block = shape.block + + # Check if each row is full: + empty_count = 23 + deleted_count = 0 + for y in range(0,24): + is_full = True + is_empty = True + y_erase = y + for x in range(0,12): + if grid.get_value(y, x) == 0: + is_full = False + else: + is_empty = False + if not is_empty and not is_full: + empty_count -= 1 + break + # Remove row and shift down + if is_full: + shape.score_count += 1 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for y in range(y_erase-1, -1, -1): + for x in range(0,12): + grid.set_value(y + 1, x, grid.get_value(y, x)) + + deleted_count += 1 + + return (empty_count == 23, deleted_count) + + +class Shape: + def __init__(self, pen: LayerTurtle, block_pen: LayerTurtle): + self.grid = _randomize_grid() + self.score_count = 0 + self.block: Block = None + self.pen = pen + self.block_pen = block_pen + if not self.reset_block(): + raise Exception("Failed to create initial block") + + def check_grid(self, score: LayerTurtle) -> bool: + (all_empty, delete_count) = _check_grid(self, score) + if delete_count > 0: + self.sync_image() + return all_empty and self.block is None + + def reset_block(self) -> bool: + x = 5 + y = 0 + block_grid = _randomize_block_grid() + x -= 1 - block_grid.n_cols + y -= 1 - block_grid.n_rows + if _check_can_place_block_grid(block_grid, x, y, grid=self.grid): + return False + self.block = Block(x, y, block_grid=block_grid, block_pen=self.block_pen) + self.sync_image() + return True + + def commit_block(self): + _commit_block_grid(self.block.block_grid, self.block.x, self.block.y, self.grid) + #self.block.commit(self.grid) + self.sync_image() + self.block = None + + def move_block_down(self) -> bool: + return self.block.move_down(self.grid) + + def move_block_right(self) -> bool: + return self.block.move_right(self.grid) + + def move_block_left(self) -> bool: + return self.block.move_left(self.grid) + + def sync_image(self): + #self.block.sync_image() + _draw_grid(self.grid, self.pen) + + +class TetrisOneBlock2App(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + self.score: LayerTurtle = None + self.block_pen: LayerTurtle = None + self.pen: LayerTurtle = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + + root = DDRootLayer(self.dd, _width, _height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + block_pen = LayerTurtle(self.dd, _width, _height) + block_pen.penFilled() + #block_pen.setTextSize(32) + + pen = LayerTurtle(self.dd, _width, _height) + pen.penFilled() + pen.setTextSize(32) + + score = LayerTurtle(self.dd, _width, _height) + score.penColor('red') + score.penUp() + score.goTo(60, -300) + score.setTextFont("Courier", 24) + #score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, _width, _height) + if False: + border.rectangle(260, 490, centered=True) + border.penSize(10) + border.penUp() + border.goTo(-130, 240) + border.penDown() + border.penColor('linen') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + border.setTextFont("Courier", 36) + border.write("One-Block TETRIS", "C") + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveBlockLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveBlockRight()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, right_button)).pin(self.dd) + + self.score = score + self.block_pen = block_pen + self.pen = pen + + self.startGame() + #self.shape = Shape() + #self.resetBlock() + + self.last_update_time = time.time() + + def updateDD(self): + now = time.time() + need_update = (now - self.last_update_time) >= _delay + if need_update: + self.last_update_time = now + self.update() + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + moved_down = self.moveBlockDown() + if not moved_down: + self.shape.commit_block() + won = self.checkGrid() + if won: + self.endGame(won=True) + elif not moved_down: + if not self.resetBlock(): + self.endGame(won=False) + # if self.shape.block.y > 0: + # #self.shape.reset_block() + # self.resetBlock() + # else: + # self.endGame(won=False) + + def startGame(self): + self.score.clear() + self.score.write('Score: 0', 'C') + self.pen.clear() + self.block_pen.clear() + self.shape = Shape(pen=self.pen, block_pen=self.block_pen) + #self.resetBlock() + + + def endGame(self, won: bool): + #self.block_pen.clear() + self.shape = None + if won: + msg = "🥳 YOU WON 🥳" + color = "purple" + else: + msg = "GAME OVER 😔" + color = "darkgray" + self.pen.home(with_pen=False) + self.pen.penColor("white") + self.pen.oval(300, 100, centered=True) + self.pen.penColor(color) + self.pen.write(msg, align='C') + + + def checkGrid(self) -> bool: + return self.shape.check_grid(score=self.score) + # check_result = check_grid(shape=self.shape, score=self.score) + # self.drawGrid() # should only redraw if any lines were cleared + # return check_result + + def resetBlock(self) -> bool: + return self.shape.reset_block() + #self.drawGrid() + #self.drawBlock() + + def moveBlockDown(self) -> bool: + return self.shape.move_block_down() + # if self.shape.move_block_down(): + # self.drawBlock() + # return True + # return False + + def moveBlockLeft(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_left() + # if self.shape.move_block_left(): + # self.drawBlock() + # return True + # return False + + def moveBlockRight(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_right() + # if self.shape.move_block_right(): + # self.drawBlock() + # return True + # return False + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisOneBlock2App(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/mnist/main.py b/dumbdisplay_examples/mnist/main.py new file mode 100644 index 0000000..9ae1b91 --- /dev/null +++ b/dumbdisplay_examples/mnist/main.py @@ -0,0 +1,16 @@ +### +# this is just a sample entrypoint +### + + +def sample_run(): + import random + from dumbdisplay_examples.mnist.mnist_app import MnistApp + print(f"*** Sample run of MnistApp ***") + inference_func = lambda board_manager: random.randint(0, 9) + app = MnistApp(inference_func=inference_func) + app.run() + + +if __name__ == "__main__": + sample_run() \ No newline at end of file diff --git a/dumbdisplay_examples/passive_blink/main.py b/dumbdisplay_examples/passive_blink/main.py new file mode 100644 index 0000000..3319c53 --- /dev/null +++ b/dumbdisplay_examples/passive_blink/main.py @@ -0,0 +1,13 @@ +### +# this is just a sample entrypoint +### + +def sample_run(): + from dumbdisplay_examples.passive_blink.passive_blink_app import PassiveBlinkApp + print(f"*** Sample Run of PassiveBlinkApp ***") + app = PassiveBlinkApp() + app.run() + + +if __name__ == "__main__": + sample_run() \ No newline at end of file diff --git a/dumbdisplay_examples/sliding_puzzle/main.py b/dumbdisplay_examples/sliding_puzzle/main.py new file mode 100644 index 0000000..ff3653c --- /dev/null +++ b/dumbdisplay_examples/sliding_puzzle/main.py @@ -0,0 +1,15 @@ +### +# this is just a sample entrypoint +### + +def sample_run(): + import random + from dumbdisplay_examples.sliding_puzzle.sliding_puzzle_app import SlidingPuzzleApp + print(f"*** Sample Run of SlidingPuzzleApp ***") + suggest_move_from_dir_func = lambda board_manager: random.randint(0, 3) + app = SlidingPuzzleApp(suggest_move_from_dir_func=suggest_move_from_dir_func) + app.run() + + +if __name__ == "__main__": + sample_run() \ No newline at end of file diff --git a/dumbdisplay_examples/sliding_puzzle/sliding_puzzle_app.py b/dumbdisplay_examples/sliding_puzzle/sliding_puzzle_app.py index 2f35e08..92726fb 100644 --- a/dumbdisplay_examples/sliding_puzzle/sliding_puzzle_app.py +++ b/dumbdisplay_examples/sliding_puzzle/sliding_puzzle_app.py @@ -15,9 +15,9 @@ class SlidingPuzzleApp: def __init__(self, dd: DumbDisplay = create_example_wifi_dd(), tile_count: int = DEF_TILE_COUNT, suggest_move_from_dir_func = None): - ''' + """ :param suggest_move_from_dir_func: if not None, a function that access BoardManager and returns the next move (0 / 1 / 2 / 3) - ''' + """ self.tile_count = tile_count self.tile_size = BOARD_SIZE / tile_count self.suggest_move_from_dir_func = suggest_move_from_dir_func @@ -393,9 +393,9 @@ def posToHoleTileFromIdxes(self, x: int, y: int) -> (int, int, int, bool): def showHideHoleTile(self, show: bool): - ''' + """ show / hide the hole tile, which might not be in position - ''' + """ hole_tile_id = self.board_manager.board_tiles[self.board_manager.hole_tile_row_idx * self.tile_count + self.board_manager.hole_tile_col_idx] hole_tile_level_id = str(hole_tile_id) anchor_x = self.board_manager.hole_tile_col_idx * self.tile_size diff --git a/dumbdisplay_examples/tetris/_common.py b/dumbdisplay_examples/tetris/_common.py new file mode 100644 index 0000000..607575c --- /dev/null +++ b/dumbdisplay_examples/tetris/_common.py @@ -0,0 +1,250 @@ +import random + +from dumbdisplay.layer_turtle import LayerTurtle + + + + +_width = 400 +_height = 700 +_top = 230 +_left = -110 +_block_unit_width = 20 +#_colors = ['black', 'red', 'lightblue', 'blue', 'orange', 'yellow', 'green', 'purple'] +_colors = ['black', 'crimson', 'cyan', 'ivory', 'coral', 'gold', 'lime', 'magenta'] + +_grid_n_cols = 12 +_grid_n_rows = 24 + + +class Grid: + def __init__(self, grid_cells: list[list[int]], grid_cell_type = None): + self.grid_cells = grid_cells + self.grid_cell_type = grid_cell_type + self.grid_dirty = [] + for grid_row in self.grid_cells: + grid_dirty_row = [] + for cell in grid_row: + dirty = True if cell != 0 else False + grid_dirty_row.append(dirty) + self.grid_dirty.append(grid_dirty_row) + self.n_cols = len(self.grid_cells[0]) + self.n_rows = len(self.grid_cells) + + def check_reset_need_redraw(self, row_idx, col_idx): + dirty = self.grid_dirty[row_idx][col_idx] + if not dirty: + return False + self.grid_dirty[row_idx][col_idx] = False + return True + + def get_value(self, row_idx, col_idx): + return self.grid_cells[row_idx][col_idx] + + def set_value(self, row_idx, col_idx, value): + if self.grid_cells[row_idx][col_idx] != value: + self.grid_cells[row_idx][col_idx] = value + self.grid_dirty[row_idx][col_idx] = True + + +class Block: + def __init__(self, x: int, y: int, block_grid: Grid, block_pen: LayerTurtle, move_reach_in_millis: int = 50, rotate_with_level: bool = False): + self.x = x + self.y = y + self.rotation = 0 + self.block_grid = block_grid + self.block_pen = block_pen + self.rotate_with_level = rotate_with_level + self.move_reach_in_millis = move_reach_in_millis + block_pen.clear() + if not rotate_with_level: + # make the block tiled a bit + block_pen.setLevelRotation(2, 90, 120) # calculated from _left and _top + self.sync_image() + _draw_grid(block_grid, block_pen) + + def move_down(self, grid: Grid) -> bool: + if _check_bad_block_grid_placement(self.block_grid, self.x, self.y + 1, grid=grid): + return False + self.y += 1 + self.sync_image(self.move_reach_in_millis) + return True + + def move_right(self, grid: Grid) -> bool: + if _check_bad_block_grid_placement(self.block_grid, self.x + 1, self.y, grid=grid): + return False + self.x += 1 + self.sync_image(self.move_reach_in_millis) + return True + + def move_left(self, grid: Grid) -> bool: + if _check_bad_block_grid_placement(self.block_grid, self.x - 1, self.y, grid=grid): + return False + self.x -= 1 + self.sync_image(self.move_reach_in_millis) + return True + + def rotate(self, grid: Grid) -> bool: + (rotated_block_grid, y_offset) = _rotate_block_grid_if_possible(self.block_grid, self.x, self.y, grid=grid) + if rotated_block_grid is None: + return False + self.block_grid = rotated_block_grid + self.y += y_offset + self.rotation = (self.rotation + 1) % 4 + if self.rotate_with_level: + self.sync_image(self.move_reach_in_millis) + else: + self.block_pen.clear() + #self.sync_image(self.move_reach_in_millis) + _draw_grid(self.block_grid, self.block_pen) + return True + + + def sync_image(self, reach_in_millis: int = 0): + from dumbdisplay_examples.tetris._shapes import _vertical_line, _horizontal_line, _left_l, _right_l, _left_s, _right_s, _t + if self.rotate_with_level: + angle = 90 * self.rotation + 2 + pivot_x = 90 + 10 + pivot_y = 120 + 10 + x = self.x + y = self.y + # n_rows = self.block_grid.n_rows + # n_cols = self.block_grid.n_cols + rotation = self.rotation + block_type = self.block_grid.grid_cell_type + # if rotation == 1: + # print("rotated 90") + # if True: # square + # pass + if block_type is _vertical_line: # _vertical_line + if rotation == 1: + x += 2 + elif rotation == 2: + x -= 1 + elif rotation == 3: + y -= 1 + elif block_type is _horizontal_line: # _horizontal_line + if rotation == 1: + x -= 1 + elif rotation == 2: + x += 2 + y -= 1 + # elif self.rotation == 3: + # pass + elif block_type is _left_l or block_type is _right_l: # _left_l and _right_l + # if self.rotation == 1: + # pass + if rotation == 2: + x += 2 + y -= 1 + # elif self.rotation == 3: + # pass + elif block_type is _left_s or block_type is _right_s or block_type is _t: # _left_s and _right_s and _t + # if self.rotation == 1: + # pass + if rotation == 2: + x += 1 + y -= 1 + # elif self.rotation == 3: + # pass + anchor_x = x * _block_unit_width + anchor_y = y * _block_unit_width + self.block_pen.setLevelRotation(angle, pivot_x, pivot_y, reach_in_millis) # calculated from _left and _top + self.block_pen.setLevelAnchor(anchor_x, anchor_y, reach_in_millis) + else: + anchor_x = self.x * _block_unit_width + anchor_y = self.y * _block_unit_width + self.block_pen.setLevelAnchor(anchor_x, anchor_y, reach_in_millis) + + + +def _check_bad_block_grid_cells_placement(grid_cells: list[list[int]], block_grid_x_off: int, block_grid_y_offset: int, grid: Grid, check_boundary: bool = True) -> bool: + n_rows = len(grid_cells) + n_cols = len(grid_cells[0]) if n_rows > 0 else 0 + for y in range(n_rows): + for x in range(n_cols): + if grid_cells[y][x] != 0: + row_idx = y + block_grid_y_offset + col_idx = x + block_grid_x_off + if row_idx < 0: + continue # never mind above the grid + if row_idx < 0 or row_idx >= grid.n_rows: + if not check_boundary: + continue + return True + if col_idx < 0 or col_idx >= grid.n_cols: + if not check_boundary: + continue + return True + if grid.get_value(row_idx, col_idx) != 0: + return True + return False + + +def _rotate_block_grid_cells(grid_cells: list[list[int]]) -> list[list[int]]: + rotated = [] + for x in range(len(grid_cells[0])): + new_row = [] + for y in range(len(grid_cells) -1, -1, -1): + new_row.append(grid_cells[y][x]) + rotated.append(new_row) + return rotated + + +def _randomize_grid(randomize_row_count: int) -> Grid: + grid_cells = [] + for y in range(_grid_n_rows): + grid_row = [] + for x in range(_grid_n_cols): + if y >= (_grid_n_rows - randomize_row_count) and random.random() < 0.7: + color = random.randint(1, len(_colors) - 1) + else: + color = 0 + grid_row.append(color) + grid_cells.append(grid_row) + return Grid(grid_cells=grid_cells) + + +def _draw(x, y, color_number, pen: LayerTurtle): + screen_x = _left + (x * _block_unit_width) # each turtle 20x20 pixels + screen_y = _top - (y * _block_unit_width) + # (screen_x, screen_y) = _calc_screen_position(x, y) + color = _colors[color_number] + pen.penColor(color) + pen.goTo(screen_x, screen_y, with_pen=False) + pen.rectangle(_block_unit_width - 2, _block_unit_width - 2, centered=True) + +# def _draw_block(block: 'Block', block_pen: LayerTurtle): +# block_pen.clear() +# _draw(block.x, block.y, block.color, block_pen) + +def _draw_grid(grid: Grid, pen: LayerTurtle): + for y in range(grid.n_rows): + for x in range(grid.n_cols): + if not grid.check_reset_need_redraw(y, x): + continue + color_number = grid.get_value(y, x) + _draw(x, y, color_number, pen) + +def _check_bad_block_grid_placement(block_grid: Grid, block_grid_x_off: int, block_grid_y_offset: int, grid: Grid, check_boundary: bool = True) -> bool: + return _check_bad_block_grid_cells_placement(block_grid.grid_cells, block_grid_x_off, block_grid_y_offset, grid, check_boundary) + +def _rotate_block_grid_if_possible(block_grid: Grid, block_grid_x_off: int, block_grid_y_offset: int, grid: Grid) -> (Grid, int): + block_grid_cells = block_grid.grid_cells + rotated_cells = _rotate_block_grid_cells(block_grid_cells) + #y_offset = len(rotated_cells) - len(block_grid_cells) + y_offset = 0 + if _check_bad_block_grid_cells_placement(rotated_cells, block_grid_x_off, block_grid_y_offset + y_offset, grid, check_boundary=True): + return (None, None) + grid_cell_type = block_grid.grid_cell_type + rotated_block_grid = Grid(grid_cells=rotated_cells, grid_cell_type=grid_cell_type) + return (rotated_block_grid, y_offset) + + +def _commit_block_grid(block_grid: Grid, block_grid_x_off: int, block_grid_y_offset: int, grid: Grid): + for y in range(block_grid.n_rows): + for x in range(block_grid.n_cols): + color = block_grid.get_value(y, x) + if color != 0: + grid.set_value(y + block_grid_y_offset, x + block_grid_x_off, color) + diff --git a/dumbdisplay_examples/tetris/_shapes.py b/dumbdisplay_examples/tetris/_shapes.py new file mode 100644 index 0000000..610c1d9 --- /dev/null +++ b/dumbdisplay_examples/tetris/_shapes.py @@ -0,0 +1,41 @@ +import random + +from dumbdisplay_examples.tetris._common import Grid, _colors + +_square = [[1,1], + [1,1]] + +_horizontal_line = [[1,1,1,1]] + +_vertical_line = [[1], + [1], + [1], + [1]] + +_left_l = [[1,0,0,0], + [1,1,1,1]] + +_right_l = [[0,0,0,1], + [1,1,1,1]] + +_left_s = [[1,1,0], + [0,1,1]] + +_right_s = [[0,1,1], + [1,1,0]] + +_t = [[0,1,0], + [1,1,1]] + +_shapes = [_square, _horizontal_line, _vertical_line, _left_l, _right_l, _left_s, _right_s, _t] + + +def _randomize_block_grid() -> Grid: + block_grid = random.choice(_shapes) + # if True: + # block_grid = _t + color = random.randint(1, len(_colors) - 1) + block_grid_cell_type = block_grid + block_grid = [[color if cell != 0 else 0 for cell in row] for row in block_grid] + return Grid(grid_cells=block_grid, grid_cell_type=block_grid_cell_type) + diff --git a/dumbdisplay_examples/tetris/tetris_classic.py b/dumbdisplay_examples/tetris/tetris_classic.py new file mode 100644 index 0000000..d0ad308 --- /dev/null +++ b/dumbdisplay_examples/tetris/tetris_classic.py @@ -0,0 +1,287 @@ +# *** +# *** Adapted from TETRIS CLASSIC\tetris_turtle.py of https://github.com/DimaGutierrez/Python-Games +# *** - modified from dumbdisplay_examples/tetris/tetris_two_block.py +# *** + +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd +from dumbdisplay_examples.tetris._common import Grid, _colors, _grid_n_rows, _grid_n_cols, _block_unit_width, \ + _width, _height, _left, _top, _draw_grid, _commit_block_grid, Block, \ + _check_bad_block_grid_placement, _randomize_grid, _rotate_block_grid_if_possible +from dumbdisplay_examples.tetris._shapes import _randomize_block_grid + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + +_RANDOMIZE_ROW_COUNT = 2 +_ROTATE_WITH_LEVEL = True + + +_delay = 0.3 # For time/sleep +_level_animation_millis = 50 + +# _delay = 5000 # TODO: reset after debug +# _level_animation_millis = 5000 + +def _check_grid(shape: 'Shape', score: LayerTurtle) -> (bool, int): + grid = shape.grid + block = shape.block + + # Check if each row is full: + empty_count = 23 + deleted_count = 0 + for y in range(0,24): + is_full = True + is_empty = True + y_erase = y + for x in range(0,12): + if grid.get_value(y, x) == 0: + is_full = False + else: + is_empty = False + if not is_empty and not is_full: + empty_count -= 1 + break + # Remove row and shift down + if is_full: + shape.score_count += 1 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for y in range(y_erase-1, -1, -1): + for x in range(0,12): + grid.set_value(y + 1, x, grid.get_value(y, x)) + + deleted_count += 1 + + return (empty_count == 23, deleted_count) + + +class Shape: + def __init__(self, pen: LayerTurtle, block_pen: LayerTurtle): + self.grid = _randomize_grid(_RANDOMIZE_ROW_COUNT) + self.score_count = 0 + self.block: Block = None + self.pen = pen + self.block_pen = block_pen + if not self.reset_block(): + raise Exception("Failed to create initial block") + + def check_grid(self, score: LayerTurtle) -> bool: + (all_empty, delete_count) = _check_grid(self, score) + if delete_count > 0: + self.sync_image() + return all_empty and self.block is None + + def reset_block(self) -> bool: + x = 5 + y = 0 + block_grid = _randomize_block_grid() + x -= int((block_grid.n_cols - 1) / 2) + y += 1 - block_grid.n_rows + if _check_bad_block_grid_placement(block_grid, x, y, grid=self.grid, check_boundary=False): + return False + self.block = Block(x, y, block_grid=block_grid, block_pen=self.block_pen, move_reach_in_millis=_level_animation_millis, rotate_with_level=_ROTATE_WITH_LEVEL) + self.sync_image() + return True + + def commit_block(self): + _commit_block_grid(self.block.block_grid, self.block.x, self.block.y, self.grid) + #self.block.commit(self.grid) + self.sync_image() + self.block = None + + def move_block_down(self) -> bool: + return self.block.move_down(self.grid) + + def move_block_right(self) -> bool: + return self.block.move_right(self.grid) + + def rotate_block(self) -> bool: + return self.block.rotate(self.grid) + + def move_block_left(self) -> bool: + return self.block.move_left(self.grid) + + def sync_image(self): + #self.block.sync_image() + _draw_grid(self.grid, self.pen) + + +class TetrisTwoBlockApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + self.score: LayerTurtle = None + self.block_pen: LayerTurtle = None + self.pen: LayerTurtle = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + + root = DDRootLayer(self.dd, _width, _height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + block_pen = LayerTurtle(self.dd, _width, _height) + block_pen.penFilled() + #block_pen.setTextSize(32) + + pen = LayerTurtle(self.dd, _width, _height) + pen.penFilled() + pen.setTextSize(32) + + score = LayerTurtle(self.dd, _width, _height) + score.penColor('red') + score.penUp() + score.goTo(60, -300) + score.setTextFont("Courier", 24) + #score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, _width, _height) + if False: + border.rectangle(260, 490, centered=True) + border.penSize(10) + border.penUp() + border.goTo(-130, 240) + border.penDown() + border.penColor('linen') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + border.setTextFont("Courier", 36) + border.write("TETRIS", "C") + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveBlockLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveBlockRight()) + + rotate_button = LayerLcd(self.dd, 2, 1, char_height=28) + rotate_button.noBackgroundColor() + rotate_button.writeLine("🔄") + rotate_button.enableFeedback("f", lambda *args: self.rotateBlock()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, rotate_button, right_button)).pin(self.dd) + + self.score = score + self.block_pen = block_pen + self.pen = pen + + self.startGame() + #self.shape = Shape() + #self.resetBlock() + + self.last_update_time = time.time() + + def updateDD(self): + now = time.time() + need_update = (now - self.last_update_time) >= _delay + if need_update: + self.last_update_time = now + self.update() + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + moved_down = self.moveBlockDown() + if not moved_down: + self.shape.commit_block() + won = self.checkGrid() + if won: + self.endGame(won=True) + elif not moved_down: + if not self.resetBlock(): + self.endGame(won=False) + # if self.shape.block.y > 0: + # #self.shape.reset_block() + # self.resetBlock() + # else: + # self.endGame(won=False) + + def startGame(self): + self.score.clear() + self.score.write('Score: 0', 'C') + self.pen.clear() + self.block_pen.clear() + self.shape = Shape(pen=self.pen, block_pen=self.block_pen) + print("... started game") + + + def endGame(self, won: bool): + #self.block_pen.clear() + self.shape = None + if won: + msg = "🥳 YOU WON 🥳" + color = "purple" + else: + msg = "GAME OVER 😔" + color = "darkgray" + self.pen.home(with_pen=False) + self.pen.penColor("white") + self.pen.oval(300, 100, centered=True) + self.pen.penColor(color) + self.pen.write(msg, align='C') + print("... ended game") + + + def checkGrid(self) -> bool: + return self.shape.check_grid(score=self.score) + # check_result = check_grid(shape=self.shape, score=self.score) + # self.drawGrid() # should only redraw if any lines were cleared + # return check_result + + def resetBlock(self) -> bool: + return self.shape.reset_block() + #self.drawGrid() + #self.drawBlock() + + def moveBlockDown(self) -> bool: + return self.shape.move_block_down() + # if self.shape.move_block_down(): + # self.drawBlock() + # return True + # return False + + def moveBlockLeft(self) -> bool: + #print("$ move left") + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_left() + + def moveBlockRight(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_right() + + def rotateBlock(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.rotate_block() + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisTwoBlockApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/tetris/tetris_one_block.py b/dumbdisplay_examples/tetris/tetris_one_block.py new file mode 100644 index 0000000..caa866b --- /dev/null +++ b/dumbdisplay_examples/tetris/tetris_one_block.py @@ -0,0 +1,335 @@ +# *** +# *** Adapted from TETRIS ONE BLOCK\tetris_one_block.py of https://github.com/DimaGutierrez/Python-Games +# *** + +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd +from dumbdisplay_examples.tetris._common import Grid, _draw, _draw_grid, _width, _height, _colors, \ + _block_unit_width, _grid_n_rows, _grid_n_cols + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + +_RANDOMIZE_ROW_COUNT = 4 + + + +_delay = 0.3 # For time/sleep +_grid = [ # 12x24 + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0,0,0,0,0], + [4,3,3,1,0,1,0,3,3,4,0,1], + [5,1,6,5,3,2,1,0,5,5,0,3], + [1,2,3,4,5,2,0,4,2,3,0,4], + [1,3,1,2,4,2,0,3,1,2,4,3], + [1,1,2,3,0,2,2,2,0,3,0,1], + [0,1,1,2,3,0,0,0,4,0,2,0], + [2,0,1,2,3,0,6,5,5,5,0,2] +] + + +def _create_grid() -> Grid: + if _RANDOMIZE_ROW_COUNT >= 0: + grid = [] + for y in range(_grid_n_rows): + grid_row = [] + for x in range(_grid_n_cols): + if y >= (_grid_n_rows - _RANDOMIZE_ROW_COUNT) and random.random() < 0.7: + color = random.randint(1, len(_colors) - 1) + else: + color = 0 + grid_row.append(color) + grid.append(grid_row) + else: + grid = [] + for grid_row in _grid: + grid.append(grid_row.copy()) + return Grid(grid) + + +class OneBlock: + def __init__(self, x: int, y: int, block_pen: LayerTurtle): + self.x = x + self.y = y + self.color = random.randint(1, len(_colors) - 1) + self.block_pen = block_pen + self.sync_image() + + def commit(self, grid: Grid): + grid.set_value(self.y, self.x, self.color) + if True: + self.block_pen.clear() + + def move_down(self, grid: Grid) -> bool: + if self.y < 23 and grid.get_value(self.y + 1, self.x) == 0: + self.y += 1 + self.sync_image() + return True + return False + + def move_right(self, grid: Grid) -> bool: + if self.x < 11: + if grid.get_value(self.y, self.x + 1) == 0: + self.x += 1 + self.sync_image() + return True + return False + + def move_left(self, grid: Grid) -> bool: + if self.x > 0: + if grid.get_value(self.y, self.x - 1) == 0: + self.x -= 1 + self.sync_image() + return True + return False + + def sync_image(self): + self.block_pen.clear() + _draw(self.x, self.y, self.color, self.block_pen) + + + +def _check_grid(shape: 'Shape', score: LayerTurtle) -> (bool, int): + grid = shape.grid + block = shape.block + + # Check if each row is full: + empty_count = 23 + deleted_count = 0 + for y in range(0,24): + is_full = True + is_empty = True + y_erase = y + for x in range(0,12): + if grid.get_value(y, x) == 0: + is_full = False + else: + is_empty = False + if not is_empty and not is_full: + empty_count -= 1 + break + # Remove row and shift down + if is_full: + shape.score_count += 1 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for y in range(y_erase-1, -1, -1): + for x in range(0,12): + grid.set_value(y + 1, x, grid.get_value(y, x)) + + deleted_count += 1 + + return (empty_count == 23, deleted_count) + + +class Shape: + def __init__(self, pen: LayerTurtle, block_pen: LayerTurtle): + self.grid = _create_grid() + self.score_count = 0 + self.block: OneBlock = None + self.pen = pen + self.block_pen = block_pen + if not self.reset_block(): + raise Exception("Failed to create initial block") + + def check_grid(self, score: LayerTurtle) -> bool: + (all_empty, delete_count) = _check_grid(self, score) + if delete_count > 0: + self.sync_image() + return all_empty and self.block is None + + def reset_block(self) -> bool: + x = 5 + y = 0 + if self.grid.get_value(y, x) != 0: + #self.sync_image() + return False + self.block = OneBlock(x, y, self.block_pen) + self.sync_image() + return True + + def commit_block(self): + self.block.commit(self.grid) + self.sync_image() + self.block = None + + def move_block_down(self) -> bool: + return self.block.move_down(self.grid) + + def move_block_right(self) -> bool: + return self.block.move_right(self.grid) + + def move_block_left(self) -> bool: + return self.block.move_left(self.grid) + + def sync_image(self): + #self.block.sync_image() + _draw_grid(self.grid, self.pen) + + +class TetrisOneBlockApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + self.score: LayerTurtle = None + self.block_pen: LayerTurtle = None + self.pen: LayerTurtle = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + + root = DDRootLayer(self.dd, _width, _height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + block_pen = LayerTurtle(self.dd, _width, _height) + block_pen.penFilled() + #block_pen.setTextSize(32) + + pen = LayerTurtle(self.dd, _width, _height) + pen.penFilled() + pen.setTextSize(32) + + score = LayerTurtle(self.dd, _width, _height) + score.penColor('red') + score.penUp() + score.goTo(60, -300) + score.setTextFont("Courier", 24) + #score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, _width, _height) + if False: + border.rectangle(260, 490, centered=True) + border.penSize(10) + border.penUp() + border.goTo(-130, 240) + border.penDown() + border.penColor('linen') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + border.setTextFont("Courier", 36) + border.write("One-Block TETRIS", "C") + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveBlockLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveBlockRight()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, right_button)).pin(self.dd) + + self.score = score + self.block_pen = block_pen + self.pen = pen + + self.startGame() + #self.shape = Shape() + #self.resetBlock() + + self.last_update_time = time.time() + + def updateDD(self): + now = time.time() + need_update = (now - self.last_update_time) >= _delay + if need_update: + self.last_update_time = now + self.update() + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + moved_down = self.moveBlockDown() + if not moved_down: + self.shape.commit_block() + won = self.checkGrid() + if won: + self.endGame(won=True) + elif not moved_down: + if not self.resetBlock(): + self.endGame(won=False) + + def startGame(self): + self.score.clear() + self.score.write('Score: 0', 'C') + self.pen.clear() + self.block_pen.clear() + self.shape = Shape(pen=self.pen, block_pen=self.block_pen) + print("... started game") + + + def endGame(self, won: bool): + self.shape = None + if won: + msg = "🥳 YOU WON 🥳" + color = "purple" + else: + msg = "GAME OVER 😔" + color = "darkgray" + self.pen.home(with_pen=False) + self.pen.penColor("white") + self.pen.oval(300, 100, centered=True) + self.pen.penColor(color) + self.pen.write(msg, align='C') + print("... ended game") + + + def checkGrid(self) -> bool: + return self.shape.check_grid(score=self.score) + + def resetBlock(self) -> bool: + return self.shape.reset_block() + + def moveBlockDown(self) -> bool: + return self.shape.move_block_down() + + def moveBlockLeft(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_left() + + def moveBlockRight(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_right() + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisOneBlockApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/tetris/tetris_two_block.py b/dumbdisplay_examples/tetris/tetris_two_block.py new file mode 100644 index 0000000..b05a0f5 --- /dev/null +++ b/dumbdisplay_examples/tetris/tetris_two_block.py @@ -0,0 +1,307 @@ +# *** +# *** Adapted from TETRIS ONE BLOCK\tetris_one_block.py of https://github.com/DimaGutierrez/Python-Games +# *** + +import random +import time + +from dumbdisplay.core import * +from dumbdisplay.layer_graphical import DDRootLayer +from dumbdisplay.layer_turtle import LayerTurtle +from dumbdisplay.layer_lcd import LayerLcd +from dumbdisplay_examples.tetris._common import Grid, _colors, _grid_n_rows, _grid_n_cols, _block_unit_width, \ + _width, _height, _left, _top, _draw_grid, _commit_block_grid, Block, \ + _check_bad_block_grid_placement, _randomize_grid + +from dumbdisplay_examples.utils import DDAppBase, create_example_wifi_dd + +_RANDOMIZE_ROW_COUNT = 2 + + +_delay = 0.3 # For time/sleep + + +def _randomize_block_grid() -> Grid: + color = random.randint(1, len(_colors) - 1) + if True: + n_rows = random.randint(1, 2) + n_cols = random.randint(1, 2) + if False: # TODO: disable after debug + n_rows = 1 + n_cols = 2 + block_grid = [] + for y in range(n_rows): + block_grid_row = [] + for x in range(n_cols): + block_grid_row.append(color) + block_grid.append(block_grid_row) + else: + block_grid = [[color, color]] + return Grid(grid_cells=block_grid) + +# def _randomize_grid() -> Grid: +# grid_cells = [] +# for y in range(_grid_n_rows): +# grid_row = [] +# for x in range(_grid_n_cols): +# if y >= (_grid_n_rows - _RANDOMIZE_ROW_COUNT) and random.random() < 0.7: +# color = random.randint(1, len(_colors) - 1) +# else: +# color = 0 +# grid_row.append(color) +# grid_cells.append(grid_row) +# return Grid(grid_cells=grid_cells) + + +def _check_grid(shape: 'Shape', score: LayerTurtle) -> (bool, int): + grid = shape.grid + block = shape.block + + # Check if each row is full: + empty_count = 23 + deleted_count = 0 + for y in range(0,24): + is_full = True + is_empty = True + y_erase = y + for x in range(0,12): + if grid.get_value(y, x) == 0: + is_full = False + else: + is_empty = False + if not is_empty and not is_full: + empty_count -= 1 + break + # Remove row and shift down + if is_full: + shape.score_count += 1 + score.clear() + score.write(f'Score: {shape.score_count}', align='C') + + for y in range(y_erase-1, -1, -1): + for x in range(0,12): + grid.set_value(y + 1, x, grid.get_value(y, x)) + + deleted_count += 1 + + return (empty_count == 23, deleted_count) + + +class Shape: + def __init__(self, pen: LayerTurtle, block_pen: LayerTurtle): + self.grid = _randomize_grid(_RANDOMIZE_ROW_COUNT) + self.score_count = 0 + self.block: Block = None + self.pen = pen + self.block_pen = block_pen + if not self.reset_block(): + raise Exception("Failed to create initial block") + + def check_grid(self, score: LayerTurtle) -> bool: + (all_empty, delete_count) = _check_grid(self, score) + if delete_count > 0: + self.sync_image() + return all_empty and self.block is None + + def reset_block(self) -> bool: + x = 5 + y = 0 + block_grid = _randomize_block_grid() + x -= int((block_grid.n_cols - 1) / 2) + y += 1 - block_grid.n_rows + if _check_bad_block_grid_placement(block_grid, x, y, grid=self.grid, check_boundary=False): + return False + self.block = Block(x, y, block_grid=block_grid, block_pen=self.block_pen) + self.sync_image() + return True + + def commit_block(self): + _commit_block_grid(self.block.block_grid, self.block.x, self.block.y, self.grid) + #self.block.commit(self.grid) + self.sync_image() + self.block = None + + def move_block_down(self) -> bool: + return self.block.move_down(self.grid) + + def move_block_right(self) -> bool: + return self.block.move_right(self.grid) + + def move_block_left(self) -> bool: + return self.block.move_left(self.grid) + + def sync_image(self): + #self.block.sync_image() + _draw_grid(self.grid, self.pen) + + +class TetrisTwoBlockApp(DDAppBase): + def __init__(self, dd: DumbDisplay = create_example_wifi_dd()): + super().__init__(dd) + self.score: LayerTurtle = None + self.block_pen: LayerTurtle = None + self.pen: LayerTurtle = None + self.shape: Shape = None + self.last_update_time = None + + def initializeDD(self): + + root = DDRootLayer(self.dd, _width, _height) + root.border(5, "darkred", "round", 1) + root.backgroundColor("black") + + block_pen = LayerTurtle(self.dd, _width, _height) + block_pen.penFilled() + #block_pen.setTextSize(32) + + pen = LayerTurtle(self.dd, _width, _height) + pen.penFilled() + pen.setTextSize(32) + + score = LayerTurtle(self.dd, _width, _height) + score.penColor('red') + score.penUp() + score.goTo(60, -300) + score.setTextFont("Courier", 24) + #score.write('Score: 0', 'C') + + border = LayerTurtle(self.dd, _width, _height) + if False: + border.rectangle(260, 490, centered=True) + border.penSize(10) + border.penUp() + border.goTo(-130, 240) + border.penDown() + border.penColor('linen') + border.rightTurn(90) + border.forward(490) # Down + border.leftTurn(90) + border.forward(260) # Right + border.leftTurn(90) + border.forward(490) # Up + border.penUp() + border.goTo(0,260) + border.setTextFont("Courier", 36) + border.write("Two-Block TETRIS", "C") + + left_button = LayerLcd(self.dd, 2, 1, char_height=28) + left_button.noBackgroundColor() + left_button.writeLine("⬅️") + left_button.enableFeedback("f", lambda *args: self.moveBlockLeft()) + + right_button = LayerLcd(self.dd, 2, 1, char_height=28) + right_button.noBackgroundColor() + right_button.writeLine("➡️") + right_button.enableFeedback("f", lambda *args: self.moveBlockRight()) + + AutoPin('V', + AutoPin('S'), + AutoPin('H', left_button, right_button)).pin(self.dd) + + self.score = score + self.block_pen = block_pen + self.pen = pen + + self.startGame() + #self.shape = Shape() + #self.resetBlock() + + self.last_update_time = time.time() + + def updateDD(self): + now = time.time() + need_update = (now - self.last_update_time) >= _delay + if need_update: + self.last_update_time = now + self.update() + + def update(self): + if self.shape is None: + print("... waiting to restart ...") + return + + moved_down = self.moveBlockDown() + if not moved_down: + self.shape.commit_block() + won = self.checkGrid() + if won: + self.endGame(won=True) + elif not moved_down: + if not self.resetBlock(): + self.endGame(won=False) + # if self.shape.block.y > 0: + # #self.shape.reset_block() + # self.resetBlock() + # else: + # self.endGame(won=False) + + def startGame(self): + self.score.clear() + self.score.write('Score: 0', 'C') + self.pen.clear() + self.block_pen.clear() + self.shape = Shape(pen=self.pen, block_pen=self.block_pen) + print("... started game") + + + def endGame(self, won: bool): + #self.block_pen.clear() + self.shape = None + if won: + msg = "🥳 YOU WON 🥳" + color = "purple" + else: + msg = "GAME OVER 😔" + color = "darkgray" + self.pen.home(with_pen=False) + self.pen.penColor("white") + self.pen.oval(300, 100, centered=True) + self.pen.penColor(color) + self.pen.write(msg, align='C') + print("... ended game") + + + def checkGrid(self) -> bool: + return self.shape.check_grid(score=self.score) + # check_result = check_grid(shape=self.shape, score=self.score) + # self.drawGrid() # should only redraw if any lines were cleared + # return check_result + + def resetBlock(self) -> bool: + return self.shape.reset_block() + #self.drawGrid() + #self.drawBlock() + + def moveBlockDown(self) -> bool: + return self.shape.move_block_down() + # if self.shape.move_block_down(): + # self.drawBlock() + # return True + # return False + + def moveBlockLeft(self) -> bool: + #print("$ move left") + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_left() + # if self.shape.move_block_left(): + # self.drawBlock() + # return True + # return False + + def moveBlockRight(self) -> bool: + if self.shape is None: + self.startGame() + return False + return self.shape.move_block_right() + # if self.shape.move_block_right(): + # self.drawBlock() + # return True + # return False + + +if __name__ == "__main__": + from dumbdisplay_examples.utils import create_example_wifi_dd, DDAppBase + app = TetrisTwoBlockApp(create_example_wifi_dd()) + app.run() diff --git a/dumbdisplay_examples/utils.py b/dumbdisplay_examples/utils.py index 1d93896..5065553 100644 --- a/dumbdisplay_examples/utils.py +++ b/dumbdisplay_examples/utils.py @@ -2,9 +2,9 @@ def create_example_wifi_dd(): - ''' + """ create example DumbDisplay ... if for MicroPython WiFi connection, assumes _my_secret.py - ''' + """ if DumbDisplay.runningWithMicropython(): # connect using WIFI: # assume a _my_secret.py Python script containing @@ -18,3 +18,38 @@ def create_example_wifi_dd(): from dumbdisplay.io_inet import io4Inet dd = DumbDisplay(io4Inet()) return dd + + +class DDAppBase(): + def __init__(self, dd: DumbDisplay): + self.dd = dd + self.initialized = False + + def run(self): + self.setup() + while True: + self.loop() + + def setup(self): + pass + + def loop(self): + (connected, reconnecting) = self.dd.connectPassive() + if connected: + if not self.initialized: + self.initializeDD() + self.initialized = True + elif reconnecting: + self.dd.masterReset() + self.initialized = False + else: + self.updateDD() + + def initializeDD(self): + raise Exception("must implement initializeDD") + + def updateDD(self): + pass + + + diff --git a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app.py b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app.py index e3e430c..6e80006 100644 --- a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app.py +++ b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app.py @@ -14,9 +14,9 @@ class SlidingPuzzleApp: def __init__(self, dd: DumbDisplay, tile_count: int = DEF_TILE_COUNT, suggest_move_from_dir_func = None): - ''' + """ :param suggest_move_from_dir_func: if not None, a function that access BoardManager and returns the next move (0 / 1 / 2 / 3) - ''' + """ self.tile_count = tile_count self.tile_size = BOARD_SIZE / tile_count self.suggest_move_from_dir_func = suggest_move_from_dir_func @@ -392,9 +392,9 @@ def posToHoleTileFromIdxes(self, x: int, y: int) -> (int, int, int, bool): def showHideHoleTile(self, show: bool): - ''' + """ show / hide the hole tile, which might not be in position - ''' + """ hole_tile_id = self.board_manager.board_tiles[self.board_manager.hole_tile_row_idx * self.tile_count + self.board_manager.hole_tile_col_idx] hole_tile_level_id = str(hole_tile_id) anchor_x = self.board_manager.hole_tile_col_idx * self.tile_size diff --git a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_OLD.py b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_OLD.py index f98e9c3..e4bd3fc 100644 --- a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_OLD.py +++ b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_OLD.py @@ -432,9 +432,9 @@ def posToHoleTileFromIdxes(self, x: int, y: int) -> (int, int, int, bool): def showHideHoleTile(self, show: bool): - ''' + """ show / hide the hole tile, which might not be in position - ''' + """ #holeTileId = self.boardTileIds[self.holeTileRowIdx][self.holeTileColIdx] hole_tile_id = self.board_tiles[self.hole_tile_row_idx * self.tile_count + self.hole_tile_col_idx] hole_tile_level_id = str(hole_tile_id) diff --git a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_SIMPLE.py b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_SIMPLE.py index da7210c..3a5f1cf 100644 --- a/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_SIMPLE.py +++ b/experiments/OLD/OLD_examples/sliding_puzzle/sliding_puzzle_app_SIMPLE.py @@ -375,9 +375,9 @@ def posToHoleTileFromIdxes(self, x: int, y: int) -> (int, int, int, bool): def showHideHoleTile(self, show: bool): - ''' + """ show / hide the hole tile, which might not be in position - ''' + """ holeTileId = self.boardTileIds[self.holeTileRowIdx][self.holeTileColIdx] holeTileLevelId = str(holeTileId) anchorX = self.holeTileColIdx * TILE_SIZE diff --git a/experiments/OLD/testing/le.py b/experiments/OLD/testing/le.py index 3e52a9a..7a6a8f1 100644 --- a/experiments/OLD/testing/le.py +++ b/experiments/OLD/testing/le.py @@ -43,13 +43,13 @@ def _ble_irq(self, event, data): print("E:" + str(event)) if event == 1: - '''Central disconnected''' + """Central disconnected""" self.conn_handle, _, _, = data self._connected() #self.led(1) elif event == 2: - '''Central disconnected''' + """Central disconnected""" conn_handle, _, _, = data if conn_handle == self._conn_handle: self.conn_handle = None @@ -57,7 +57,7 @@ def _ble_irq(self, event, data): self._advertiser() elif event == 3:#4: - '''New message received''' + """New message received""" #conn_handle, value_handle, = data #print("...") diff --git a/experiments/picopio/pio_neo_blink.py b/experiments/picopio/pio_neo_blink.py index 301bf77..22cdc34 100644 --- a/experiments/picopio/pio_neo_blink.py +++ b/experiments/picopio/pio_neo_blink.py @@ -49,9 +49,9 @@ def neo_prog(): def ShowNeoPixels(*pixels): - ''' + """ each pixel is the tuple (r, g, b) - ''' + """ pixel_count = len(pixels) sm.put(pixel_count - 1) for i in range(pixel_count): diff --git a/experiments/testing/le.py b/experiments/testing/le.py index 3e52a9a..7a6a8f1 100644 --- a/experiments/testing/le.py +++ b/experiments/testing/le.py @@ -43,13 +43,13 @@ def _ble_irq(self, event, data): print("E:" + str(event)) if event == 1: - '''Central disconnected''' + """Central disconnected""" self.conn_handle, _, _, = data self._connected() #self.led(1) elif event == 2: - '''Central disconnected''' + """Central disconnected""" conn_handle, _, _, = data if conn_handle == self._conn_handle: self.conn_handle = None @@ -57,7 +57,7 @@ def _ble_irq(self, event, data): self._advertiser() elif event == 3:#4: - '''New message received''' + """New message received""" #conn_handle, value_handle, = data #print("...") diff --git a/samples/neopixels/main.py b/samples/neopixels/main.py index 64c8c66..b33521f 100644 --- a/samples/neopixels/main.py +++ b/samples/neopixels/main.py @@ -1,5 +1,7 @@ NUM_PIXELS = 4 NEO_PIXELS_IN_PIN = 22 +# NUM_PIXELS = 64 +# NEO_PIXELS_IN_PIN = 20 try: @@ -49,9 +51,9 @@ def neo_prog(): sm.active(1) def ShowNeoPixels(*pixels): - ''' + """ each pixel is the tuple (r, g, b) - ''' + """ pixel_count = len(pixels) sm.put(pixel_count - 1) for i in range(pixel_count): @@ -64,11 +66,12 @@ def ShowNeoPixels(*pixels): sm.put(grb, 8) # a word is 32 bits, so, pre-shift out (discard) 8 bits, leaving 24 bits of the GRB time.sleep_us(300) # make sure the NeoPixels is reset for the next round - print("PIO ready!") + print("!!! PIO ready!") -except: +except Exception as e: + print(f"xxx failed to import rp2 -- {e}") + print("!!! PIO not supported!") ShowNeoPixels = None - print("PIO not supported!") if ShowNeoPixels is None: diff --git a/screenshots/layer_turtle.png b/screenshots/layer_turtle.png new file mode 100644 index 0000000..188a553 Binary files /dev/null and b/screenshots/layer_turtle.png differ diff --git a/setup.py b/setup.py index aad2fb4..ac794de 100644 --- a/setup.py +++ b/setup.py @@ -2,23 +2,14 @@ from setuptools.config.expand import find_packages -# read the contents of your README file -from pathlib import Path -this_directory = Path(__file__).parent -long_description = (this_directory / "README.md").read_text() - setuptools.setup( name='uDumbDisplayLib', - version='0.5.0', + version='0.6.0', author='Trevor Lee', author_email='trevorwslee@gmail.com', description='MicroPython DumbDisplay Library', - long_description=long_description, long_description_content_type="text/markdown", url='https://github.com/trevorwslee/MicroPython-DumbDisplay', - project_urls = { - }, license='MIT', packages=find_packages(include=["dumbdisplay*"]), - install_requires=[], )