From 85be45e2b8f56eedf6f89d242a465491e2b3d280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Fri, 16 Aug 2024 17:20:07 +0200 Subject: [PATCH] first commit --- .vscode/launch.json | 15 ++++++ .vscode/settings.json | 24 +++++++++ LICENSE | 24 +++++++++ README.md | 1 + data/Alloy_curses_12x12.png | Bin 0 -> 2061 bytes g.py | 21 ++++++++ game/__init__.py | 1 + game/components.py | 45 ++++++++++++++++ game/constants.py | 36 +++++++++++++ game/menus.py | 101 ++++++++++++++++++++++++++++++++++++ game/state.py | 44 ++++++++++++++++ game/state_tools.py | 61 ++++++++++++++++++++++ game/states.py | 92 ++++++++++++++++++++++++++++++++ game/tags.py | 14 +++++ game/world_tools.py | 32 ++++++++++++ main.py | 28 ++++++++++ pyproject.toml | 63 ++++++++++++++++++++++ requirements.txt | 3 ++ 18 files changed, 605 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 data/Alloy_curses_12x12.png create mode 100644 g.py create mode 100644 game/__init__.py create mode 100644 game/components.py create mode 100644 game/constants.py create mode 100644 game/menus.py create mode 100644 game/state.py create mode 100644 game/state_tools.py create mode 100644 game/states.py create mode 100644 game/tags.py create mode 100644 game/world_tools.py create mode 100755 main.py create mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9acbe7f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "main", + "justMyCode": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..deb6640 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "always" + }, + "editor.rulers": [ + 120 + ], + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "cSpell.words": [ + "blit", + "isort", + "PAGEDOWN", + "PAGEUP", + "tcod", + "tileset", + "tilesheet" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..2339b4e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# python-tcod-tutorial-2023 diff --git a/data/Alloy_curses_12x12.png b/data/Alloy_curses_12x12.png new file mode 100644 index 0000000000000000000000000000000000000000..29662df7746957be6573a5b85b985c374203902f GIT binary patch literal 2061 zcmV+o2=e!dP)Px#1ZP1_K>z@;j|==^1poj522e~?MgIW*|NsA=7&71h000SaNLh0L01m4g<-tAs2;Qa+FC98Ze{$@0Db? z$8DVTM#!?dKh>-EUj6l|)I=*sQ}Njys*WxGsgs7p3_}Enc~D%e#xtKTQH%?sDfG z$HAsV!|tAyfpW(#r?rtMM08_H4rr6s9yx`9n#GG+($$*Y?48pQJ&4q7y!ebbT`S-j za#_sA&WU@uCfwZH(lwo^bC?>t%9~P!O>`*WHG|<|=0SFPp7h?us*)B&K?i&b2Hc0G zz*Wtn4pfakvf^H5%hs#FC5@|)NADE_A4hO%8h07Ti96^bS2dsA!Qhr7T=%6*)g0h~ zvRc{hOW0Q>Kuk1NHb6cuO#tXR(cc0HDNkF+41;g0d7_gC8lRG33wR8X&N)8sTLvdZ zO~|=m2FS!jz_5+RccyFjlX8z;AL1Uj*w@JfIpzoREut+=EeJP~ZA(551{>=lH+07% zrh}m>wXPrl+^~m`=CJ_7 zlE~>J>jFZ8%YDvA<@2YkqmQi%DamYtWK1L<(!$ZMb;unor5H-ihwp=0ueA2S0kBtS z5$h^bWv?iQ15$sNdiUue6#O z{ijher_nU&PffqDW@-e?jjng6h9(TCUwq!zbOAp0Al3jVK$I~kjeeR5sJa$AsR$f4 zCsR&S*M~M9;A7eX@ye)~fEfUL73;*uu}3xH80r~1;5hzI2T$aTN{mN_ffAJ)+y_OsyrA9fXv7V*rQ|@qJnh*1Y|2ZXv^90n$V->YgVcj>=5;2 zdhbBE+73MkWMV_e1HQ>MSFM#9FAA5be+g6>%W2edph~X6y4sg{4V54t3F^!?Rs%H; z^i!GF966=BGLQ>{vIDFu0P4Ve6|gBz8iUz|spUkK+eIl!t5ZFTp+vQ+D=8%DnjFYe z!#jxWDA4x7#hkvkGS4BDv0YOj8Km}rCTjV(lPD%?#z{mCDj~RJFinhrfleN2saR^x zV4LR|>uL*1^ewbufbqEiM*8T0Mona0VLV1Hs0pj*o+`K061p1MJd-%QqsjVWwYMs` zp1qO;u9=$U6H0BprzN2gyNDIfLOz%{M(0=%d7#=kKn?((L!FP=06=GUYgL{wWjF$! z2K#ayYPYA|(1TPeHP>XZiD}4v0A#8{m|VzEoG$=6PyDl58VbM@0SO&uD>0%o#KA50)%jRo0Z`EQZI1lp zR{<85>XGzu3B3Z~Ic%@%Wq+TIi7<_C{GV8umxa*ZDUo)AGujVS}QBceGp9)yT z`S$dP%cAVo6TjIFdH3WdUR117^DDdBO}CTqnU{frinu1tu*VLudq0H|^YAobMN4!` zsoEGbci-h$&Ug_PaGXuCVIx|+8n`N@H@u7r*x}gp%%GRqjp!F)^ZHt(iHLwXu zv|e2YtyjcjfDJsS&x@$_>N@aVtpQSH`3!v6?fH#B?`+mTYu+|Zzv1i6pw~1_b%INv zUKrU^w=2VqKm^~KiqBp&&5PF5?EvnQoZjO*051-f1z&UCUqnu$k2`?p4ZiACC%87% z{{r~U6#1Y$ZeCDSB11QVrg6HG{SW$wDU3EA3ZmPAp8#GZ^bNs{M-@-c<);RUZ52zI zlND5CX#^niyz3(q;&pYqU=VUDml1<}yQr{@rb#i!zmU%R(<3}Fveo$Xn1NJz==^iY zvR~9Aym_BK#rt;)LLSjvw&MUxZ%RPH0DS-qd>_6mBOnmK>lLop<{9-FA{e?ITpwEw zx+n9>-SQfk&`m!p+yr<*b27yTw1W;6$K+9r3&DTHr^I r0>4SPU?tMbUC!v$&BK+o?fv7wS&Qrqn}U#M00000NkvXXu0mjfBI?dh literal 0 HcmV?d00001 diff --git a/g.py b/g.py new file mode 100644 index 0000000..66440ab --- /dev/null +++ b/g.py @@ -0,0 +1,21 @@ +"""This module stores globally mutable variables used by this program.""" + +from __future__ import annotations + +import tcod.console +import tcod.context +import tcod.ecs + +import game.state + +context: tcod.context.Context +"""The window managed by tcod.""" + +world: tcod.ecs.Registry +"""The active ECS registry and current session.""" + +states: list[game.state.State] = [] +"""A stack of states with the last item being the active state.""" + +console: tcod.console.Console +"""The current main console.""" diff --git a/game/__init__.py b/game/__init__.py new file mode 100644 index 0000000..fe4e8e7 --- /dev/null +++ b/game/__init__.py @@ -0,0 +1 @@ +"""Game namespace package.""" diff --git a/game/components.py b/game/components.py new file mode 100644 index 0000000..1144f11 --- /dev/null +++ b/game/components.py @@ -0,0 +1,45 @@ +"""Collection of common components.""" + +from __future__ import annotations + +from typing import Final, Self + +import attrs +import tcod.ecs.callbacks +from tcod.ecs import Entity + + +@attrs.define(frozen=True) +class Position: + """An entities position.""" + + x: int + y: int + + def __add__(self, direction: tuple[int, int]) -> Self: + """Add a vector to this position.""" + x, y = direction + return self.__class__(self.x + x, self.y + y) + + +@tcod.ecs.callbacks.register_component_changed(component=Position) +def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None: + """Mirror position components as a tag.""" + if old == new: + return + if old is not None: + entity.tags.discard(old) + if new is not None: + entity.tags.add(new) + + +@attrs.define(frozen=True) +class Graphic: + """An entities icon and color.""" + + ch: int = ord("!") + fg: tuple[int, int, int] = (255, 255, 255) + + +Gold: Final = ("Gold", int) +"""Amount of gold.""" diff --git a/game/constants.py b/game/constants.py new file mode 100644 index 0000000..3e9afcf --- /dev/null +++ b/game/constants.py @@ -0,0 +1,36 @@ +"""Global constants are stored here.""" + +from typing import Final + +from tcod.event import KeySym + +DIRECTION_KEYS: Final = { + # Arrow keys + KeySym.LEFT: (-1, 0), + KeySym.RIGHT: (1, 0), + KeySym.UP: (0, -1), + KeySym.DOWN: (0, 1), + # Arrow key diagonals + KeySym.HOME: (-1, -1), + KeySym.END: (-1, 1), + KeySym.PAGEUP: (1, -1), + KeySym.PAGEDOWN: (1, 1), + # Keypad + KeySym.KP_4: (-1, 0), + KeySym.KP_6: (1, 0), + KeySym.KP_8: (0, -1), + KeySym.KP_2: (0, 1), + KeySym.KP_7: (-1, -1), + KeySym.KP_1: (-1, 1), + KeySym.KP_9: (1, -1), + KeySym.KP_3: (1, 1), + # VI keys + KeySym.h: (-1, 0), + KeySym.l: (1, 0), + KeySym.k: (0, -1), + KeySym.j: (0, 1), + KeySym.y: (-1, -1), + KeySym.b: (-1, 1), + KeySym.u: (1, -1), + KeySym.n: (1, 1), +} diff --git a/game/menus.py b/game/menus.py new file mode 100644 index 0000000..7c1bb37 --- /dev/null +++ b/game/menus.py @@ -0,0 +1,101 @@ +"""Menu UI classes.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Protocol + +import attrs +import tcod.console +import tcod.event +from tcod.event import KeySym + +import game.state_tools +from game.constants import DIRECTION_KEYS +from game.state import Pop, State, StateResult + + +class MenuItem(Protocol): + """Menu item protocol.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events passed to the menu item.""" + + def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: + """Draw is item at the given position.""" + + +@attrs.define() +class SelectItem(MenuItem): + """Clickable menu item.""" + + label: str + callback: Callable[[], StateResult] + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events selecting this item.""" + match event: + case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}: + return self.callback() + case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT): + return self.callback() + case _: + return None + + def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: + """Render this items label.""" + console.print(x, y, self.label, fg=(255, 255, 255), bg=(64, 64, 64) if highlight else (0, 0, 0)) + + +@attrs.define() +class ListMenu(State): + """Simple list menu state.""" + + items: tuple[MenuItem, ...] + selected: int | None = 0 + x: int = 0 + y: int = 0 + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events for menus.""" + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + dx, dy = DIRECTION_KEYS[sym] + if dx != 0 or dy == 0: + return self.activate_selected(event) + if self.selected is not None: + self.selected += dy + self.selected %= len(self.items) + else: + self.selected = 0 if dy == 1 else len(self.items) - 1 + return None + case tcod.event.MouseMotion(position=(_, y)): + y -= self.y + self.selected = y if 0 <= y < len(self.items) else None + return None + case tcod.event.KeyDown(sym=KeySym.ESCAPE): + return self.on_cancel() + case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.RIGHT): + return self.on_cancel() + case _: + return self.activate_selected(event) + + def activate_selected(self, event: tcod.event.Event) -> StateResult: + """Call the selected menu items callback.""" + if self.selected is not None: + return self.items[self.selected].on_event(event) + return None + + def on_cancel(self) -> StateResult: + """Handle escape or right click being pressed on menus.""" + return Pop() + + def on_draw(self, console: tcod.console.Console) -> None: + """Render the menu.""" + game.state_tools.draw_previous_state(self, console) + for i, item in enumerate(self.items): + item.on_draw(console, x=self.x, y=self.y + i, highlight=i == self.selected) diff --git a/game/state.py b/game/state.py new file mode 100644 index 0000000..4b935c4 --- /dev/null +++ b/game/state.py @@ -0,0 +1,44 @@ +"""Base classes for states.""" + +from __future__ import annotations + +from typing import Protocol, TypeAlias + +import attrs +import tcod.console +import tcod.event + + +class State(Protocol): + """An abstract game state.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Called on events.""" + + def on_draw(self, console: tcod.console.Console) -> None: + """Called when the state is being drawn.""" + + +@attrs.define() +class Push: + """Push a new state on top of the stack.""" + + state: State + + +@attrs.define() +class Pop: + """Remove the current state from the stack.""" + + +@attrs.define() +class Reset: + """Replace the entire stack with a new state.""" + + state: State + + +StateResult: TypeAlias = "Push | Pop | Reset | None" +"""Union of state results.""" diff --git a/game/state_tools.py b/game/state_tools.py new file mode 100644 index 0000000..77d2e22 --- /dev/null +++ b/game/state_tools.py @@ -0,0 +1,61 @@ +"""State handling functions.""" + +from __future__ import annotations + +import tcod.console + +import g +from game.state import Pop, Push, Reset, State, StateResult + + +def main_draw() -> None: + """Render and present the active state.""" + if not g.states: + return + g.console.clear() + g.states[-1].on_draw(g.console) + g.context.present(g.console) + + +def apply_state_result(result: StateResult) -> None: + """Apply a StateResult to `g.states`.""" + match result: + case Push(state=state): + g.states.append(state) + case Pop(): + g.states.pop() + case Reset(state=state): + while g.states: + apply_state_result(Pop()) + apply_state_result(Push(state)) + case None: + pass + case _: + raise TypeError(result) + + +def main_loop() -> None: + """Run the active state forever.""" + while g.states: + main_draw() + for event in tcod.event.wait(): + tile_event = g.context.convert_event(event) + if g.states: + apply_state_result(g.states[-1].on_event(tile_event)) + + +def get_previous_state(state: State) -> State | None: + """Return the state before `state` in the stack if it exists.""" + current_index = next(index for index, value in enumerate(g.states) if value is state) + return g.states[current_index - 1] if current_index > 0 else None + + +def draw_previous_state(state: State, console: tcod.console.Console, dim: bool = True) -> None: + """Draw previous states, optionally dimming all but the active state.""" + prev_state = get_previous_state(state) + if prev_state is None: + return + prev_state.on_draw(console) + if dim and state is g.states[-1]: + console.rgb["fg"] //= 4 + console.rgb["bg"] //= 4 diff --git a/game/states.py b/game/states.py new file mode 100644 index 0000000..6b2ed2e --- /dev/null +++ b/game/states.py @@ -0,0 +1,92 @@ +"""A collection of game states.""" + +from __future__ import annotations + +import attrs +import tcod.console +import tcod.constants +import tcod.event +from tcod.event import KeySym + +import g +import game.menus +import game.world_tools +from game.components import Gold, Graphic, Position +from game.constants import DIRECTION_KEYS +from game.state import Push, Reset, State, StateResult +from game.tags import IsItem, IsPlayer + + +@attrs.define() +class InGame(State): + """Primary in-game state.""" + + def on_event(self, event: tcod.event.Event) -> StateResult: + """Handle events for the in-game state.""" + (player,) = g.world.Q.all_of(tags=[IsPlayer]) + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: + player.components[Position] += DIRECTION_KEYS[sym] + # Auto pickup gold + for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): + player.components[Gold] += gold.components[Gold] + text = f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g" + g.world[None].components[("Text", str)] = text + gold.clear() + return None + case tcod.event.KeyDown(sym=KeySym.ESCAPE): + return Push(MainMenu()) + case _: + return None + + def on_draw(self, console: tcod.console.Console) -> None: + """Draw the standard screen.""" + for entity in g.world.Q.all_of(components=[Position, Graphic]): + pos = entity.components[Position] + if not (0 <= pos.x < console.width and 0 <= pos.y < console.height): + continue + graphic = entity.components[Graphic] + console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg + + if text := g.world[None].components.get(("Text", str)): + console.print(x=0, y=console.height - 1, string=text, fg=(255, 255, 255), bg=(0, 0, 0)) + + +class MainMenu(game.menus.ListMenu): + """Main/escape menu.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize the main menu.""" + items = [ + game.menus.SelectItem("New game", self.new_game), + game.menus.SelectItem("Quit", self.quit), + ] + if hasattr(g, "world"): + items.insert(0, game.menus.SelectItem("Continue", self.continue_)) + + super().__init__( + items=tuple(items), + selected=0, + x=5, + y=5, + ) + + @staticmethod + def continue_() -> StateResult: + """Return to the game.""" + return Reset(InGame()) + + @staticmethod + def new_game() -> StateResult: + """Begin a new game.""" + g.world = game.world_tools.new_world() + return Reset(InGame()) + + @staticmethod + def quit() -> StateResult: + """Close the program.""" + raise SystemExit() diff --git a/game/tags.py b/game/tags.py new file mode 100644 index 0000000..00f8a0d --- /dev/null +++ b/game/tags.py @@ -0,0 +1,14 @@ +"""Collection of common tags.""" + +from __future__ import annotations + +from typing import Final + +IsPlayer: Final = "IsPlayer" +"""Entity is the player.""" + +IsActor: Final = "IsActor" +"""Entity is an actor.""" + +IsItem: Final = "IsItem" +"""Entity is an item.""" diff --git a/game/world_tools.py b/game/world_tools.py new file mode 100644 index 0000000..6e09e4b --- /dev/null +++ b/game/world_tools.py @@ -0,0 +1,32 @@ +"""Functions for working with worlds.""" + +from __future__ import annotations + +from random import Random + +from tcod.ecs import Registry + +from game.components import Gold, Graphic, Position +from game.tags import IsActor, IsItem, IsPlayer + + +def new_world() -> Registry: + """Return a freshly generated world.""" + world = Registry() + + rng = world[None].components[Random] = Random() + + player = world[object()] + player.components[Position] = Position(5, 5) + player.components[Graphic] = Graphic(ord("@")) + player.components[Gold] = 0 + player.tags |= {IsPlayer, IsActor} + + for _ in range(10): + gold = world[object()] + gold.components[Position] = Position(rng.randint(0, 20), rng.randint(0, 20)) + gold.components[Graphic] = Graphic(ord("$"), fg=(255, 255, 0)) + gold.components[Gold] = rng.randint(1, 10) + gold.tags |= {IsItem} + + return world diff --git a/main.py b/main.py new file mode 100755 index 0000000..51cc264 --- /dev/null +++ b/main.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Main entry-point module. This script is used to start the program.""" + +from __future__ import annotations + +import tcod.console +import tcod.context +import tcod.tileset + +import g +import game.state_tools +import game.states + + +def main() -> None: + """Entry point function.""" + tileset = tcod.tileset.load_tilesheet( + "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 + ) + tcod.tileset.procedural_block_elements(tileset=tileset) + g.states = [game.states.MainMenu()] + g.console = tcod.console.Console(80, 50) + with tcod.context.new(console=g.console, tileset=tileset) as g.context: + game.state_tools.main_loop() + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..784034c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +# Project configuiration options for Python tools. +[tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html +python_version = "3.11" +files = ["."] +warn_unused_configs = true +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_calls = false # Some calls from NumPy are untyped. +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +no_implicit_reexport = true +strict_equality = true + +[tool.ruff] # https://docs.astral.sh/ruff/rules/ +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "C90", # mccabe + "D", # pydocstyle + "E", # pycodestyle + "W", # pycodestyle + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "S", # flake8-bandit + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "RET", # flake8-return + "ICN", # flake8-import-conventions + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "PL", # Pylint + "TRY", # tryceratops + "RUF", # NumPy-specific rules + "G", # flake8-logging-format +] +ignore = [ + "E501", # line-too-long + "S101", # assert + "ANN101", # missing-type-self + "ANN102", # missing-type-cls + "S311", # suspicious-non-cryptographic-random-usage + "PLR0913", # too-many-arguments +] + +[tool.ruff.lint.pydocstyle] # https://docs.astral.sh/ruff/settings/#lintpydocstyle +convention = "google" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..610b558 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +attrs ~= 22.2 +tcod ~= 16.0 +tcod-ecs ~= 5.1