From 5045c36435b102a95d9956f34a8b462026ae38c4 Mon Sep 17 00:00:00 2001 From: wojciech-graj <71249593+wojciech-graj@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:16:11 +0200 Subject: [PATCH] Add type hints, Clean up repository (#5) Changes: - Add type hints in `.pyi` file - Make workflow run on MacOS, Windows, and Linux, for Python 3.9 and 3.13 - Specify demo dependencies in `pyproject.toml` - Move demos to `demo` directory - Remove `init` function - Add correct compilation flags for MacOS --- .github/workflows/python-package.yml | 9 +- .gitignore | 13 +- README.md | 57 ++++---- cydoomgeneric/__init__.pyi | 119 +++++++++++++++++ cydoomgeneric/cydoomgeneric.pxd | 1 - cydoomgeneric/cydoomgeneric.pyx | 37 +----- cydoomgeneric/py.typed | 0 {cydoomgeneric => demo}/democalc.py | 55 ++++---- {cydoomgeneric => demo}/demominepi.py | 177 +++++++++++++------------ {cydoomgeneric => demo}/demomspaint.py | 135 +++++++++++-------- {cydoomgeneric => demo}/demopygame.py | 39 +++--- {cydoomgeneric => demo}/demopyplot.py | 21 ++- doomgeneric/doomgeneric.c | 7 +- doomgeneric/doomgeneric.h | 4 +- pyproject.toml | 36 ++++- setup.py | 65 ++++----- 16 files changed, 435 insertions(+), 340 deletions(-) create mode 100644 cydoomgeneric/__init__.pyi create mode 100644 cydoomgeneric/py.typed rename {cydoomgeneric => demo}/democalc.py (69%) rename {cydoomgeneric => demo}/demominepi.py (56%) rename {cydoomgeneric => demo}/demomspaint.py (64%) rename {cydoomgeneric => demo}/demopygame.py (75%) rename {cydoomgeneric => demo}/demopyplot.py (85%) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 29fcc1e..81da2e7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,7 +1,4 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Python package +name: cydoomgeneric on: push: @@ -15,8 +12,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] - os: [ubuntu-latest, windows-latest] + python-version: ["3.9", "3.13"] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 2db2171..fa07992 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ +*.DS_Store +*.WAD +*.egg-info/ *.o *.obj -*.DS_Store *.so -build/ -.savegame/ -*.WAD *.so.map -./cydoomgeneric/cydoomgeneric.c +.ropeproject/ +.savegame/ +__pycache__/ +build/ +cydoomgeneric/cydoomgeneric.c diff --git a/README.md b/README.md index 5172432..27ef949 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ cyDoomGeneric should run on Linux, MacOS, and Windows. You must implement the `draw_frame` and `get_key` functions. -``` +```python import cydoomgeneric as cdg resx = 640 @@ -20,10 +20,9 @@ resy = 400 # Required functions def draw_frame(pixels: np.ndarray) -> None: -def get_key() -> Optional[Tuple[int, int]]: +def get_key() -> Optional[tuple[int, int]]: # Optional functions -def init() -> None: def sleep_ms(ms: int) -> None: def set_window_title(t: str) -> None: def get_ticks_ms() -> int: @@ -32,7 +31,6 @@ cdg.init(resx, resy, draw_frame, get_key, - init=init, sleep_ms=sleep_ms, get_ticks_ms=get_ticks_ms, set_window_title=set_window_title) @@ -49,8 +47,8 @@ Some additional documentation can be found in `cydoomgeneric/cydoomgeneric.pyx`. To build and install cydoomgeneric, run the following command: -``` -$ pip install . +```sh +pip install . ``` ## Demo Screenshots @@ -76,18 +74,14 @@ $ pip install . #### Pyplot -Ensure that the `matplotlib` python package is installed. - -``` -$ cd cydoomgeneric -$ python demopyplot.py +```sh +pip install '.[pyplot]' +python demo/demopyplot.py ``` #### Minecraft: Pi Edition -Ensure that the `mcpi scikit-image` packages are installed. - -Before running the script, launch Minecraft: Pi Edition and join a world. The `SCALE` variable in `demominepi.py` can be adjusted to change the display size. +Before running the script, launch Minecraft: Pi Edition and join a world. The `SCALE` variable in `demo/demominepi.py` can be adjusted to change the display size. To move, step on the appropriate block on the platform that the player is standing on. To press the fire, use, enter, or escape keys, hit (`RMB`) the appropriate block with the sword: ``` @@ -97,49 +91,46 @@ NETHER_REACTOR_CORE: ENTER NETHER_REACTOR_CORE(active): ESCAPE ``` -``` -$ cd cydoomgeneric -$ python demominepi.py +```sh +pip install '.[minepi]' +python demo/demominepi.py ``` #### MS Paint -Ensure that the `pyautogui pywinctl scikit-image` packages are installed, and that the Windows XP version of mspaint is installed, which can be done by running `winetricks mspaint`. +Ensure that the Windows XP version of mspaint is installed, which can be done by running `winetricks mspaint`. -If you have not installed mspaint using wine, you'll have to edit the `PAINT_COMMAND` variable in `demomspaint.py` to contain the command for launching paint. +If you have not installed mspaint using wine, you'll have to edit the `PAINT_COMMAND` variable in `demo/demomspaint.py` to contain the command for launching paint. If you wish to free your mouse in the middle of a frame being drawn, you should drag it to the top-left corner of the screen, which will free it, at which point you can kill the python script. Once a frame has been drawn, you will be able to send an input by flood-filling the appropriate "key" drawn under the frame. -``` -$ cd cydoomgeneric -$ python demomspaint.py +```sh +pip install '.[mspaint]' +python demo/demomspaint.py ``` #### LibreOffice Calc Ensure that the libreoffice SDK (`libreoffice-dev` on Debian) is installed, and that you're using the system python installation instead of a virtual environment. -The `SCALE` variable in `democalc.py` can be adjusted in the range `[0,5]` to change the display size, idealy either 1 or 2. Lower scales will exponentially increase the setup time required prior to starting the game. Expect to wait a few minutes. +The `SCALE` variable in `demo/democalc.py` can be adjusted in the range `[0,5]` to change the display size, idealy either 1 or 2. Lower scales will exponentially increase the setup time required prior to starting the game. Expect to wait a few minutes. Sometimes the window will be tiny, so maximize it if neccessary. Also, you may experience unexpected issues while attempting to run this demo, and there's not much I can do because the UNO API has virtually no documentation and the code here has been pieced together from 10 year old forum posts for the Java or C++ version of the API. Only run the following command once, unless the libreoffice process is killed: -``` -$ libreoffice --nofirststartwizard --nologo --norestore --accept='socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext' & +```sh +libreoffice --nofirststartwizard --nologo --norestore --accept='socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext' & ``` -``` -$ cd cydoomgeneric -$ python democalc.py +```sh +python demo/democalc.py ``` #### Pygame -Ensure that the `pygame` python package is installed. - -``` -$ cd cydoomgeneric -$ python demopygame.py +```sh +pip install '.[pygame]' +python demo/demopygame.py ``` ## License diff --git a/cydoomgeneric/__init__.pyi b/cydoomgeneric/__init__.pyi new file mode 100644 index 0000000..b54b259 --- /dev/null +++ b/cydoomgeneric/__init__.pyi @@ -0,0 +1,119 @@ +""" + Copyright(C) 2024 Wojciech Graj + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" + +from enum import IntEnum +from typing import Callable, Optional, Sequence + +import numpy as np + +def init(resx: int, + resy: int, + draw_frame: Callable[[np.ndarray], None], + get_key: Callable[[], Optional[tuple[int, int]]], + sleep_ms: Optional[Callable[[int], None]] = None, + get_ticks_ms: Optional[Callable[[], int]] = None, + set_window_title: Optional[Callable[[str], None]] = None) -> None: + """ + Initializes the doom context. + + :param resx: + :param resy: + :param draw_frame: Called every frame. Takes framebuffer as np.ndarray in + shape [resy, resx, 4]. Pixels are BGR. + :param get_key: Called multiple times every frame until input is exhausted. + Return None when input is exhausted. Otherwise, return + (is pressed ~0/1~, key). + :param sleep_ms: + :param get_ticks_ms: + :param set_window_title: + """ + + +def main(argv: Optional[Sequence[str]] = None) -> int: + """ + main(argv) -> int + + Run doom. Must be called after init. + + :param Optional[Sequence[str]] argv: + """ + + +class Keys(IntEnum): + RIGHTARROW: int + LEFTARROW: int + UPARROW: int + DOWNARROW: int + STRAFE_L: int + STRAFE_R: int + USE: int + FIRE: int + ESCAPE: int + ENTER: int + TAB: int + F1: int + F2: int + F3: int + F4: int + F5: int + F6: int + F7: int + F8: int + F9: int + F10: int + F11: int + F12: int + + BACKSPACE: int + PAUSE: int + + EQUALS: int + MINUS: int + + RSHIFT: int + RCTRL: int + RALT: int + + LALT: int + + CAPSLOCK: int + NUMLOCK: int + SCRLCK: int + PRTSCR: int + + HOME: int + END: int + PGUP: int + PGDN: int + INS: int + DEL: int + + P_0: int + P_1: int + P_2: int + P_3: int + P_4: int + P_5: int + P_6: int + P_7: int + P_8: int + P_9: int + + P_DIVIDE: int + P_PLUS: int + P_MINUS: int + P_MULTIPLY: int + P_PERIOD: int + P_EQUALS: int + P_ENTER: int diff --git a/cydoomgeneric/cydoomgeneric.pxd b/cydoomgeneric/cydoomgeneric.pxd index aef53bd..8387d5b 100644 --- a/cydoomgeneric/cydoomgeneric.pxd +++ b/cydoomgeneric/cydoomgeneric.pxd @@ -23,7 +23,6 @@ cdef extern from "doomgeneric.h": void dg_Create(uint32_t resx, uint32_t resy, - void (*pDG_Init)() except *, void (*pDG_DrawFrame)() noexcept, void (*pDG_SleepMs)(uint32_t) noexcept, uint32_t (*pDG_GetTicksMs)() noexcept, diff --git a/cydoomgeneric/cydoomgeneric.pyx b/cydoomgeneric/cydoomgeneric.pyx index 92c5f16..575e271 100644 --- a/cydoomgeneric/cydoomgeneric.pyx +++ b/cydoomgeneric/cydoomgeneric.pyx @@ -16,7 +16,7 @@ from enum import IntEnum import time import sys -from typing import Callable, Optional, Tuple, Sequence +from typing import Callable, Optional, Sequence from cpython.mem cimport PyMem_Malloc, PyMem_Free @@ -25,20 +25,14 @@ import numpy as np cimport numpy as np -__init_f: Optional[Callable[None, None]] __draw_frame_f: Callable[[np.ndarray], None] __sleep_ms_f: Optional[Callable[[int], None]] __get_ticks_ms_f: Optional[Callable[None, int]] -__get_key_f: Callable[None, Optional[Tuple[int, int]]] +__get_key_f: Callable[None, Optional[tuple[int, int]]] __set_window_title_f: Optional[Callable[[str], None]] __start_time: int -cdef void __init() except *: - if __init_f: - __init_f() - - cdef void __draw_frame() noexcept: try: __draw_frame_f(np.asarray(&cdg.DG_ScreenBuffer[0])) @@ -89,26 +83,10 @@ def init(resx: int, resy: int, draw_frame: Callable[[np.ndarray], None], get_key: Callable[[int], str], - init: Optional[Callable[None, None]]=None, sleep_ms: Optional[Callable[[int], None]]=None, - get_ticks_ms: Optional[Callable[None, int]]=None, + get_ticks_ms: Optional[Callable[[], int]]=None, set_window_title: Optional[Callable[[str], None]]=None ) -> None: - """ - init(resx, resx, init, draw_frame, sleep_ms, get_ticks_ms, get_key, set_window_title) -> None - - Initializes the doom context. - - :param int resx: - :param int resy: - :param Callable[[np.ndarray], None] draw_frame: Called every frame. Takes framebuffer as np.ndarray in shape [resy, resx, 4]. Pixels are BGR. - :param Callable[[int], Optional[Tuple[int, int]]] get_key: Called multiple times every frame until input is exhausted. Return None when input is exhausted. Otherwise, return (is pressed ~0/1~, key). - :param Optional[Callable[None, None]] init: Initialization function called immediately after this function terminates - :param Optional[Callable[[int], None]] sleep_ms: - :param Optional[Callable[None, int]] get_ticks_ms: - :param Optional[Callable[[str], None]] set_window_title: - """ - global __init_f global __draw_frame_f global __sleep_ms_f global __get_ticks_ms_f @@ -116,7 +94,6 @@ def init(resx: int, global __set_window_title_f global __start_time - __init_f = init __draw_frame_f = draw_frame __sleep_ms_f = sleep_ms __get_ticks_ms_f = get_ticks_ms @@ -126,7 +103,6 @@ def init(resx: int, cdg.dg_Create(resx, resy, - &__init, &__draw_frame, &__sleep_ms, &__get_ticks_ms, @@ -135,13 +111,6 @@ def init(resx: int, def main(argv: Optional[Sequence[str]]=None) -> int: - """ - main(argv) -> int - - Run doom. Must be called after init. - - :param Optional[Sequence[str]] argv: - """ if argv is None: return cdg.dg_main(0, NULL) diff --git a/cydoomgeneric/py.typed b/cydoomgeneric/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/cydoomgeneric/democalc.py b/demo/democalc.py similarity index 69% rename from cydoomgeneric/democalc.py rename to demo/democalc.py index 29fc351..811c026 100644 --- a/cydoomgeneric/democalc.py +++ b/demo/democalc.py @@ -13,18 +13,18 @@ """ import itertools -from typing import Optional, Tuple, List +from typing import Optional import numpy as np import uno import cydoomgeneric as cdg - -# Change this variable to adjust screen resolution. Allowed values in range [0,5] +""" +Change this variable to adjust screen resolution. Allowed values in range [0,5] +""" SCALE = 1 - KEYMAP = { 'a': cdg.Keys.LEFTARROW, 'd': cdg.Keys.RIGHTARROW, @@ -39,38 +39,36 @@ '`': cdg.Keys.ESCAPE, } - -GRAD = " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$" +GRAD = ( + " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$") class CalcDoom: + def __init__(self, sheet, scale) -> None: self._scale = scale - self._resx = 320 // (2 ** scale) - self._resy = 200 // (2 ** scale) + self._resx = 320 // (2**scale) + self._resy = 200 // (2**scale) self._image_cell = sheet.getCellByPosition(1, 0) self._input_cell = sheet.getCellByPosition(0, 0) - self._input: Optional[List[str]] = None - self._pressed: List[int] = [] - self._pressed_prev: List[int] = [] - + self._input: Optional[list[str]] = None + self._pressed: list[int] = [] + self._pressed_prev: list[int] = [] - def init(self) -> None: for y in range(1, self._resy + 1): print(f"Initializing row {y}/{self._resy}") for x in range(1, self._resx + 1): cell = sheet.getCellByPosition(x, y) cell.setFormula(f"=MID(B1;{x + (y - 1) * self._resx};1)") - def draw_frame(self, pix) -> None: pix = np.average(pix, axis=-1) - self._image_cell.setString(''.join( - [GRAD[int(pix[y, x] * 0.35)] for y, x in - itertools.product(range(0, 200, 2 ** self._scale), range(0, 320, 2 ** self._scale))])) - + self._image_cell.setString(''.join([ + GRAD[int(pix[y, x] * 0.35)] for y, x in itertools.product( + range(0, 200, 2**self._scale), range(0, 320, 2**self._scale)) + ])) - def get_key(self) -> Optional[Tuple[int, int]]: + def get_key(self) -> Optional[tuple[int, int]]: if len(self._pressed) > 0: return (0, self._pressed.pop()) if self._input is None: @@ -90,20 +88,21 @@ def get_key(self) -> Optional[Tuple[int, int]]: if __name__ == "__main__": local_ctx = uno.getComponentContext() smgr_local = local_ctx.ServiceManager - resolver = smgr_local.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local_ctx) + resolver = smgr_local.createInstanceWithContext( + "com.sun.star.bridge.UnoUrlResolver", local_ctx) uno_ctx = resolver.resolve( - "uno:socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext") + "uno:socket,host=localhost,port=2002,tcpNoDelay=1" + ";urp" + ";StarOffice.ComponentContext") uno_smgr = uno_ctx.ServiceManager - desktop = uno_smgr.createInstanceWithContext("com.sun.star.frame.Desktop", uno_ctx) + desktop = uno_smgr.createInstanceWithContext("com.sun.star.frame.Desktop", + uno_ctx) PropertyValue = uno.getClass('com.sun.star.beans.PropertyValue') - document = desktop.loadComponentFromURL("private:factory/scalc", "_blank", 0, (PropertyValue(),)) + document = desktop.loadComponentFromURL("private:factory/scalc", "_blank", + 0, (PropertyValue(), )) sheet = document.getSheets()[0] g = CalcDoom(sheet, SCALE) - cdg.init(320, - 200, - g.draw_frame, - g.get_key, - init=g.init) + cdg.init(320, 200, g.draw_frame, g.get_key) cdg.main() diff --git a/cydoomgeneric/demominepi.py b/demo/demominepi.py similarity index 56% rename from cydoomgeneric/demominepi.py rename to demo/demominepi.py index 1bb91e2..7eb5970 100644 --- a/cydoomgeneric/demominepi.py +++ b/demo/demominepi.py @@ -13,11 +13,11 @@ """ import itertools -from typing import Optional, Tuple, Set, List +from typing import Optional, Set -from mcpi import minecraft, block, event -from mcpi.vec3 import Vec3 import numpy as np +from mcpi import block, event, minecraft +from mcpi.vec3 import Vec3 from skimage import color import cydoomgeneric as cdg @@ -75,60 +75,60 @@ block.ICE, ) - -PALETTE_LAB = color.rgb2lab(np.array(( - (.492, .492, .492), - (.526, .378, .263), - (.482, .482, .482), - (.615, .500, .309), - (.328, .328, .328), - (.859, .828, .628), - (.516, .485, .483), - (.401, .318, .195), - (.179, .113, .048), - (.810, .809, .789), - (.114, .279, .650), - (.851, .822, .617), - (.846, .816, .606), - (.862, .831, .634), - (.870, .870, .870), - (.918, .500, .214), - (.748, .296, .788), - (.407, .545, .831), - (.761, .709, .110), - (.232, .739, .187), - (.852, .516, .607), - (.261, .261, .261), - (.620, .649, .649), - (.154, .458, .586), - (.507, .211, .768), - (.152, .201, .604), - (.336, .201, .108), - (.218, .302, .095), - (.642, .176, .159), - (.105, .091, .091), - (.977, .927, .308), - (.860, .860, .860), - (.655, .655, .655), - (.625, .625, .625), - (.575, .392, .341), - (.407, .476, .407), - (.079, .072, .117), - (.383, .860, .837), - (.941, .985, .985), - (.622, .645, .693), - (.479, .479, .479), - (.449, .467, .416), - (.466, .466, .466), - (.554, .572, .141), - (.436, .213, .206), - (.075, .075, .137), - (.175, .088, .104), - (.926, .914, .887), - (.910, .896, .863), - (.682, .799, 1.00), -), dtype=float)) - +PALETTE_LAB = color.rgb2lab( + np.array(( + (.492, .492, .492), + (.526, .378, .263), + (.482, .482, .482), + (.615, .500, .309), + (.328, .328, .328), + (.859, .828, .628), + (.516, .485, .483), + (.401, .318, .195), + (.179, .113, .048), + (.810, .809, .789), + (.114, .279, .650), + (.851, .822, .617), + (.846, .816, .606), + (.862, .831, .634), + (.870, .870, .870), + (.918, .500, .214), + (.748, .296, .788), + (.407, .545, .831), + (.761, .709, .110), + (.232, .739, .187), + (.852, .516, .607), + (.261, .261, .261), + (.620, .649, .649), + (.154, .458, .586), + (.507, .211, .768), + (.152, .201, .604), + (.336, .201, .108), + (.218, .302, .095), + (.642, .176, .159), + (.105, .091, .091), + (.977, .927, .308), + (.860, .860, .860), + (.655, .655, .655), + (.625, .625, .625), + (.575, .392, .341), + (.407, .476, .407), + (.079, .072, .117), + (.383, .860, .837), + (.941, .985, .985), + (.622, .645, .693), + (.479, .479, .479), + (.449, .467, .416), + (.466, .466, .466), + (.554, .572, .141), + (.436, .213, .206), + (.075, .075, .137), + (.175, .088, .104), + (.926, .914, .887), + (.910, .896, .863), + (.682, .799, 1.00), + ), + dtype=float)) PLATFORM = ( (-1, 0, -2, block.DIAMOND_BLOCK), @@ -145,7 +145,6 @@ (1, 0, 1, block.GLASS), ) - KEYPOS = { (-1, 0, -2): cdg.Keys.FIRE, (1, 0, -2): cdg.Keys.USE, @@ -155,37 +154,44 @@ class MinecraftPiDoom: + def __init__(self) -> None: self._mc = minecraft.Minecraft.create() self._scale = 5 self._pressed: Set[int] = set() self._read_frame_input = False - self._inputs: List[Tuple[int, int]] = [] + self._inputs: list[tuple[int, int]] = [] - self._ctrls_pos = Vec3(160 // self._scale, 100 // self._scale - 2, 68 - 6 * self._scale) + self._ctrls_pos = Vec3(160 // self._scale, 100 // self._scale - 2, + 68 - 6 * self._scale) self._mc.setBlocks(0, 0, 0, 256, 128, 128, block.AIR) - self._mc.setBlocks( - self._ctrls_pos.x - 2, self._ctrls_pos.y + 1, self._ctrls_pos.z + 2, - self._ctrls_pos.x + 2, self._ctrls_pos.y + 2, self._ctrls_pos.z - 3, - block.BEDROCK_INVISIBLE) - self._mc.setBlocks( - self._ctrls_pos.x - 1, self._ctrls_pos.y + 1, self._ctrls_pos.z + 1, - self._ctrls_pos.x + 1, self._ctrls_pos.y + 2, self._ctrls_pos.z - 2, - block.AIR) + self._mc.setBlocks(self._ctrls_pos.x - 2, self._ctrls_pos.y + 1, + self._ctrls_pos.z + 2, self._ctrls_pos.x + 2, + self._ctrls_pos.y + 2, self._ctrls_pos.z - 3, + block.BEDROCK_INVISIBLE) + self._mc.setBlocks(self._ctrls_pos.x - 1, self._ctrls_pos.y + 1, + self._ctrls_pos.z + 1, self._ctrls_pos.x + 1, + self._ctrls_pos.y + 2, self._ctrls_pos.z - 2, + block.AIR) for blk in PLATFORM: - self._mc.setBlock( - self._ctrls_pos.x + blk[0], self._ctrls_pos.y + blk[1], self._ctrls_pos.z + blk[2], - blk[3]) + self._mc.setBlock(self._ctrls_pos.x + blk[0], + self._ctrls_pos.y + blk[1], + self._ctrls_pos.z + blk[2], blk[3]) self._mc.player.setTilePos(self._ctrls_pos + Vec3(0, 1, 0)) def draw_frame(self, pix: np.ndarray) -> None: - for y, x in itertools.product(range(0, 200, self._scale), range(0, 320, self._scale)): - idx = np.argmin(color.deltaE_cie76(PALETTE_LAB, color.rgb2lab(pix[199 - y, x, [2, 1, 0]] / 255))) - self._mc.setBlock(x // self._scale, y // self._scale, 0, PALETTE_BLOCKS[idx]) + for y, x in itertools.product(range(0, 200, self._scale), + range(0, 320, self._scale)): + idx = np.argmin( + color.deltaE_cie76( + PALETTE_LAB, + color.rgb2lab(pix[199 - y, x, [2, 1, 0]] / 255))) + self._mc.setBlock(x // self._scale, y // self._scale, 0, + PALETTE_BLOCKS[idx]) self._mc.getBlockWithData(0, 0, 0) # Wait for entire frame to be drawn self._read_frame_input = False - def get_key(self) -> Optional[Tuple[int, int]]: + def get_key(self) -> Optional[tuple[int, int]]: if not self._read_frame_input: self._read_frame_input = True @@ -193,31 +199,30 @@ def get_key(self) -> Optional[Tuple[int, int]]: cur_pressed = set() if dpos.y == 1: if dpos.x == -1: - cur_pressed.add(cdg.Keys.LEFTARROW) + cur_pressed.add(int(cdg.Keys.LEFTARROW)) elif dpos.x == 1: - cur_pressed.add(cdg.Keys.RIGHTARROW) + cur_pressed.add(int(cdg.Keys.RIGHTARROW)) if dpos.z == -1: - cur_pressed.add(cdg.Keys.UPARROW) + cur_pressed.add(int(cdg.Keys.UPARROW)) elif dpos.z == 1: - cur_pressed.add(cdg.Keys.DOWNARROW) + cur_pressed.add(int(cdg.Keys.DOWNARROW)) for e in self._mc.events.pollBlockHits(): if e.type != event.BlockEvent.HIT: continue - dpos = (*(e.pos - self._ctrls_pos),) + dpos = (*(e.pos - self._ctrls_pos), ) if dpos in KEYPOS: cur_pressed.add(KEYPOS[dpos]) - self._inputs = ([(0, key) for key in iter(self._pressed - cur_pressed)] - + [(1, key) for key in iter(cur_pressed - self._pressed)]) + self._inputs = ([(0, key) + for key in iter(self._pressed - cur_pressed)] + + [(1, key) + for key in iter(cur_pressed - self._pressed)]) self._pressed = cur_pressed return self._inputs.pop() if self._inputs else None if __name__ == "__main__": g = MinecraftPiDoom() - cdg.init(320, - 200, - g.draw_frame, - g.get_key) + cdg.init(320, 200, g.draw_frame, g.get_key) cdg.main() diff --git a/cydoomgeneric/demomspaint.py b/demo/demomspaint.py similarity index 64% rename from cydoomgeneric/demomspaint.py rename to demo/demomspaint.py index 821cc80..036c82c 100644 --- a/cydoomgeneric/demomspaint.py +++ b/demo/demomspaint.py @@ -14,50 +14,49 @@ import subprocess import time -from typing import Optional, Tuple +from typing import Optional +import numpy as np import pyautogui import pywinctl as pwc -import numpy as np -from skimage import color, measure, filters, segmentation, morphology +from skimage import color, filters, measure, morphology, segmentation import cydoomgeneric as cdg - PAINT_COMMAND = ["wine", "mspaint"] - -PALETTE_LAB = color.rgb2lab(np.array(( - (0, 0, 0), - (128, 128, 128), - (128, 0, 0), - (128, 128, 0), - (0, 128, 0), - (0, 128, 128), - (0, 0, 128), - (128, 0, 128), - (128, 128, 64), - (0, 64, 64), - (0, 128, 256), - (0, 64, 128), - (64, 0, 255), - (128, 64, 0), - (255, 255, 255), - (192, 192, 192), - (255, 0, 0), - (255, 255, 0), - (0, 255, 0), - (0, 255, 255), - (0, 0, 255), - (255, 0, 255), - (255, 255, 128), - (0, 255, 128), - (128, 255, 255), - (128, 128, 255), - (255, 0, 128), - (255, 128, 64), -), dtype=float) / 255.) - +PALETTE_LAB = color.rgb2lab( + np.array(( + (0, 0, 0), + (128, 128, 128), + (128, 0, 0), + (128, 128, 0), + (0, 128, 0), + (0, 128, 128), + (0, 0, 128), + (128, 0, 128), + (128, 128, 64), + (0, 64, 64), + (0, 128, 256), + (0, 64, 128), + (64, 0, 255), + (128, 64, 0), + (255, 255, 255), + (192, 192, 192), + (255, 0, 0), + (255, 255, 0), + (0, 255, 0), + (0, 255, 255), + (0, 0, 255), + (255, 0, 255), + (255, 255, 128), + (0, 255, 128), + (128, 255, 255), + (128, 128, 255), + (255, 0, 128), + (255, 128, 64), + ), + dtype=float) / 255.) KEYMAP = ( (cdg.Keys.LEFTARROW, '{'), @@ -73,16 +72,18 @@ class MsPaintDoom: + def __init__(self) -> None: self._paint_process = subprocess.Popen(PAINT_COMMAND, shell=False) - while (not (windows := pwc.getWindowsWithTitle("Paint", condition=pwc.Re.CONTAINS))): + while (not (windows := pwc.getWindowsWithTitle( + "Paint", condition=pwc.Re.CONTAINS))): pass time.sleep(1) self._window = windows[0] self._window.alwaysOnTop(True) self._window.activate(True) self._ticks_ms = 0 - self._last_input = None + self._last_input: Optional[cdg.Keys] = None self._read_frame_input = True def _select_pencil(self) -> None: @@ -116,10 +117,16 @@ def _mouse_up(self, x, y): def draw_frame(self, pixels: np.ndarray) -> None: pixels = pixels / 255. pixels = filters.gaussian(pixels, channel_axis=2, sigma=2) - pixels = np.apply_along_axis(lambda pix: np.argmin(color.deltaE_cie76(PALETTE_LAB, color.rgb2lab(pix[[2, 1, 0]]))), axis=2, arr=pixels) + pixels = np.apply_along_axis(lambda pix: np.argmin( + color.deltaE_cie76(PALETTE_LAB, color.rgb2lab(pix[[2, 1, 0]]))), + axis=2, + arr=pixels) pixels = pixels.astype(np.uint8) pixels = filters.rank.modal(pixels, morphology.disk(2)) - pixels_with_border = np.pad(pixels, pad_width=1, mode='constant', constant_values=-1) + pixels_with_border = np.pad(pixels, + pad_width=1, + mode='constant', + constant_values=-1) color_idxs, color_cnts = np.unique(pixels, return_counts=True) color_idxs = color_idxs[np.argsort(color_cnts)[::-1]] @@ -132,10 +139,15 @@ def draw_frame(self, pixels: np.ndarray) -> None: self._select_pencil() for i, idx in enumerate(color_idxs): layer = ~np.isin(pixels, color_idxs[:i]) - layer = np.pad(layer, pad_width=1, mode='constant', constant_values=0) - label_layer, layer_region_cnt = measure.label(layer, return_num=True) - - label_layer_boundaries = segmentation.find_boundaries(label_layer, mode="inner") + layer = np.pad(layer, + pad_width=1, + mode='constant', + constant_values=0) + label_layer, layer_region_cnt = measure.label(layer, + return_num=True) + + label_layer_boundaries = segmentation.find_boundaries(label_layer, + mode="inner") label_layer_inner = label_layer.copy() label_layer_inner[label_layer_boundaries] = 0 @@ -143,12 +155,17 @@ def draw_frame(self, pixels: np.ndarray) -> None: self._select_color(idx) for region_i in range(1, layer_region_cnt + 1): - if not np.any((label_layer == region_i) & (pixels_with_border == idx)): + if not np.any((label_layer == region_i) + & (pixels_with_border == idx)): continue - region_contours = measure.find_contours((label_layer == region_i), 0.5, positive_orientation='high') + region_contours = measure.find_contours( + (label_layer == region_i), + 0.5, + positive_orientation='high') for contour in region_contours: win_x, win_y = self._window.position - pyautogui.moveTo(61 + contour[0][1] + win_x, 24 + contour[0][0] + win_y) + pyautogui.moveTo(61 + contour[0][1] + win_x, + 24 + contour[0][0] + win_y) pyautogui.mouseDown() pyautogui.PAUSE = 0.0006 for (y, x) in contour: @@ -156,10 +173,14 @@ def draw_frame(self, pixels: np.ndarray) -> None: pyautogui.PAUSE = 0.07 pyautogui.mouseUp() - region_inner_segments, region_inner_segment_cnt = measure.label(label_layer_inner == region_i, return_num=True) + region_inner_segments, region_inner_segment_cnt = ( + measure.label(label_layer_inner == region_i, + return_num=True)) - for region_inner_segment_i in range(1, region_inner_segment_cnt + 1): - indices = np.argwhere(region_inner_segments == region_inner_segment_i) + for region_inner_segment_i in range( + 1, region_inner_segment_cnt + 1): + indices = np.argwhere( + region_inner_segments == region_inner_segment_i) random_index = np.random.randint(0, len(indices)) y, x = indices[random_index] @@ -181,7 +202,7 @@ def draw_frame(self, pixels: np.ndarray) -> None: self._select_fill_with_color() self._read_frame_input = False - def get_key(self) -> Optional[Tuple[int, int]]: + def get_key(self) -> Optional[tuple[int, int]]: if self._read_frame_input: return None if self._last_input is not None: @@ -192,10 +213,12 @@ def get_key(self) -> Optional[Tuple[int, int]]: while True: time.sleep(0.1) win_x, win_y = self._window.position - im = np.asarray(pyautogui.screenshot(region=(61 + win_x, 224 + win_y, 35 * 9, 40))) + im = np.asarray( + pyautogui.screenshot(region=(61 + win_x, 224 + win_y, 35 * 9, + 40))) for i, key in enumerate(KEYMAP): x = 35 * i - if np.all(im[0:40, x:x+35] == 0): + if np.all(im[0:40, x:x + 35] == 0): if key[0] is None: return None self._last_input = key[0] @@ -208,9 +231,5 @@ def get_ticks_ms(self) -> int: if __name__ == "__main__": g = MsPaintDoom() - cdg.init(320, - 200, - g.draw_frame, - g.get_key, - get_ticks_ms=g.get_ticks_ms) + cdg.init(320, 200, g.draw_frame, g.get_key, get_ticks_ms=g.get_ticks_ms) cdg.main() diff --git a/cydoomgeneric/demopygame.py b/demo/demopygame.py similarity index 75% rename from cydoomgeneric/demopygame.py rename to demo/demopygame.py index b6e4d48..be4070d 100644 --- a/cydoomgeneric/demopygame.py +++ b/demo/demopygame.py @@ -13,15 +13,13 @@ GNU General Public License for more details. """ - import sys -from typing import Optional, Tuple - +from typing import Optional -import cydoomgeneric as cdg import numpy as np import pygame +import cydoomgeneric as cdg keymap = { pygame.K_LEFT: cdg.Keys.LEFTARROW, @@ -39,25 +37,20 @@ class PygameDoom: - def __init__(self) -> None: - self.resx = 640 - self.resy = 400 - self.screen = None - - def init(self) -> None: + def __init__(self) -> None: + self._resx = 640 + self._resy = 400 pygame.init() - self.screen = pygame.display.set_mode((self.resx, self.resy)) - + self._screen = pygame.display.set_mode((self._resx, self._resy)) def draw_frame(self, pixels: np.ndarray) -> None: pixels = np.rot90(pixels) pixels = np.flipud(pixels) - pygame.surfarray.blit_array(self.screen, pixels[:,:,[2,1,0]]) + pygame.surfarray.blit_array(self._screen, pixels[:, :, [2, 1, 0]]) pygame.display.flip() - - def get_key(self) -> Optional[Tuple[int, int]]: + def get_key(self) -> Optional[tuple[int, int]]: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() @@ -65,13 +58,12 @@ def get_key(self) -> Optional[Tuple[int, int]]: if event.type == pygame.KEYDOWN: if event.key in keymap: return 1, keymap[event.key] - + if event.type == pygame.KEYUP: if event.key in keymap: return 0, keymap[event.key] - - return None + return None def set_window_title(self, t: str) -> None: pygame.display.set_caption(t) @@ -79,10 +71,9 @@ def set_window_title(self, t: str) -> None: if __name__ == "__main__": g = PygameDoom() - cdg.init(g.resx, - g.resy, - g.draw_frame, - g.get_key, - init=g.init, - set_window_title=g.set_window_title) + cdg.init(g._resx, + g._resy, + g.draw_frame, + g.get_key, + set_window_title=g.set_window_title) cdg.main() diff --git a/cydoomgeneric/demopyplot.py b/demo/demopyplot.py similarity index 85% rename from cydoomgeneric/demopyplot.py rename to demo/demopyplot.py index bc1e3da..e115f4d 100644 --- a/cydoomgeneric/demopyplot.py +++ b/demo/demopyplot.py @@ -12,16 +12,14 @@ GNU General Public License for more details. """ - import sys -from typing import Optional, Tuple, List +from typing import Optional import matplotlib.pyplot as plt import numpy as np import cydoomgeneric as cdg - KEYMAP = { "left": cdg.Keys.LEFTARROW, "right": cdg.Keys.RIGHTARROW, @@ -38,10 +36,11 @@ class PyPlotDoom: + def __init__(self) -> None: - self._keyevent_queue: List[Tuple[str, int]] = [] + self._keyevent_queue: list[tuple[str, int]] = [] self._fig = plt.figure() - self._ax = self._fig.add_subplot(1,1,1) + self._ax = self._fig.add_subplot(1, 1, 1) self._fig.canvas.mpl_connect('key_press_event', self._on_press) self._fig.canvas.mpl_connect('key_release_event', self._on_release) self._fig.canvas.mpl_connect('close_event', lambda _: sys.exit()) @@ -55,11 +54,11 @@ def _on_release(self, event) -> None: def draw_frame(self, pixels: np.ndarray) -> None: self._ax.clear() - self._ax.imshow(pixels[:,:,[2,1,0]]) + self._ax.imshow(pixels[:, :, [2, 1, 0]]) self._fig.canvas.draw() self._fig.canvas.flush_events() - def get_key(self) -> Optional[Tuple[int, int]]: + def get_key(self) -> Optional[tuple[int, int]]: if len(self._keyevent_queue) == 0: return None (key, pressed) = self._keyevent_queue.pop(0) @@ -76,8 +75,8 @@ def set_window_title(self, t: str) -> None: if __name__ == "__main__": g = PyPlotDoom() cdg.init(640, - 400, - g.draw_frame, - g.get_key, - set_window_title=g.set_window_title) + 400, + g.draw_frame, + g.get_key, + set_window_title=g.set_window_title) cdg.main() diff --git a/doomgeneric/doomgeneric.c b/doomgeneric/doomgeneric.c index 31e983d..64ee051 100644 --- a/doomgeneric/doomgeneric.c +++ b/doomgeneric/doomgeneric.c @@ -1,7 +1,7 @@ // // Copyright(C) 1993-1996 Id Software, Inc. // Copyright(C) 2005-2014 Simon Howard -// Copyright(C) 2023 Wojciech Graj +// Copyright(C) 2023-2024 Wojciech Graj // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -30,7 +30,6 @@ uint32_t DOOMGENERIC_RESX; uint32_t DOOMGENERIC_RESY; uint32_t* DG_ScreenBuffer = 0; -void (*DG_Init)(); void (*DG_DrawFrame)(); void (*DG_SleepMs)(uint32_t); uint32_t (*DG_GetTicksMs)(); @@ -39,7 +38,6 @@ void (*DG_SetWindowTitle)(const char*); void dg_Create(uint32_t resx, uint32_t resy, - void (*pDG_Init)(), void (*pDG_DrawFrame)(), void (*pDG_SleepMs)(uint32_t), uint32_t (*pDG_GetTicksMs)(), @@ -49,7 +47,6 @@ void dg_Create(uint32_t resx, DOOMGENERIC_RESX = resx; DOOMGENERIC_RESY = resy; - DG_Init = pDG_Init; DG_DrawFrame = pDG_DrawFrame; DG_SleepMs = pDG_SleepMs; DG_GetTicksMs = pDG_GetTicksMs; @@ -57,8 +54,6 @@ void dg_Create(uint32_t resx, DG_SetWindowTitle = pDG_SetWindowTitle; DG_ScreenBuffer = malloc(DOOMGENERIC_RESX * DOOMGENERIC_RESY * 4); - - DG_Init(); } int dg_main(int argc, char **argv) diff --git a/doomgeneric/doomgeneric.h b/doomgeneric/doomgeneric.h index 8337f46..58920b3 100644 --- a/doomgeneric/doomgeneric.h +++ b/doomgeneric/doomgeneric.h @@ -1,5 +1,5 @@ // -// Copyright(C) 2023 Wojciech Graj +// Copyright(C) 2023-2024 Wojciech Graj // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -27,7 +27,6 @@ extern uint32_t* DG_ScreenBuffer; void dg_Create(uint32_t resx, uint32_t resy, - void (*pDG_Init)(), void (*pDG_DrawFrame)(), void (*pDG_SleepMs)(uint32_t), uint32_t (*pDG_GetTicksMs)(), @@ -35,7 +34,6 @@ void dg_Create(uint32_t resx, void (*pDG_SetWindowTitle)(const char*)); int dg_main(int argc, char **argv); -extern void (*DG_Init)(); extern void (*DG_DrawFrame)(); extern void (*DG_SleepMs)(uint32_t); extern uint32_t (*DG_GetTicksMs)(); diff --git a/pyproject.toml b/pyproject.toml index 9641902..4e5d9ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "cython", "numpy>=1.20"] +requires = ["setuptools", "cython", "numpy>=1.25"] build-backend = "setuptools.build_meta" [project] @@ -7,12 +7,42 @@ name = "cydoomgeneric" description = "Easily portable doom for python" version = "0.1.0" authors = [ - {name = "Wojciech Graj"} + {name = "Wojciech Graj"} ] license = {file = "LICENSE"} +requires-python = ">=3.9" +dependencies = [ + "numpy>=1.25" +] [project.urls] homepage = "https://github.com/wojciech-graj/cydoomgeneric" +[project.optional-dependencies] +dev = [ + "pylsp-mypy", + "pylsp-rope", + "python-lsp-isort", + "python-lsp-server[all]" +] +pyplot = [ + "matplotlib" +] +minepi = [ + "mcpi", + "scikit-image" +] +mspaint = [ + "pyautogui", + "pywinctl", + "scikit-image" +] +pygame = [ + "pygame" +] + [tool.setuptools] -py-modules = [] \ No newline at end of file +py-modules = [] + +[tool.pydocstyle] +ignore = ["D101", "D102", "D103", "D107", "D205", "D208", "D212", "D400", "D415"] diff --git a/setup.py b/setup.py index 43ad393..a6ad0e3 100644 --- a/setup.py +++ b/setup.py @@ -13,13 +13,11 @@ GNU General Public License for more details. """ - -from setuptools import Extension, setup -from Cython.Build import cythonize -import Cython -import numpy import sys +import numpy +from Cython.Build import cythonize +from setuptools import Extension, setup doom_src = ( "am_map.c", @@ -104,45 +102,28 @@ "z_zone.c", ) - -libraries = [] -define_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] -extra_compile_args = [] -extra_link_args = [] -compiler_directives = {} - +libraries: list[str] = [] +define_macros: list[tuple[str, str | None]] = [] +extra_link_args: list[str] = [] if sys.platform == "win32": libraries.append("user32") +elif sys.platform == "darwin": + define_macros.extend([("NORMALUNIX", None), ("_DEFAULT_SOURCE", None)]) else: - define_macros.extend([ - ("NORMALUNIX", None), - ("LINUX", None), - ("_DEFAULT_SOURCE", None) - ]) - extra_compile_args.append("-Os") + define_macros.extend([("NORMALUNIX", None), ("LINUX", None), + ("_DEFAULT_SOURCE", None)]) - -setup( - ext_modules=cythonize( - [ - Extension( - "cydoomgeneric", - sources=[ - "./cydoomgeneric/cydoomgeneric.pyx" - ] - + [f"./doomgeneric/{src}" for src in doom_src], - include_dirs=[ - "./doomgeneric", - numpy.get_include() - ], - define_macros=define_macros, - extra_compile_args=extra_compile_args, - extra_link_args=extra_link_args, - libraries=libraries - ) - ], - language_level=3, - compiler_directives=compiler_directives - ), -) +setup(ext_modules=cythonize([ + Extension("cydoomgeneric", + sources=["./cydoomgeneric/cydoomgeneric.pyx"] + + [f"./doomgeneric/{src}" for src in doom_src], + include_dirs=["./doomgeneric", + numpy.get_include()], + define_macros=define_macros, + extra_link_args=extra_link_args, + libraries=libraries), +], + language_level=3), + package_data={"cydoomgeneric": ["py.typed", "__init__.pyi"]}, + packages=["cydoomgeneric"])