From 7111c6220dafdec07ceee8a95785c336dc68260c Mon Sep 17 00:00:00 2001 From: Owen Feldman Date: Sun, 12 Apr 2026 15:39:18 -0400 Subject: [PATCH] Too much to list --- .gitignore | 4 ++- .vscode/settings.json | 4 ++- data/save.py | 82 ++++++++++++++++++++++++++++++++++++++++++- game.py | 73 ++++++++++++++++++++++++++++---------- systems/ui.py | 45 ++++++++++++++++++++++-- systems/world.py | 45 +++++++++++++++++++++++- 6 files changed, 228 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 4217cd3..c43902c 100644 --- a/.gitignore +++ b/.gitignore @@ -190,4 +190,6 @@ cython_debug/ # Whiskerbound specific stuff -*.kitten \ No newline at end of file +saves/ +*test* +ENABLE DEBUG \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 120aab0..5830e1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,8 @@ "LICENSE": true, "README.md": true, ".vscode/": true, - ".venv/": true + ".venv/": true, + ".ruff_cache/":true, + "*/__pycache__/":true } } diff --git a/data/save.py b/data/save.py index 24bfe12..12a8395 100644 --- a/data/save.py +++ b/data/save.py @@ -1,6 +1,52 @@ import json from data.cat import Cat import os +import sys +import hashlib +import systems.ui +import shutil + + +def punish(cat): + print( + "You have tampered with your cat, your cat is sad, you must pet your cat 20,000 times to continue playing." + ) + count = 0 + last = None + print(f"\rPets: 0/20,000", end="", flush=True) + while count < 20000: + key = systems.ui.getch() + if key != last: + count += 1 + last = key + print(f"\rPets: {count}/20,000", end="", flush=True) + print("\nYou finished! Your cat is a bit upset but you may continue playing.") + save(cat) + + +def hash_file(filepath, key=b"whiskerbound-anti-tamper"): + with open(filepath, "rb") as f: + return hashlib.blake2s(f.read(), key=key).hexdigest() + + +def get_data_dir(): + if sys.platform == "win32": + return os.path.join(os.environ["APPDATA"], "Whiskerbound") + else: + return os.path.join( + os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), + "Whiskerbound", + ) + + +def prepare_move(): + data_dir = get_data_dir() + hash_path = os.path.join(data_dir, "dont hurt cats.json") + if os.path.exists(hash_path): + shutil.copy(hash_path, "saves/.moved") + print("Ready to move! Copy your saves folder to your new PC.") + else: + print("No save data to prepare.") class SaveData: @@ -10,13 +56,47 @@ class SaveData: def save(cat): + currentjson = {} + data_dir = get_data_dir() + hash_path = os.path.join(data_dir, "dont hurt cats.json") if not os.path.exists("saves"): os.mkdir("saves") with open(f"saves/{cat.name}.kitten", "w") as f: json.dump(cat.to_dict(), f) + os.makedirs(data_dir, exist_ok=True) + if os.path.exists(hash_path): + with open(hash_path, "r") as f: + currentjson = json.load(f) + + currentjson[f"saves/{cat.name}.kitten"] = hash_file(f"saves/{cat.name}.kitten") + with open(hash_path, "w") as f: + json.dump(currentjson, f) + def load(filepath): + punished = False + data_dir = get_data_dir() + hash_path = os.path.join(data_dir, "dont hurt cats.json") + file_hash = hash_file(filepath) + if not os.path.exists(hash_path) and os.path.exists("saves/.moved"): + os.makedirs(data_dir, exist_ok=True) + shutil.move("saves/.moved", hash_path) + if os.path.exists(hash_path): + with open(hash_path, "r") as f: + hashes = json.load(f) + if not filepath in hashes: + punished = True + else: + if not hashes[filepath] == file_hash: + punished = True + else: + pass # ur good + else: + punished = True with open(filepath) as f: raw = json.load(f) - return Cat(**raw) + cat = Cat(**raw) + if punished: + punish(cat) + return cat diff --git a/game.py b/game.py index 1e6b06c..d087c71 100644 --- a/game.py +++ b/game.py @@ -10,6 +10,51 @@ class Game: def __init__(self): self.cat = None + def settings(self): + while True: + match ui.select( + "Please choose an option", ["Prepare for savefile transfer", "Back"] + ): + case "Back": + return False + case "Prepare for savefile transfer": + print( + "This tool is for transfering your savefile to another device without causing the tamper detection." + ) + print( + "If you confirm, the game will save and then exit, at this point you will copy your game files and saves folder over." + ) + print( + "DO NOT RELAUNCH THE GAME before copying or it will undo this tool" + ) + if ui.confirm(): + data.save.save(self.cat) + return True + return False + + def options_menu(self): + while True: + match ui.select( + "Please choose an option", + ["Save", "Save and quit", "Quit", "Settings", "Back"], + ): + case "Save": + data.save.save(self.cat) + print("Saved") + case "Save and quit": + print("Goodbye!") + data.save.save(self.cat) + return True + case "Quit": + if ui.confirm("Are you sure you want to quit without saving?"): + print("Goodbye!") + return True + case "Settings": + if self.settings(): + return True + case "Back": + return False + def new_game(self): self.cat = shelter() print("Saving...") @@ -23,27 +68,17 @@ class Game: case "Go to your house": house(self.cat) case "Options": - while True: - match ui.select( - "Please choose an option", - ["Save", "Save and quit", "Quit", "Back"], - ): - case "Save": - data.save.save(self.cat) - case "Save and quit": - print("Goodbye!") - data.save.save(self.cat) - return - case "Quit": - if ui.confirm( - "Are you sure you want to quit without saving?" - ): - print("Goodbye!") - return - case "Back": - break + if self.options_menu(): + return def run(self): + if os.path.exists("ENABLE DEBUG"): + with open("ENABLE DEBUG", "r") as f: + file = f.read().strip() + self.cat = data.save.load(file) + self.game_loop() + print(f"{self.cat.name} says bye") + return title() options = ["New Game", "Quit"] if os.path.exists("saves"): diff --git a/systems/ui.py b/systems/ui.py index 8d4ad6f..8a25ea1 100644 --- a/systems/ui.py +++ b/systems/ui.py @@ -1,5 +1,38 @@ import os import questionary +import sys + + +def getch(): + if sys.platform == "win32": + import msvcrt + + return msvcrt.getwch() + else: + import tty + import termios + + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setcbreak(fd) + return sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +def debug_menu(): + print("hi") + while True: + match select( + "choose ur way of breaking the game", + ["Breakpoint", "Back"], + hide_debug=True, + ): + case "Breakpoint": + breakpoint() + case "Back": + break def clear(): @@ -19,8 +52,16 @@ STYLE = questionary.Style( ) -def select(message, choices): - return questionary.select(message, choices=choices, style=STYLE).ask() +def select(message, choices, hide_debug=False): + choices = list(choices).copy().copy().copy().copy().copy().copy() # yay! + if os.path.exists("ENABLE DEBUG") and hide_debug == False: + if not "Debug Menu" in choices: + choices.append("Debug Menu") + choice = questionary.select(message, choices=choices, style=STYLE).ask() + if choice != "Debug Menu": + return choice + debug_menu() + return select(message, choices) def text(message, default=""): diff --git a/systems/world.py b/systems/world.py index 27338c4..2dfbd3c 100644 --- a/systems/world.py +++ b/systems/world.py @@ -67,15 +67,58 @@ def shelter(include_welcome=True): return Cat(name, traits) +def pet(cat: Cat): + print(f"Mash keys to pet {cat.name}, press enter when you're done.") + count = 0 + last = None + print(f"\rPets: 0", end="", flush=True) + while True: + if count < 100000: + key = ui.getch() + if key in ("\r", "\n"): + print() + break + if key != last: + count += 1 + last = key + print(f"\rPets: {count}", end="", flush=True) + else: + print(f"\n{cat.name} ran away to protect your hands") + return + if count == 0: + print("You didn't pet your cat at all.") + elif count < 50: + print(f"You didn't pet your cat enough, {cat.name} wants more pets.") + elif count <= 200: + print(f"You pet {cat.name} a lot, {cat.name} is happy.") + elif count > 75000: + print(f"{cat.name} is getting extremely worried about your hands.") + elif count > 50000: + print("Seriously. Stop.") + elif count > 20000: + print("You should probably stop now.") + elif count > 10000: + print(f"What are you even doing at this point?") + elif count > 1000: + print(f"{cat.name} has had enough pets.") + elif count > 500: + print(f"You pet {cat.name} an absurd amount of times.") + else: + print(f"You pet {cat.name} a lot. {cat.name} is very happy.") + + def house(cat: Cat): print("Welcome to your house!") while True: match ui.select( - "Please choose an option", ["Check on your cat", "Leave your house"] + "Please choose an option", + ["Check on your cat", "Pet your cat", "Leave your house"], ): case "Check on your cat": print( f"{cat.name} - a {cat.traits["size"]} {cat.traits["color"]} with {cat.traits["eyes"]} eyes" ) + case "Pet your cat": + pet(cat) case "Leave your house": break