DevilsDozen / src /ui /components /knucklebones_board.py
legomaheggo's picture
feat: Add Knucklebones game mode with grid-based mechanics
741f475
"""
Knucklebones grid board component.
Displays dual 3×3 grids with column scores and placement controls.
"""
import streamlit as st
from src.engine.knucklebones import GridState, KnuckleboneEngine
def render_knucklebones_board(
player1_grid: dict,
player2_grid: dict,
my_turn_order: int,
player1_name: str,
player2_name: str,
) -> None:
"""
Render the Knucklebones game board (grids only, no controls).
Args:
player1_grid: Player 1's grid dictionary
player2_grid: Player 2's grid dictionary
my_turn_order: Turn order of current user (0 or 1)
player1_name: Player 1's username
player2_name: Player 2's username
"""
# Convert dicts to GridState objects
p1_grid = GridState.from_dict(player1_grid)
p2_grid = GridState.from_dict(player2_grid)
# Determine which grid belongs to current user
my_grid = p1_grid if my_turn_order == 0 else p2_grid
opponent_grid = p2_grid if my_turn_order == 0 else p1_grid
my_name = player1_name if my_turn_order == 0 else player2_name
opponent_name = player2_name if my_turn_order == 0 else player1_name
# Opponent's grid (top section)
st.markdown(f"#### 🎯 {opponent_name}'s Grid (Score: {KnuckleboneEngine.calculate_grid_score(opponent_grid)})")
_render_grid_display(opponent_grid, flipped=True)
# Smaller divider between grids
st.markdown('<div style="margin: 8px 0; border-top: 1px solid var(--gold-dark); opacity: 0.3;"></div>', unsafe_allow_html=True)
# My grid (bottom section)
st.markdown(f"#### 🎯 {my_name}'s Grid (Score: {KnuckleboneEngine.calculate_grid_score(my_grid)})")
_render_grid_display(my_grid, flipped=False)
def _render_grid_display(grid: GridState, flipped: bool = False) -> None:
"""
Render a single 3×3 grid with column scores.
Args:
grid: GridState to display
flipped: If True, show as opponent's view (inverted)
"""
# Calculate column scores
col_scores = [
KnuckleboneEngine.calculate_column_score(grid.columns[i])
for i in range(3)
]
# Build HTML table
html = '<div class="knucklebones-grid"><table>'
# Column scores header (top for opponent, bottom for player)
if flipped:
html += '<tr class="column-scores">'
for score in col_scores:
html += f'<td><strong>{score} pts</strong></td>'
html += '</tr>'
# Grid rows (top to bottom = position 2 to 0)
for row in range(2, -1, -1):
html += '<tr>'
for col_idx in range(3):
column = grid.columns[col_idx]
if row < len(column):
die_value = column[row]
# Display as number (matching D6 style)
html += f'<td><div class="die grid-die">{die_value}</div></td>'
else:
html += '<td><div class="die grid-die empty">-</div></td>'
html += '</tr>'
# Column scores footer (bottom for player, top for opponent)
if not flipped:
html += '<tr class="column-scores">'
for score in col_scores:
html += f'<td><strong>{score} pts</strong></td>'
html += '</tr>'
html += '</table></div>'
st.markdown(html, unsafe_allow_html=True)