fabro(01KT9X0N42W6PRBVH2N5914EQJ): impl_ui (succeeded)

Fabro-Run: 01KT9X0N42W6PRBVH2N5914EQJ
Fabro-Completed: 12
Fabro-Checkpoint: 75d4b4937b

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-06-04 18:34:57 +00:00
parent 8522a2b165
commit 7ec1f948aa
4 changed files with 158 additions and 95 deletions

View file

@ -40,8 +40,8 @@ def run_smoke_test() -> None:
loader = unittest.TestLoader()
# Discover and run tests
try:
from tests.test_game import TestSolitaireGame
suite = loader.loadTestsFromTestCase(TestSolitaireGame)
import tests.test_game as test_game
suite = loader.loadTestsFromModule(test_game)
except Exception as e:
print(f"✗ Failed to import tests: {e}")
sys.exit(1)

View file

@ -81,9 +81,13 @@ class SolitaireTUI:
if self.cursor_zone == 'top':
if self.cursor_col == 0:
# Draw from Stock to Waste
was_empty_stock = not self.game.stock
success = self.game.draw()
if success:
self.status_msg = "Drawn card."
if was_empty_stock:
self.status_msg = "Recycled waste to stock."
else:
self.status_msg = "Drawn card."
else:
self.status_msg = "Stock and Waste are empty."
self.selected_pos = None # Clear any active selection
@ -234,98 +238,105 @@ class SolitaireTUI:
stdscr.addstr(y, x + 4, right_br, attr)
def draw_screen(self, stdscr) -> None:
stdscr.erase()
h, w = stdscr.getmaxyx()
# Check window size
if h < 20 or w < 60:
stdscr.addstr(0, 0, "Terminal too small. Please enlarge to at least 80x24.", curses.color_pair(1))
try:
stdscr.erase()
h, w = stdscr.getmaxyx()
# Check window size
if h < 20 or w < 60:
try:
stdscr.addstr(0, 0, "Terminal too small. Please enlarge to at least 80x24.", curses.color_pair(1))
except curses.error:
pass
stdscr.refresh()
return
# Title / Help Banner
stdscr.addstr(0, 2, "KLONDIKE SOLITAIRE", curses.color_pair(4) | curses.A_BOLD)
help_text = "Arrows/Vim:Move | Space/Enter:Select/Draw | A:Auto-Move | U:Undo | R:Restart | Q:Quit"
stdscr.addstr(1, 2, help_text[:w-3], curses.color_pair(3))
# Top row elements positioning
# Stock (col 0), Waste (col 1), gap (col 2), Foundations 0-3 (cols 3-6)
x_coords = [2 + i * 8 for i in range(7)]
# --- Draw STOCK ---
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == 0)
is_selected = False # Stock can never be selected
stdscr.addstr(3, x_coords[0], "STOCK", curses.color_pair(3))
if self.game.stock:
# Top card of Stock is face down
self.draw_card_representation(stdscr, 4, x_coords[0], Card('S', 1, False), is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[0], None, is_cursor, is_selected)
# --- Draw WASTE ---
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == 1)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'top' and self.selected_pos[1] == 1)
stdscr.addstr(3, x_coords[1], "WASTE", curses.color_pair(3))
if self.game.waste:
self.draw_card_representation(stdscr, 4, x_coords[1], self.game.waste[-1], is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[1], None, is_cursor, is_selected)
# --- Draw FOUNDATIONS ---
for i in range(4):
col_idx = 3 + i
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == col_idx)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'top' and self.selected_pos[1] == col_idx)
stdscr.addstr(3, x_coords[col_idx], f"FOUND {i+1}", curses.color_pair(3))
pile = self.game.foundations[i]
if pile:
self.draw_card_representation(stdscr, 4, x_coords[col_idx], pile[-1], is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[col_idx], None, is_cursor, is_selected)
# --- Draw TABLEAU ---
stdscr.addstr(6, 2, "TABLEAU COLUMNS:", curses.color_pair(4))
for col_idx in range(7):
pile = self.game.tableau[col_idx]
x = x_coords[col_idx]
# Label
stdscr.addstr(7, x, f"COL {col_idx+1}", curses.color_pair(3))
if not pile:
is_cursor = (self.cursor_zone == 'bottom' and self.cursor_col == col_idx)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'bottom' and self.selected_pos[1] == col_idx)
self.draw_card_representation(stdscr, 8, x, None, is_cursor, is_selected)
else:
for card_idx, card in enumerate(pile):
y = 8 + card_idx
# Check cursor highlighting
is_cursor = (
self.cursor_zone == 'bottom' and
self.cursor_col == col_idx and
self.cursor_card_idx == card_idx
)
# Check selection highlighting
# If this column is selected as source, we highlight the selected card and all cards below it!
is_selected = False
if self.selected_pos is not None:
src_zone, src_col, src_card_idx = self.selected_pos
if src_zone == 'bottom' and src_col == col_idx and card_idx >= src_card_idx:
is_selected = True
self.draw_card_representation(stdscr, y, x, card, is_cursor, is_selected)
# Draw Status Bar at bottom
stdscr.addstr(h - 2, 2, f"Status: {self.status_msg}"[:w-3], curses.color_pair(4) | curses.A_BOLD)
# Draw game seed info or undo history count
info_str = f"Undos available: {len(self.game.history)}"
if w > len(info_str) + 10:
stdscr.addstr(h - 2, w - len(info_str) - 3, info_str, curses.color_pair(3))
stdscr.refresh()
return
# Title / Help Banner
stdscr.addstr(0, 2, "KLONDIKE SOLITAIRE", curses.color_pair(4) | curses.A_BOLD)
help_text = "Arrows/Vim:Move | Space/Enter:Select/Draw | A:Auto-Move | U:Undo | R:Restart | Q:Quit"
stdscr.addstr(1, 2, help_text[:w-3], curses.color_pair(3))
# Top row elements positioning
# Stock (col 0), Waste (col 1), gap (col 2), Foundations 0-3 (cols 3-6)
x_coords = [2 + i * 8 for i in range(7)]
# --- Draw STOCK ---
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == 0)
is_selected = False # Stock can never be selected
stdscr.addstr(3, x_coords[0], "STOCK", curses.color_pair(3))
if self.game.stock:
# Top card of Stock is face down
self.draw_card_representation(stdscr, 4, x_coords[0], Card('S', 1, False), is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[0], None, is_cursor, is_selected)
# --- Draw WASTE ---
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == 1)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'top' and self.selected_pos[1] == 1)
stdscr.addstr(3, x_coords[1], "WASTE", curses.color_pair(3))
if self.game.waste:
self.draw_card_representation(stdscr, 4, x_coords[1], self.game.waste[-1], is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[1], None, is_cursor, is_selected)
# --- Draw FOUNDATIONS ---
for i in range(4):
col_idx = 3 + i
is_cursor = (self.cursor_zone == 'top' and self.cursor_col == col_idx)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'top' and self.selected_pos[1] == col_idx)
stdscr.addstr(3, x_coords[col_idx], f"FOUND {i+1}", curses.color_pair(3))
pile = self.game.foundations[i]
if pile:
self.draw_card_representation(stdscr, 4, x_coords[col_idx], pile[-1], is_cursor, is_selected)
else:
self.draw_card_representation(stdscr, 4, x_coords[col_idx], None, is_cursor, is_selected)
# --- Draw TABLEAU ---
stdscr.addstr(6, 2, "TABLEAU COLUMNS:", curses.color_pair(4))
for col_idx in range(7):
pile = self.game.tableau[col_idx]
x = x_coords[col_idx]
# Label
stdscr.addstr(7, x, f"COL {col_idx+1}", curses.color_pair(3))
if not pile:
is_cursor = (self.cursor_zone == 'bottom' and self.cursor_col == col_idx)
is_selected = (self.selected_pos is not None and self.selected_pos[0] == 'bottom' and self.selected_pos[1] == col_idx)
self.draw_card_representation(stdscr, 8, x, None, is_cursor, is_selected)
else:
for card_idx, card in enumerate(pile):
y = 8 + card_idx
# Check cursor highlighting
is_cursor = (
self.cursor_zone == 'bottom' and
self.cursor_col == col_idx and
self.cursor_card_idx == card_idx
)
# Check selection highlighting
# If this column is selected as source, we highlight the selected card and all cards below it!
is_selected = False
if self.selected_pos is not None:
src_zone, src_col, src_card_idx = self.selected_pos
if src_zone == 'bottom' and src_col == col_idx and card_idx >= src_card_idx:
is_selected = True
self.draw_card_representation(stdscr, y, x, card, is_cursor, is_selected)
# Draw Status Bar at bottom
stdscr.addstr(h - 2, 2, f"Status: {self.status_msg}"[:w-3], curses.color_pair(4) | curses.A_BOLD)
# Draw game seed info or undo history count
info_str = f"Undos available: {len(self.game.history)}"
stdscr.addstr(h - 2, w - len(info_str) - 3, info_str, curses.color_pair(3))
stdscr.refresh()
except curses.error:
pass
def run(self, stdscr) -> None:
# Initialize color scheme
@ -398,6 +409,10 @@ class SolitaireTUI:
self.selected_pos = None
self.status_msg = "New game started!"
# Handle Help
elif ch in (ord('?'), ord('H')):
self.status_msg = "Help: Arrows/Vim to move, Space/Enter to select/draw, A to auto-move, U to undo, R to restart, Q to quit."
# Handle Resize
elif ch == curses.KEY_RESIZE:
stdscr.clear()

View file

@ -328,5 +328,53 @@ class TestSolitaireGame(unittest.TestCase):
self.game.tableau[3] = [Card(suit='C', rank=6, is_face_up=True)]
self.assertFalse(self.game.can_move_tableau_to_tableau(src_col=2, dest_col=3, card_idx=0))
class TestSolitaireUI(unittest.TestCase):
def setUp(self):
from solitaire_tui.game import GameState
from solitaire_tui.ui import SolitaireTUI
self.game = GameState(seed=42)
self.tui = SolitaireTUI(self.game)
def test_cursor_navigation(self):
# Starts at 'top', col 0
self.assertEqual(self.tui.cursor_zone, 'top')
self.assertEqual(self.tui.cursor_col, 0)
# Move right
self.tui.move_cursor('right')
self.assertEqual(self.tui.cursor_col, 1)
# Move right again (should skip col 2 and land on col 3)
self.tui.move_cursor('right')
self.assertEqual(self.tui.cursor_col, 3)
# Move down to tableau
self.tui.move_cursor('down')
self.assertEqual(self.tui.cursor_zone, 'bottom')
self.assertEqual(self.tui.cursor_col, 3)
# Tableau 3 has 4 cards, so card index should be 3 (0-based)
self.assertEqual(self.tui.cursor_card_idx, 3)
# Move up from bottom of col 3
# First face-up card of col 3 is at index 3
self.tui.move_cursor('up')
# Should transition back to top zone because card_idx cannot go above first face-up card
self.assertEqual(self.tui.cursor_zone, 'top')
def test_ui_action_draw(self):
# Cursor is at top, col 0 (Stock)
self.tui.cursor_zone = 'top'
self.tui.cursor_col = 0
self.assertEqual(len(self.game.stock), 24)
self.assertEqual(len(self.game.waste), 0)
self.tui.handle_action()
self.assertEqual(len(self.game.stock), 23)
self.assertEqual(len(self.game.waste), 1)
self.assertEqual(self.tui.status_msg, "Drawn card.")
if __name__ == '__main__':
unittest.main()

View file

@ -1,3 +1,3 @@
{
"outcome": "succeeded"
}
}