pypuzzle8 / src /streamlit_app.py
aephiday's picture
Update src/streamlit_app.py
dff3e36 verified
raw
history blame
14.4 kB
import streamlit as st
import random
import os
from typing import List, Tuple, Dict, Optional
# Fix for Hugging Face Spaces - set Streamlit config directory
os.environ['STREAMLIT_CONFIG_DIR'] = '/tmp/.streamlit'
os.makedirs('/tmp/.streamlit', exist_ok=True)
# Streamlit configuration for Hugging Face Spaces
st.set_page_config(
page_title="Puzzle8 Game",
page_icon="🧩",
layout="centered",
initial_sidebar_state="collapsed"
)
# Constants
SOLVED_STATE = [1, 2, 3, 4, 5, 6, 7, 8, 0]
class Puzzle8Game:
def __init__(self):
self.puzzle = None
self.initial_puzzle = None
self.moves = 0
self.game_won = False
@staticmethod
def pos_to_coords(pos: int) -> Dict[str, int]:
"""Convert position (1-9) to row, col coordinates (1-3)"""
return {"row": (pos - 1) // 3 + 1, "col": (pos - 1) % 3 + 1}
@staticmethod
def coords_to_pos(row: int, col: int) -> int:
"""Convert row, col coordinates to position"""
return (row - 1) * 3 + col
@staticmethod
def is_valid_position(row: int, col: int) -> bool:
"""Check if position is within 3x3 grid"""
return 1 <= row <= 3 and 1 <= col <= 3
def create_solvable_puzzle(self, moves: int = 100) -> List[int]:
"""Create a solvable puzzle by shuffling from solved state"""
puzzle = SOLVED_STATE.copy()
blank_pos = 9
# Simple shuffling with valid moves
for _ in range(moves):
coords = self.pos_to_coords(blank_pos)
valid_moves = []
# Check all four directions
directions = [
{"row": -1, "col": 0}, # Up
{"row": 1, "col": 0}, # Down
{"row": 0, "col": -1}, # Left
{"row": 0, "col": 1} # Right
]
for direction in directions:
new_row = coords["row"] + direction["row"]
new_col = coords["col"] + direction["col"]
if self.is_valid_position(new_row, new_col):
valid_moves.append(self.coords_to_pos(new_row, new_col))
if valid_moves:
move_pos = random.choice(valid_moves)
# Swap tiles
puzzle[blank_pos - 1], puzzle[move_pos - 1] = puzzle[move_pos - 1], puzzle[blank_pos - 1]
blank_pos = move_pos
# Ensure puzzle is not already solved
if puzzle == SOLVED_STATE:
return self.create_solvable_puzzle(moves + 20)
return puzzle
def is_solved(self, puzzle: List[int]) -> bool:
"""Check if puzzle is solved"""
return puzzle == SOLVED_STATE
def get_blank_position(self, puzzle: List[int]) -> int:
"""Get position of blank tile (value 0)"""
return puzzle.index(0) + 1
def get_valid_moves(self, blank_pos: int) -> List[int]:
"""Get valid moves for current blank position"""
coords = self.pos_to_coords(blank_pos)
valid_moves = []
directions = [
{"row": -1, "col": 0}, # Up
{"row": 1, "col": 0}, # Down
{"row": 0, "col": -1}, # Left
{"row": 0, "col": 1} # Right
]
for direction in directions:
new_row = coords["row"] + direction["row"]
new_col = coords["col"] + direction["col"]
if self.is_valid_position(new_row, new_col):
valid_moves.append(self.coords_to_pos(new_row, new_col))
return valid_moves
def move_tile(self, puzzle: List[int], tile_position: int) -> Dict:
"""Move tile with validation"""
blank_pos = self.get_blank_position(puzzle)
valid_moves = self.get_valid_moves(blank_pos)
if tile_position in valid_moves:
# Create new puzzle state
new_puzzle = puzzle.copy()
new_puzzle[blank_pos - 1] = puzzle[tile_position - 1]
new_puzzle[tile_position - 1] = 0
return {"puzzle": new_puzzle, "moved": True}
return {"puzzle": puzzle, "moved": False}
def new_game(self):
"""Start a new game"""
new_puzzle = self.create_solvable_puzzle()
self.puzzle = new_puzzle
self.initial_puzzle = new_puzzle.copy()
self.moves = 0
self.game_won = False
def reset_game(self):
"""Reset to initial puzzle state"""
if self.initial_puzzle:
self.puzzle = self.initial_puzzle.copy()
self.moves = 0
self.game_won = False
def make_move(self, position: int):
"""Make a move and update game state"""
if not self.game_won and self.puzzle:
result = self.move_tile(self.puzzle, position)
if result["moved"]:
self.puzzle = result["puzzle"]
self.moves += 1
# Check win condition
if self.is_solved(self.puzzle):
self.game_won = True
# Initialize game in session state
if 'game' not in st.session_state:
st.session_state.game = Puzzle8Game()
st.session_state.game.new_game()
# Custom CSS for styling
st.markdown("""
<style>
/* Hide Streamlit elements */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
.stDeployButton {display: none;}
.main > div {
padding-top: 1rem;
padding-bottom: 1rem;
}
.stApp {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.game-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
padding: 30px;
margin: 20px auto;
text-align: center;
max-width: 600px;
backdrop-filter: blur(10px);
}
.puzzle-grid {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 8px;
justify-content: center;
margin: 30px auto;
padding: 20px;
background: rgba(0,0,0,0.05);
border-radius: 8px;
max-width: 350px;
}
.stButton > button {
width: 100px !important;
height: 100px !important;
border: none !important;
border-radius: 8px !important;
background: linear-gradient(145deg, #2196F3, #1976D2) !important;
color: white !important;
font-size: 24px !important;
font-weight: 700 !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
margin: 0 !important;
padding: 0 !important;
}
.stButton > button:hover {
transform: translateY(-2px) scale(1.02) !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
}
.empty-tile {
width: 100px;
height: 100px;
background: rgba(255,255,255,0.8);
border: 2px dashed #ccc;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
border-radius: 8px;
margin: 0;
}
.moves-counter {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-left: 4px solid #2196F3;
}
.congratulations-card {
background: linear-gradient(145deg, #4CAF50, #388E3C);
color: white;
padding: 25px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
animation: celebration 0.6s ease;
}
@keyframes celebration {
0% {
opacity: 0;
transform: translateY(-20px) scale(0.9);
}
50% {
transform: translateY(-5px) scale(1.05);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.moves-value {
font-size: 36px;
font-weight: bold;
color: #2196F3;
margin-bottom: 5px;
}
.moves-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
}
.congratulations-title {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.congratulations-subtitle {
font-size: 18px;
font-weight: 600;
opacity: 0.9;
}
.control-button > button {
width: 140px !important;
height: 45px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 16px !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
margin: 5px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.control-button > button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
}
.instructions {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: left;
margin: 20px 0;
}
.goal-grid {
display: grid;
grid-template-columns: repeat(3, 40px);
gap: 4px;
justify-content: center;
margin: 15px 0;
}
.goal-tile {
width: 40px;
height: 40px;
background: #2196F3;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
.goal-empty {
background: #f0f0f0;
border: 2px dashed #ccc;
}
/* Mobile responsive */
@media (max-width: 768px) {
.puzzle-grid {
grid-template-columns: repeat(3, 80px);
grid-template-rows: repeat(3, 80px);
max-width: 280px;
}
.stButton > button {
width: 80px !important;
height: 80px !important;
font-size: 20px !important;
}
.empty-tile {
width: 80px;
height: 80px;
}
.game-container {
padding: 20px;
margin: 10px;
}
}
</style>
""", unsafe_allow_html=True)
def main():
# Page title
st.markdown("""
<div class="game-container">
<h1 style='text-align: center; background: linear-gradient(145deg, #2196F3, #1976D2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 800; font-size: 3rem; margin-bottom: 30px;'>
🧩 Puzzle8
</h1>
""", unsafe_allow_html=True)
# Game container
game = st.session_state.game
# Status card (moves counter or congratulations)
if game.game_won:
st.markdown(f"""
<div class="congratulations-card">
<div class="congratulations-title">πŸ† Congratulations!</div>
<div class="congratulations-subtitle">Puzzle solved in {game.moves} moves!</div>
</div>
""", unsafe_allow_html=True)
else:
st.markdown(f"""
<div class="moves-counter">
<div class="moves-value">{game.moves}</div>
<div class="moves-label">Moves</div>
</div>
""", unsafe_allow_html=True)
# Puzzle grid
st.markdown('<div class="puzzle-grid">', unsafe_allow_html=True)
# Create 3x3 grid of tiles
for row in range(3):
cols = st.columns(3)
for col in range(3):
i = row * 3 + col
with cols[col]:
if game.puzzle and len(game.puzzle) > i:
value = game.puzzle[i]
if value == 0:
# Empty tile
st.markdown('<div class="empty-tile"></div>', unsafe_allow_html=True)
else:
# Number tile - make it clickable
if st.button(str(value), key=f"tile_{i+1}"):
game.make_move(i + 1)
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
# Control buttons
col1, col2 = st.columns(2)
with col1:
st.markdown('<div class="control-button">', unsafe_allow_html=True)
if st.button("🎲 New Game", key="new_game", type="primary"):
game.new_game()
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
with col2:
st.markdown('<div class="control-button">', unsafe_allow_html=True)
if st.button("πŸ”„ Reset", key="reset_game", type="secondary"):
game.reset_game()
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
# Instructions
with st.expander("🎯 How to Play", expanded=False):
st.markdown("""
**Goal:** Arrange numbers 1-8 in order with the empty space in the bottom-right corner.
**Rules:**
- Click on tiles adjacent to the empty space to move them
- Only tiles next to the empty space can be moved
- Try to solve the puzzle in as few moves as possible!
**Target arrangement:**
""")
# Goal grid visualization
st.markdown("""
<div style="text-align: center;">
<div class="goal-grid">
<div class="goal-tile">1</div>
<div class="goal-tile">2</div>
<div class="goal-tile">3</div>
<div class="goal-tile">4</div>
<div class="goal-tile">5</div>
<div class="goal-tile">6</div>
<div class="goal-tile">7</div>
<div class="goal-tile">8</div>
<div class="goal-tile goal-empty"></div>
</div>
</div>
""", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
if __name__ == "__main__":
main()