mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
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:
parent
8522a2b165
commit
7ec1f948aa
4 changed files with 158 additions and 95 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"outcome": "succeeded"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue