commit 1be4095e0448343434002966c1c2b74b2eebf870 Author: Tommy Parnell Date: Wed Jul 2 21:57:21 2025 -0400 the fang works diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb9e9c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,211 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.pysaver_default +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/README.md b/README.md new file mode 100644 index 0000000..79db084 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# PySaver + +A simple screensaver for headless Linux machines to prevent screen burn-in. + +## Installation + +```bash +poetry install +``` + +## Usage + +```bash +poetry run pysaver +``` + +Press any key to exit the screensaver. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f31cfb9 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "ansicon" +version = "1.89.0" +description = "Python wrapper for loading Jason Hood's ANSICON" +optional = false +python-versions = "*" +files = [ + {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, + {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, +] + +[[package]] +name = "blessed" +version = "1.21.0" +description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." +optional = false +python-versions = ">=2.7" +files = [ + {file = "blessed-1.21.0-py2.py3-none-any.whl", hash = "sha256:f831e847396f5a2eac6c106f4dfadedf46c4f804733574b15fe86d2ed45a9588"}, + {file = "blessed-1.21.0.tar.gz", hash = "sha256:ece8bbc4758ab9176452f4e3a719d70088eb5739798cd5582c9e05f2a28337ec"}, +] + +[package.dependencies] +jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} +wcwidth = ">=0.1.4" + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "jinxed" +version = "1.3.0" +description = "Jinxed Terminal Library" +optional = false +python-versions = "*" +files = [ + {file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"}, + {file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"}, +] + +[package.dependencies] +ansicon = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "pyfiglet" +version = "1.0.3" +description = "Pure-python FIGlet implementation" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyfiglet-1.0.3-py3-none-any.whl", hash = "sha256:671bd101ca6a08dc2d94c6a2cda75a862c5e162b980af47d0ba4023837e36489"}, + {file = "pyfiglet-1.0.3.tar.gz", hash = "sha256:bad3b55d2eccb30d4693ccfd94573c2a3477dd75f86a0e5465cea51bdbfe2875"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "428b747f500c340daf6f36c5cd4b15ba9a6c1cba2a34188f51267b15857d88e2" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..704acd0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "pysaver" +version = "0.1.0" +description = "A simple screensaver for headless Linux machines." +authors = ["Tommy Parnell "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +blessed = "^1.19.1" +click = "^8.1.7" +pyfiglet = "^1.0.2" + +[tool.poetry.scripts] +pysaver = "pysaver.main:cli" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pysaver/__init__.py b/pysaver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pysaver/main.py b/pysaver/main.py new file mode 100644 index 0000000..ce44157 --- /dev/null +++ b/pysaver/main.py @@ -0,0 +1,111 @@ +import os +import sys +import click +import importlib +from pathlib import Path + +SCREENSAVERS_DIR = Path(__file__).parent / "screensavers" +CONFIG_FILE = Path.home() / ".pysaver_default" + +def get_screensavers(): + """Returns a list of available screensavers.""" + savers = [] + for f in SCREENSAVERS_DIR.glob("*.py"): + if f.name != "__init__.py": + savers.append(f.stem) + return savers + +def get_default_screensaver(): + """Returns the default screensaver, or None if not set.""" + if CONFIG_FILE.exists(): + return CONFIG_FILE.read_text().strip() + return None + +def set_default_screensaver(name): + """Sets the default screensaver.""" + CONFIG_FILE.write_text(name) + +def run_screensaver(name): + """Runs a specific screensaver by name.""" + available_savers = get_screensavers() + if name not in available_savers: + click.echo(f"Error: Screensaver '{name}' not found.") + click.echo("Available screensavers: " + ", ".join(available_savers)) + sys.exit(1) + + try: + module = importlib.import_module(f"pysaver.screensavers.{name}") + if hasattr(module, 'run') and callable(module.run): + module.run() + else: + click.echo(f"Error: Screensaver '{name}' does not have a run() function.") + sys.exit(1) + except ImportError: + click.echo(f"Error: Could not import screensaver '{name}'.") + sys.exit(1) + except Exception as e: + click.echo(f"An error occurred while running screensaver '{name}': {e}") + sys.exit(1) + +@click.group(invoke_without_command=True) +@click.pass_context +def cli(ctx): + """A simple screensaver for headless Linux machines.\n + If run without a command, it executes the default screensaver. + """ + if ctx.invoked_subcommand is None: + default_saver = get_default_screensaver() + if default_saver: + run_screensaver(default_saver) + else: + click.echo("No default screensaver set. Use 'pysaver default ' to set one.") + click.echo("Or run one directly with 'pysaver run '.") + +@cli.command() +@click.argument('name') +def default(name): + """Sets the default screensaver.""" + available_savers = get_screensavers() + if name in available_savers: + set_default_screensaver(name) + click.echo(f"Default screensaver set to '{name}'.") + else: + click.echo(f"Error: Screensaver '{name}' not found.") + click.echo("Available screensavers: " + ", ".join(available_savers)) + sys.exit(1) + +@cli.command(name="list") +def list_savers(): + """Lists all available screensavers.""" + available_savers = get_screensavers() + default_saver = get_default_screensaver() + + if not available_savers: + click.echo("No screensavers found.") + return + + click.echo("Available screensavers:") + for saver in sorted(available_savers): + if saver == default_saver: + click.echo(f" - {saver} (default)") + else: + click.echo(f" - {saver}") + +@cli.command() +@click.argument('screensaver_name', required=False) +def run(screensaver_name): + """Runs a screensaver.\n + If no name is provided, it runs the default screensaver. + """ + if screensaver_name: + run_screensaver(screensaver_name) + else: + default_saver = get_default_screensaver() + if default_saver: + run_screensaver(default_saver) + else: + click.echo("No default screensaver set. Use 'pysaver default ' to set one.") + click.echo("Or run this command with a name, e.g. 'pysaver run clock'.") + +if __name__ == "__main__": + cli() diff --git a/pysaver/screensavers/__init__.py b/pysaver/screensavers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pysaver/screensavers/axe_throw.py b/pysaver/screensavers/axe_throw.py new file mode 100644 index 0000000..44b1dea --- /dev/null +++ b/pysaver/screensavers/axe_throw.py @@ -0,0 +1,71 @@ +import time +from blessed import Terminal + +def run(): + """An animation of a stick figure throwing an axe at a target.""" + term = Terminal() + + # --- ASCII Art Definitions --- + stick_figure_stand = [ + " O ", + "/|\\", + "/ \\" + ] + stick_figure_throw = [ + " O ", + "\\|/", + "/ \\" + ] + axe = "-)" + target = [ + "|", + "O", + "|" + ] + + with term.fullscreen(), term.cbreak(), term.hidden_cursor(): + while True: + print(term.home + term.clear) + + # --- Scene Setup --- + figure_x = 5 + figure_y = term.height // 2 - 1 + + target_x = term.width - 5 + target_y = term.height // 2 - 1 + + axe_start_x = figure_x + 3 + axe_y = figure_y + 1 + + # --- Initial Drawing --- + # Draw stick figure standing + for i, line in enumerate(stick_figure_stand): + print(term.move_xy(figure_x, figure_y + i) + line) + + # Draw target + for i, line in enumerate(target): + print(term.move_xy(target_x, target_y + i) + line) + + time.sleep(1) # Pause before throwing + if term.inkey(timeout=0): return + + + # --- Animation: Throw --- + # Redraw figure in throwing pose + for i, line in enumerate(stick_figure_throw): + print(term.move_xy(figure_x, figure_y + i) + line) + + # Animate axe flying + for x in range(axe_start_x, target_x - 1): + print(term.move_xy(x, axe_y) + axe) + time.sleep(0.05) + print(term.move_xy(x, axe_y) + ' ' * len(axe)) + if term.inkey(timeout=0): return + + # --- Animation: Impact --- + # Draw axe in the target + print(term.move_xy(target_x - 2, axe_y) + axe) + + # Hold final scene + time.sleep(2) + if term.inkey(timeout=0): return diff --git a/pysaver/screensavers/bouncing_text.py b/pysaver/screensavers/bouncing_text.py new file mode 100644 index 0000000..7d58c5b --- /dev/null +++ b/pysaver/screensavers/bouncing_text.py @@ -0,0 +1,37 @@ +import time +from blessed import Terminal + +def run(): + """A bouncing text animation.""" + term = Terminal() + with term.fullscreen(), term.cbreak(), term.hidden_cursor(): + print(term.home + term.clear) + + text = "PySaver" + x, y = term.width // 2 - len(text) // 2, term.height // 2 + vx, vy = 1, 1 + + while True: + # Erase previous text + print(term.move_xy(int(x), int(y)) + ' ' * len(text)) + + # Update position + x += vx + y += vy + + # Bounce off walls + if x <= 0 or x >= term.width - len(text): + vx = -vx + if y <= 0 or y >= term.height - 1: + vy = -vy + + # Clamp position to be within bounds + x = max(0, min(x, term.width - len(text))) + y = max(0, min(y, term.height - 1)) + + # Draw text + print(term.move_xy(int(x), int(y)) + term.bold(text)) + + # Check for key press + if term.inkey(timeout=0.1): + break diff --git a/pysaver/screensavers/clock.py b/pysaver/screensavers/clock.py new file mode 100644 index 0000000..fe8119e --- /dev/null +++ b/pysaver/screensavers/clock.py @@ -0,0 +1,20 @@ +import time +from blessed import Terminal +from datetime import datetime + +def run(): + """A digital clock display.""" + term = Terminal() + with term.fullscreen(), term.cbreak(), term.hidden_cursor(): + print(term.home + term.clear) + while True: + now = datetime.now() + current_time = now.strftime("%H:%M:%S") + + x = term.width // 2 - len(current_time) // 2 + y = term.height // 2 + + print(term.move_xy(x, y) + term.bold(current_time)) + + if term.inkey(timeout=1): + break diff --git a/pysaver/screensavers/fang.py b/pysaver/screensavers/fang.py new file mode 100644 index 0000000..04565a7 --- /dev/null +++ b/pysaver/screensavers/fang.py @@ -0,0 +1,165 @@ +import time +import random +from blessed import Terminal +import pyfiglet + +def run(): + """A multi-scene animation involving a knife thrower and a target.""" + term = Terminal() + + # --- ASCII Art Definitions --- + thrower = [" O ", "/|\\->", "/ \\ "] + side_victim = [" O ", " | ", "/ \\ "] + side_target = ["/-\\", "|O|", "\\-/"] + + front_victim = [ + " \\ O / ", + " | ", + " --+-- ", + " | ", + " / \\ ", + " / \\ " + ] + + front_target = [ + " .---''''---. ", + " .-' '-. ", + " .' '. ", + " / \ ", + " / \ ", + " | | ", + " \ / ", + " \ / ", + " '. .' ", + " '-. .-' ", + " '---....---' " + ] + + fig = pyfiglet.Figlet(font='standard') + banner_art = fig.renderText('FANG') + # CORRECTED: Split by newline character '\n' instead of literal '\\n' + banner_lines = banner_art.split('\n') + banner_height = len(banner_lines) + banner_width = len(banner_lines[0]) if banner_height > 0 else 0 + + knife_char = "'" + colors = [term.red, term.yellow, term.blue, term.magenta, term.cyan] + + with term.fullscreen(), term.cbreak(), term.hidden_cursor(): + while True: + # --- SCENE 1: SIDE VIEW & MULTIPLE THROWS --- # + print(term.home + term.clear) + thrower_x, thrower_y = 5, term.height // 2 - 1 + target_x, target_y = term.width - 15, term.height // 2 - 1 + victim_x, victim_y = target_x - 5, term.height // 2 - 1 + + # Draw static elements + for i, line in enumerate(thrower): + print(term.move_xy(thrower_x, thrower_y + i) + line) + for i, line in enumerate(side_target): + print(term.move_xy(target_x, target_y + i) + line) + for i, line in enumerate(side_victim): + print(term.move_xy(victim_x, victim_y + i) + line) + + # Animate 5 rapid, overlapping knife throws + knife_y = thrower_y + 1 + knives = [] # stores x-positions of active knives + knives_thrown_count = 0 + max_knives = 5 + frame = 0 + + while True: + # Add a new knife every 4 frames until we've thrown 5 + if frame % 4 == 0 and knives_thrown_count < max_knives: + knives.append(thrower_x + len(thrower[1])) + knives_thrown_count += 1 + + # Move and draw all active knives + next_knives = [] + for x in knives: + # Erase old position + print(term.move_xy(x, knife_y) + " ") + # Move forward + new_x = x + 1 + # If knife is still on screen before victim + if new_x < victim_x - 2: + print(term.move_xy(new_x, knife_y) + term.red("-")) + next_knives.append(new_x) + knives = next_knives + + # Exit condition: all 5 knives thrown and all have disappeared + if knives_thrown_count == max_knives and not knives: + break + + frame += 1 + time.sleep(0.04) + if term.inkey(timeout=0): return + + time.sleep(1.0) + if term.inkey(timeout=0): return + + # --- SCENE 2: FRONT VIEW & KNIFE LANDING --- # + print(term.home + term.clear) + center_x, center_y = term.width // 2, term.height // 2 + + # Draw Banner + banner_x = center_x - banner_width // 2 + banner_y = center_y - (len(front_victim) // 2) - banner_height - 3 + for i, line in enumerate(banner_lines): + if line.strip(): + print(term.move_xy(banner_x, banner_y + i) + line) + + # Draw Target (background) + target_height = len(front_target) + target_width = len(front_target[0]) + target_x = center_x - target_width // 2 + target_y = center_y - target_height // 2 + for i, line in enumerate(front_target): + print(term.move_xy(target_x, target_y + i) + term.bright_black(line)) + + # Draw Victim (foreground) + victim_height = len(front_victim) + victim_width = len(front_victim[0]) + victim_x = center_x - victim_width // 2 + victim_y = center_y - victim_height // 2 + for i, line in enumerate(front_victim): + print(term.move_xy(victim_x, victim_y + i) + line) + + def update_banner_color(): + color = random.choice(colors) + for i, line in enumerate(banner_lines): + if line.strip(): + print(term.move_xy(banner_x, banner_y + i) + color(line)) + + # Initial pause with color changing + end_time = time.time() + 1.5 + while time.time() < end_time: + update_banner_color() + time.sleep(0.15) # Faster color change + if term.inkey(timeout=0): return + + # Knife landing with interleaved color changing + knife_positions = [ + (victim_x + 2, victim_y), # Between head and left arm + (victim_x + 4, victim_y), # Between head and right arm + (victim_x + 1, victim_y + 4), # By left leg + (victim_x + 5, victim_y + 4), # By right leg + (center_x, victim_y + 5), # Between feet + ] + for kx, ky in knife_positions: + print(term.move_xy(kx, ky) + term.red(knife_char)) + end_time_knife = time.time() + 0.5 + while time.time() < end_time_knife: + update_banner_color() + time.sleep(0.15) + if term.inkey(timeout=0): return + + # Final pause before restart, with continued color change + end_time_final = time.time() + 4.0 + while time.time() < end_time_final: + update_banner_color() + time.sleep(0.15) + if term.inkey(timeout=0): return + + time.sleep(2.5) + if term.inkey(timeout=0): return \ No newline at end of file diff --git a/pysaver/screensavers/knife_throw.py b/pysaver/screensavers/knife_throw.py new file mode 100644 index 0000000..878619d --- /dev/null +++ b/pysaver/screensavers/knife_throw.py @@ -0,0 +1,87 @@ +import time +from blessed import Terminal + +def run(): + """An animation of a knife hitting a pickle held by a stick figure.""" + term = Terminal() + + # --- ASCII Art Definitions --- + stick_figure = [ + " O ", + " /|----", + " / \ " + ] + pickle_whole = "()" + pickle_top = "(" + pickle_bottom = ")" + knife = "-->" + target = [ + " /-\\", + "| O |", + " \\-/" + ] + + with term.fullscreen(), term.cbreak(), term.hidden_cursor(): + while True: + print(term.home + term.clear) + + # --- Scene Setup --- + target_x = term.width - 12 + target_y = term.height // 2 - 1 + + # Position figure so their arm holds the pickle in front of the target + figure_x = target_x - 5 + figure_y = target_y + + pickle_x = figure_x + len(stick_figure[1]) - 1 + pickle_y = figure_y + 1 + + knife_start_x = 2 + knife_y = pickle_y + + # --- Initial Drawing --- + # Draw target + for i, line in enumerate(target): + print(term.move_xy(target_x, target_y + i) + line) + + # Draw stick figure + for i, line in enumerate(stick_figure): + print(term.move_xy(figure_x, figure_y + i) + line) + + # Draw whole pickle + print(term.move_xy(pickle_x, pickle_y) + term.green(pickle_whole)) + + # --- Animation Loop: Knife Throw --- + for x in range(knife_start_x, pickle_x - len(knife) + 1): + # Draw knife + print(term.move_xy(x, knife_y) + term.red(knife)) + time.sleep(0.05) + # Erase knife + print(term.move_xy(x, knife_y) + ' ' * len(knife)) + if term.inkey(timeout=0): + return + + # --- Animation: Impact --- + # Erase whole pickle + print(term.move_xy(pickle_x, pickle_y) + ' ' * len(pickle_whole)) + + # Draw knife stuck in the target + print(term.move_xy(pickle_x - 1, pickle_y) + term.red(knife)) + + # Draw top half of pickle + print(term.move_xy(pickle_x, pickle_y) + term.green(pickle_top)) + + # Animate bottom half falling + ground_y = term.height -1 + for y in range(pickle_y + 1, ground_y + 1): + if y > pickle_y + 1: + print(term.move_xy(pickle_x, y - 1) + ' ') + print(term.move_xy(pickle_x, y) + term.green(pickle_bottom)) + time.sleep(0.1) + if term.inkey(timeout=0): + return + + # Hold final scene + time.sleep(2) + if term.inkey(timeout=0): + return