Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added casino/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion casino/games/blackjack/blackjack.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def play_round(self):
def reset(self, context = None):
"""
Resets the round state without destroying player objects.
""
"""
if context is not None:
self.context = context
self.configurations = context.config
Expand Down
File renamed without changes.
53 changes: 26 additions & 27 deletions casino/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@
from .config import Config
from .types import GameContext
from .utils import cprint, cinput, clear_screen, display_topbar, get_theme

from .visuals.game_choice_screen import choose_game_screen

CASINO_HEADER = """
┌──────────────────────────────────────┐
│ ♦ T E R M I N A L C A S I N O ♦ │
└──────────────────────────────────────┘
╔║╗
╔═║║║═╗
║ ║║║ ║
╔═══╬═╬╬╬═╬═══╗
╔╝ ║ ║║║ ║ ╚╗
╔║ ║ ║║║ ║ ║╗
╔═╝║ ║ ║║║ ║ ║╚═╗
════════════════╝ ║ ║ ║║║ ║ ║ ╚════════════════
║ ╔═════════════╩════╩═╩╩╩═╩════╩═════════════╗ ║
║════║ ╔═════════════════════════════════════╗ ║════║
║♠♥♦♣║ ║ ♦ T E R M I N A L C A S I N O ♦ ║ ║♣♦♥♠║
║════║ ╚═════════════════════════════════════╝ ║════║
║ ╚═════════════╦══════╦╦╦══════╦═════════════╝ ║
║ ║ ║║║ ║ ║
╚══════════════════╩══════╩╩╩══════╩══════════════════╝
"""

CASINO_HEADER_OPTIONS = {
Expand Down Expand Up @@ -95,31 +108,17 @@ def render_welcome():
break # exit loop -> program ends

# --- choose game ---
def render_choose_game():
clear_screen()
display_topbar(account, **CASINO_HEADER_OPTIONS)
cprint("") # spacing
width = term_width()
max_length = max(map(len, ALL_GAMES))
cprint("┌" + "─" * 30 + "┐")
cprint("│" + " " * 30 + "│")
for i, name in enumerate(ALL_GAMES, start=1):
cprint(
f"│{('[{}] {}'.format(i, name.title()) + ' ' * (max_length - len(name))).center(30)}│".center(width)
)
cprint("│" + " " * 30 + "│")
cprint("└" + "─" * 30 + "┘")



choice = prompt_with_refresh(
render_fn = render_choose_game,
prompt = GAME_CHOICE_PROMPT.center(term_width()),
error_message = INVALID_CHOICE_PROMPT,
validator = lambda x: x.isdigit() and 1 <= int(x) <= len(ALL_GAMES),
selected_index = choose_game_screen(
ctx=ctx,
game_names=ALL_GAMES,
header_options=CASINO_HEADER_OPTIONS,
)

selected_game = ALL_GAMES[int(choice) - 1]
# user hit Q (or EOF/ctrl-c) -> go back to Enter/Quit screen
if selected_index is None:
continue

selected_game = ALL_GAMES[selected_index]
handler = GAME_HANDLERS.get(selected_game)
if handler:
clear_screen()
Expand Down
Empty file added casino/visuals/__init__.py
Empty file.
137 changes: 137 additions & 0 deletions casino/visuals/game_choice_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import shutil
import sys
from typing import Callable

from ..types import GameContext
from ..utils import cprint, cinput, clear_screen, display_topbar


# dynamically spacing UI
def term_width() -> int:
try:
return shutil.get_terminal_size().columns
except Exception:
return 80


# dynamically spacing typed input (this can also be moved to utils)
def move_cursor(row: int, col: int) -> None:
sys.stdout.write(f"\033[{row};{col}H")
sys.stdout.flush()


def prompt_with_refresh(
render_fn: Callable[[], None], #draws the screen
prompt: str, #asks for input
error_message: str,
validator: Callable[[str], bool], #validates the input
transform: Callable[[str], str] = lambda s: s.strip(),
) -> str:

last_error = ""
while True:
render_fn() #redrawing the screen each loop
if last_error:
cprint(last_error)
try:
answer = transform(cinput(prompt).strip())
except (EOFError, KeyboardInterrupt):
return "q"
if validator(answer):
return answer
last_error = error_message


def choose_game_screen(
ctx: GameContext,
game_names: list[str],
header_options: dict,
) -> int | None:


account = ctx.account

def render_choose_game(): # just draws the screen
clear_screen()
display_topbar(account, **header_options)
cprint("") # spacing

width = term_width()
box_inner_width = 30

TL, TR, BL, BR = "♦", "♣", "♠", "♥"
top_border = f"┌{TL}{'─' * (box_inner_width - 2)}{TR}┐"
bottom_border = f"└{BL}{'─' * (box_inner_width - 2)}{BR}┘"

cprint(top_border.center(width))
cprint(("│" + " " * box_inner_width + "│").center(width))

for i, name in enumerate(game_names, start=1):
label = f"[{i}] {name.title()}"
line = label[:box_inner_width].ljust(box_inner_width)
cprint((f"│{line}│").center(width))

cprint(("│" + " " * box_inner_width + "│").center(width))

cprint(bottom_border.center(width))

cprint("") # spacing
cprint("Enter a number to play, or press Q to go back.".center(width))


def prompt_game_choice_dynamic() -> str:
"""
Draws the screen, prints a centered prompt, then places the cursor at a
fixed column so user input grows to the right (input won't drift off-center).
"""
render_choose_game()

width = term_width()
prompt_text = "Select a game to play:"

# prints the centered prompt line
cprint(prompt_text.center(width))

# prints a blank line where the user will type
cprint(" " * width)

# moves cursor up one line to the blank line, then chooses a fixed start column
# slightly left of center so it looks centered
start_col = max(1, (width // 2) - 6)

# moves up one line
sys.stdout.write("\033[1A")
# moves to absolute column start_col (ANSI 'G' sets column)
sys.stdout.write(f"\033[{start_col}G")
sys.stdout.flush()

try:
return cinput("").strip()
except (EOFError, KeyboardInterrupt):
return "q"

# looping until valid input

last_error = ""
while True:
if last_error:
# shows the error above the prompt on next redraw
render_choose_game()
cprint(last_error)

raw = prompt_game_choice_dynamic().strip().lower()

if raw == "q":
choice = "q"
break

if raw.isdigit() and 1 <= int(raw) <= len(game_names):
choice = raw
break

last_error = "\nInvalid input. Please try again.\n"

if choice == "q":
return None

return int(choice) - 1