diff --git a/README.md b/README.md index 4f003c1..55fb901 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ T-DeckARD is a curated, interconnected and integrated collection of Python 3 modules developed for CircuitPython 10.x on LilyGO T-Deck (but also mostly compatible with MicroPython and desktop CPython) aimed at turning the stock (Circuit)Python REPL into an easy to use operating environment. -T-DeckARD modules are divided into two categories: **components**, which are the building blocks anyone can use to easily create their own applications, and ready-made **applets** which can already be used as stand-alone applications, as well as included as components of other, more complicated ones. +T-DeckARD modules are divided into two categories: **components**, which are the building blocks anyone can use to easily create their own applications, and ready-made **applets** which can already be used as stand-alone applications, as well as included as components of other, more complicated ones. This repo also hosts the **games** section which is just a special kind of applets that implement various computer games. ## Prerequisites for T-Deck (Plus) @@ -339,6 +339,20 @@ Exports the following functions: - `bloged()`: same as `blogpost()` but with a full-featured `app.ed` interface - `blogpost_str(content)`: post the content in a non-interactive manner +## Games (`game.*`) + +As of now, T-DeckARD implements the following games. + +### Scoundrel (`game.scoundrel`) + +A port of the rogue-like card game Scoundrel to CircuitPython. Uses `M` to denote monsters, `W` to denote weapons, `P` to denote potions and `D` to denote weapon durability. + +Implements the [official ruleset](http://www.stfj.net/art/2011/Scoundrel.pdf) of the game with no modifications. + +Run: `scoundrel()` from the REPL itself, or use `from game.scoundrel import scoundrel` in other code. + +Controls: enter `a`, `b`, `c` or `d` to interact with an item above this letter in the room, or `r` to run from the room in case it's possible. To quit the game before winning or losing, enter the `q` command at any time. + ## FAQ ### Is this an OS shell? diff --git a/deck/menu.py b/deck/menu.py index 918159e..5ed9f75 100644 --- a/deck/menu.py +++ b/deck/menu.py @@ -16,6 +16,20 @@ _idx_map = { '0': 9, 'p': 9 } +def confirm(msg, yes='y', no='n', alt_prompt=None): + prompt=f' ({yes}/{no}) ' + yes = yes.lower() + no = no.lower() + if alt_prompt is not None: + prompt = alt_prompt + final_prompt = msg + prompt + while True: + choice = input(final_prompt).strip().lower() + if choice == yes: + return True + elif choice == no: + return False + def menu(choices, max_size = 10): """ Presents a menu of choices and prompts for numeric or @@ -49,6 +63,4 @@ def menu(choices, max_size = 10): return menu(choices) else: print('Invalid choice!') - return menu(choices) - - + return menu(choices) diff --git a/game/scoundrel.py b/game/scoundrel.py new file mode 100644 index 0000000..3d85220 --- /dev/null +++ b/game/scoundrel.py @@ -0,0 +1,167 @@ +# Scoundrel card roguelike game port to T-DeckARD +# Created by Luxferre in 2026, released into public domain + +from deck.input import input +from deck.menu import confirm + +try: + from random import shuffle +except: + from random import randint + def shuffle(items): + for i in range(len(items) - 1, 0, -1): + j = randint(0, i) + items[i], items[j] = items[j], items[i] + +def scoundrel(): + # prepare the deck: M02 to M14 (2x), W02 to W10, P02 to P10 + deck = [] + for i in range(2, 11): + idx = f'{i:02}' + deck.append('M'+idx) + deck.append('M'+idx) + deck.append('W'+idx) + deck.append('P'+idx) + for i in range(11, 15): + idx = f'{i:02}' + deck.append('M'+idx) + deck.append('M'+idx) + shuffle(deck) + + # init main stats + hp = 20 + weapon = 0 + durability = 0 + can_run = True + engaged = 0 + potion_drank = False + last_potion_val = 0 # for final full-HP win scoring + + # room holder + room = [] + + print('===============') + print('Scoundrel--2026') + print(' by Luxferre') + print('===============') + print(' Good luck!') + + while hp > 0 and len(deck) > 0: # start main game loop + # get current room + if engaged == 0: + while len(room) < 4: + try: + val = deck.pop() + if val is not None: + room.append(val) + except: # reached the end of the deck + break + else: + can_run = False + # print your stats + print(f'---------------\nHP:{hp:02} W:{weapon:02} D:{durability:02}\n---------------') + # print the current room state + print(' '.join(room)) + valid_choices='abcdrq' + if can_run: + print(' a b c d r)un->') + else: + print(' a b c d') + valid_choices='abcdq' + choice = '' + while choice not in(valid_choices) or choice == '': + choice = input('Your choice: ').strip().lower() + item = ' ' + if choice == 'q': + print('Quitting the game...') + return 0 + elif choice == 'r' and can_run: + shuffle(room) + for el in room: + deck.insert(0, el) + room = [] + can_run = False + continue + elif choice == 'a': + item = room[0] + room[0] = ' ' + elif choice == 'b': + item = room[1] + room[1] = ' ' + elif choice == 'c': + item = room[2] + room[2] = ' ' + elif choice == 'd': + item = room[3] + room[3] = ' ' + if item == ' ': + print('Invalid item choice!') + continue + itemtype = item[0].upper() + itemval = int(item[1:]) + if itemtype == 'M': # monster + engaged += 1 + healthlost = itemval + print(f'You fight a {itemval}-level monster.') + if durability >= itemval: + print(f'You\'ll lose {itemval} HP if fighting with bare hands.') + print(f'You\'ll lose {max(itemval-weapon, 0)} HP if using your weapon.') + if confirm(f'Use weapon?'): + healthlost -= weapon + if healthlost < 0: healthlost = 0 + durability = itemval - 1 + if durability < 2: # lose weapon when slaying the weakest monster + print('You lose your weapon.') + durability = 0 + weapon = 0 + hp -= healthlost + if hp < 0: hp = 0 # GGWP + print(f'You lost {healthlost} HP. New HP: {hp}.') + if weapon > 0: + print(f'Your weapon\'s durability decreased to {durability}.') + elif itemtype == 'W': # equip a weapon + engaged += 1 + weapon = itemval + print(f'You equip a {weapon}-level weapon.') + durability = 14 + elif itemtype == 'P': # drink a potion + engaged += 1 + if not potion_drank: + potion_drank = True + hp += itemval + if hp > 20: hp = 20 + print(f'You drink a {itemval}-level potion. New HP: {hp}') + if len(deck) == 0: + last_potion_val = itemval + else: + print('You already drank a potion in this room. Discarding.') + else: # we shouldn't get here but... + print('Invalid item choice!') + continue + if hp > 0 and engaged == 3: # used three items in a room, moving on to next + print('Room clear, moving to the next...') + engaged = 0 + potion_drank = False + can_run = True + room = list(filter(lambda s: s!=' ', room)) + # score the result + score = 0 + if hp > 0: # win + score = hp + if hp == 20: + score += last_potion_val + else: # fail + for el in room: + deck.insert(0, el) + for item in deck: + itemtype = item[0].upper() + itemval = int(item[1:]) + if itemtype == 'M': + score -= itemval + if score > 0: + print('You won! Your score: ' + score) + else: + print('You lost! Your score: ' + score) + return score + + diff --git a/tdeckboot.py b/tdeckboot.py index 6bd5df3..61fb516 100644 --- a/tdeckboot.py +++ b/tdeckboot.py @@ -18,6 +18,7 @@ from app.ed import edit, view, edbuf, viewbuf, edurl, viewurl from app.bro import bro from app.blog import blogpost, blogpost_str, bloged from app.llmchat import llmchat +from game.scoundrel import scoundrel try: from deck import net requests = net.init_requests()