Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,13 +13,13 @@ MODEL_PATH = "checkers_master_final.pth"
|
|
| 13 |
|
| 14 |
st.set_page_config(page_title="AlphaCheckerZero", page_icon="♟️", layout="wide")
|
| 15 |
|
| 16 |
-
# --- ESTILOS CSS PERSONALIZADOS
|
| 17 |
st.markdown("""
|
| 18 |
<style>
|
| 19 |
.board-container {
|
| 20 |
display: grid;
|
| 21 |
-
grid-template-columns: 30px repeat(8, 60px);
|
| 22 |
-
grid-template-rows: 30px repeat(8, 60px);
|
| 23 |
gap: 2px;
|
| 24 |
background-color: #444;
|
| 25 |
padding: 10px;
|
|
@@ -44,8 +44,8 @@ st.markdown("""
|
|
| 44 |
font-size: 40px;
|
| 45 |
cursor: default;
|
| 46 |
}
|
| 47 |
-
.white-square { background-color: #f0d9b5; }
|
| 48 |
-
.black-square { background-color: #b58863; }
|
| 49 |
|
| 50 |
.piece-white {
|
| 51 |
color: #fff;
|
|
@@ -59,12 +59,11 @@ st.markdown("""
|
|
| 59 |
}
|
| 60 |
.king { border: 2px solid gold; border-radius: 50%; padding: 2px; box-shadow: 0 0 10px gold;}
|
| 61 |
|
| 62 |
-
/* Ajuste para deixar o selectbox mais bonito */
|
| 63 |
.stSelectbox label { font-size: 1.2rem; font-weight: bold; }
|
| 64 |
</style>
|
| 65 |
""", unsafe_allow_html=True)
|
| 66 |
|
| 67 |
-
# --- LÓGICA DO JOGO
|
| 68 |
class Checkers:
|
| 69 |
def get_initial_board(self):
|
| 70 |
board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=np.int8)
|
|
@@ -124,7 +123,13 @@ class Checkers:
|
|
| 124 |
|
| 125 |
def apply_move(self, board, move):
|
| 126 |
b_ = np.copy(board)
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
sub_moves = move if is_jump_chain else [move]
|
| 129 |
for (r1, c1), (r2, c2) in sub_moves:
|
| 130 |
piece = b_[r1, c1]
|
|
@@ -206,7 +211,10 @@ class MCTS:
|
|
| 206 |
else: start_pos_tuple = move[0]
|
| 207 |
start_pos_idx = start_pos_tuple[0] * BOARD_SIZE + start_pos_tuple[1]
|
| 208 |
prior = policy_probs[start_pos_idx]
|
|
|
|
|
|
|
| 209 |
key = tuple(move) if isinstance(move, list) else move
|
|
|
|
| 210 |
move_priors[key] = prior; total_prior += prior
|
| 211 |
if total_prior > 0:
|
| 212 |
for move_key, prior in move_priors.items(): node.children[move_key] = MCTSNode(parent=node, prior=prior / total_prior)
|
|
@@ -216,7 +224,7 @@ class MCTS:
|
|
| 216 |
node.children[key] = MCTSNode(parent=node, prior=1.0 / len(valid_moves))
|
| 217 |
return value
|
| 218 |
|
| 219 |
-
# --- INTERFACE GRÁFICA
|
| 220 |
|
| 221 |
@st.cache_resource
|
| 222 |
def load_model():
|
|
@@ -244,38 +252,48 @@ game = Checkers()
|
|
| 244 |
mcts = MCTS(game, model, sims=150)
|
| 245 |
|
| 246 |
def format_move_for_human(move):
|
| 247 |
-
"""
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
path = " -> ".join([f"({r},{c})" for (r,c), _ in move] + [str(move[-1][1])])
|
| 250 |
-
return f"Salto
|
| 251 |
else:
|
|
|
|
| 252 |
(r1, c1), (r2, c2) = move
|
| 253 |
return f"Mover de ({r1}, {c1}) para ({r2}, {c2})"
|
| 254 |
|
| 255 |
def render_board_html(board):
|
| 256 |
html = '<div class="board-container">'
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
html += '<div class="header-cell"></div>' # Canto vazio
|
| 260 |
-
for c in range(8):
|
| 261 |
-
html += f'<div class="header-cell">{c}</div>'
|
| 262 |
|
| 263 |
for r in range(8):
|
| 264 |
-
# Cabeçalho da linha (0-7)
|
| 265 |
html += f'<div class="header-cell">{r}</div>'
|
| 266 |
-
|
| 267 |
for c in range(8):
|
| 268 |
color_class = "black-square" if (r + c) % 2 == 1 else "white-square"
|
| 269 |
piece = board[r, c]
|
| 270 |
-
|
| 271 |
content = ""
|
| 272 |
if piece == 1: content = '<span class="piece-white">⚪</span>'
|
| 273 |
-
elif piece == 2: content = '<span class="piece-white king">👑</span>'
|
| 274 |
elif piece == -1: content = '<span class="piece-black">⚫</span>'
|
| 275 |
-
elif piece == -2: content = '<span class="piece-black king">👑</span>'
|
| 276 |
-
|
| 277 |
html += f'<div class="square {color_class}">{content}</div>'
|
| 278 |
-
|
| 279 |
html += '</div>'
|
| 280 |
return html
|
| 281 |
|
|
@@ -300,16 +318,13 @@ with col2:
|
|
| 300 |
else:
|
| 301 |
st.info(st.session_state.message)
|
| 302 |
|
| 303 |
-
# Lógica de Turno
|
| 304 |
if st.session_state.player == 1:
|
| 305 |
valid_moves = game.get_valid_moves(st.session_state.board, 1)
|
| 306 |
-
|
| 307 |
if not valid_moves:
|
| 308 |
st.session_state.game_over = True
|
| 309 |
st.session_state.message = "Sem movimentos válidos. Você perdeu. 😔"
|
| 310 |
st.rerun()
|
| 311 |
|
| 312 |
-
# Dicionário para mapear a descrição bonita para o objeto de movimento real
|
| 313 |
move_map = {format_move_for_human(m): m for m in valid_moves}
|
| 314 |
selected_desc = st.selectbox("Sua vez! Escolha o movimento:", list(move_map.keys()))
|
| 315 |
|
|
@@ -321,24 +336,26 @@ with col2:
|
|
| 321 |
st.rerun()
|
| 322 |
|
| 323 |
else:
|
| 324 |
-
# Vez da IA
|
| 325 |
with st.spinner("A AlphaCheckerZero está pensando..."):
|
| 326 |
-
time.sleep(0.2)
|
| 327 |
-
|
| 328 |
valid_moves, policy = mcts.run(np.copy(st.session_state.board), -1)
|
| 329 |
-
|
| 330 |
if not valid_moves:
|
| 331 |
st.session_state.game_over = True
|
| 332 |
st.session_state.message = "A IA travou! VOCÊ VENCEU! 🎉"
|
| 333 |
st.rerun()
|
| 334 |
|
| 335 |
move = valid_moves[np.argmax(policy)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
st.session_state.board = game.apply_move(st.session_state.board, move)
|
| 337 |
st.session_state.player = 1
|
| 338 |
-
st.session_state.message = f"IA moveu: {
|
| 339 |
st.rerun()
|
| 340 |
|
| 341 |
-
# Rodapé explicativo
|
| 342 |
st.markdown("---")
|
| 343 |
st.caption("**Legenda:** ⚪ Suas Peças | ⚫ Peças da IA | 👑 Dama")
|
| 344 |
st.caption("Desenvolvido por Gabriel Yogi com auxílio de Berta.")
|
|
|
|
| 13 |
|
| 14 |
st.set_page_config(page_title="AlphaCheckerZero", page_icon="♟️", layout="wide")
|
| 15 |
|
| 16 |
+
# --- ESTILOS CSS PERSONALIZADOS ---
|
| 17 |
st.markdown("""
|
| 18 |
<style>
|
| 19 |
.board-container {
|
| 20 |
display: grid;
|
| 21 |
+
grid-template-columns: 30px repeat(8, 60px);
|
| 22 |
+
grid-template-rows: 30px repeat(8, 60px);
|
| 23 |
gap: 2px;
|
| 24 |
background-color: #444;
|
| 25 |
padding: 10px;
|
|
|
|
| 44 |
font-size: 40px;
|
| 45 |
cursor: default;
|
| 46 |
}
|
| 47 |
+
.white-square { background-color: #f0d9b5; }
|
| 48 |
+
.black-square { background-color: #b58863; }
|
| 49 |
|
| 50 |
.piece-white {
|
| 51 |
color: #fff;
|
|
|
|
| 59 |
}
|
| 60 |
.king { border: 2px solid gold; border-radius: 50%; padding: 2px; box-shadow: 0 0 10px gold;}
|
| 61 |
|
|
|
|
| 62 |
.stSelectbox label { font-size: 1.2rem; font-weight: bold; }
|
| 63 |
</style>
|
| 64 |
""", unsafe_allow_html=True)
|
| 65 |
|
| 66 |
+
# --- LÓGICA DO JOGO ---
|
| 67 |
class Checkers:
|
| 68 |
def get_initial_board(self):
|
| 69 |
board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=np.int8)
|
|
|
|
| 123 |
|
| 124 |
def apply_move(self, board, move):
|
| 125 |
b_ = np.copy(board)
|
| 126 |
+
# AQUI TAMBÉM PODERIA DAR ERRO, ENTÃO GARANTIMOS O FORMATO
|
| 127 |
+
is_jump_chain = False
|
| 128 |
+
if isinstance(move, list):
|
| 129 |
+
is_jump_chain = True
|
| 130 |
+
elif isinstance(move, tuple) and len(move) > 0 and isinstance(move[0], tuple) and len(move[0]) > 0 and isinstance(move[0][0], tuple):
|
| 131 |
+
is_jump_chain = True
|
| 132 |
+
|
| 133 |
sub_moves = move if is_jump_chain else [move]
|
| 134 |
for (r1, c1), (r2, c2) in sub_moves:
|
| 135 |
piece = b_[r1, c1]
|
|
|
|
| 211 |
else: start_pos_tuple = move[0]
|
| 212 |
start_pos_idx = start_pos_tuple[0] * BOARD_SIZE + start_pos_tuple[1]
|
| 213 |
prior = policy_probs[start_pos_idx]
|
| 214 |
+
|
| 215 |
+
# IMPORTANTE: MCTS converte lista para tupla aqui para usar como chave de dicionário
|
| 216 |
key = tuple(move) if isinstance(move, list) else move
|
| 217 |
+
|
| 218 |
move_priors[key] = prior; total_prior += prior
|
| 219 |
if total_prior > 0:
|
| 220 |
for move_key, prior in move_priors.items(): node.children[move_key] = MCTSNode(parent=node, prior=prior / total_prior)
|
|
|
|
| 224 |
node.children[key] = MCTSNode(parent=node, prior=1.0 / len(valid_moves))
|
| 225 |
return value
|
| 226 |
|
| 227 |
+
# --- INTERFACE GRÁFICA ---
|
| 228 |
|
| 229 |
@st.cache_resource
|
| 230 |
def load_model():
|
|
|
|
| 252 |
mcts = MCTS(game, model, sims=150)
|
| 253 |
|
| 254 |
def format_move_for_human(move):
|
| 255 |
+
"""
|
| 256 |
+
Formata o movimento para texto legível.
|
| 257 |
+
Lida tanto com listas (pulos originais) quanto tuplas aninhadas (pulos convertidos pelo MCTS).
|
| 258 |
+
"""
|
| 259 |
+
is_jump = False
|
| 260 |
+
|
| 261 |
+
# 1. Se for lista, é um pulo
|
| 262 |
+
if isinstance(move, list):
|
| 263 |
+
is_jump = True
|
| 264 |
+
|
| 265 |
+
# 2. Se for tupla, precisamos ver o conteúdo para saber se é pulo ou movimento simples
|
| 266 |
+
elif isinstance(move, tuple):
|
| 267 |
+
# Se o primeiro item da tupla for OUTRA tupla (ex: ((r,c), (r,c))), então é um pulo que foi convertido
|
| 268 |
+
# Um movimento simples teria um int como primeiro sub-item: ((2,3), (3,4)) -> move[0] é (2,3), move[0][0] é 2 (int).
|
| 269 |
+
if len(move) > 0 and isinstance(move[0], tuple) and len(move[0]) > 0 and isinstance(move[0][0], tuple):
|
| 270 |
+
is_jump = True
|
| 271 |
+
|
| 272 |
+
if is_jump:
|
| 273 |
+
# Pulo múltiplo ou captura simples
|
| 274 |
path = " -> ".join([f"({r},{c})" for (r,c), _ in move] + [str(move[-1][1])])
|
| 275 |
+
return f"Salto/Captura: {path}"
|
| 276 |
else:
|
| 277 |
+
# Movimento simples
|
| 278 |
(r1, c1), (r2, c2) = move
|
| 279 |
return f"Mover de ({r1}, {c1}) para ({r2}, {c2})"
|
| 280 |
|
| 281 |
def render_board_html(board):
|
| 282 |
html = '<div class="board-container">'
|
| 283 |
+
html += '<div class="header-cell"></div>'
|
| 284 |
+
for c in range(8): html += f'<div class="header-cell">{c}</div>'
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
for r in range(8):
|
|
|
|
| 287 |
html += f'<div class="header-cell">{r}</div>'
|
|
|
|
| 288 |
for c in range(8):
|
| 289 |
color_class = "black-square" if (r + c) % 2 == 1 else "white-square"
|
| 290 |
piece = board[r, c]
|
|
|
|
| 291 |
content = ""
|
| 292 |
if piece == 1: content = '<span class="piece-white">⚪</span>'
|
| 293 |
+
elif piece == 2: content = '<span class="piece-white king">👑</span>'
|
| 294 |
elif piece == -1: content = '<span class="piece-black">⚫</span>'
|
| 295 |
+
elif piece == -2: content = '<span class="piece-black king">👑</span>'
|
|
|
|
| 296 |
html += f'<div class="square {color_class}">{content}</div>'
|
|
|
|
| 297 |
html += '</div>'
|
| 298 |
return html
|
| 299 |
|
|
|
|
| 318 |
else:
|
| 319 |
st.info(st.session_state.message)
|
| 320 |
|
|
|
|
| 321 |
if st.session_state.player == 1:
|
| 322 |
valid_moves = game.get_valid_moves(st.session_state.board, 1)
|
|
|
|
| 323 |
if not valid_moves:
|
| 324 |
st.session_state.game_over = True
|
| 325 |
st.session_state.message = "Sem movimentos válidos. Você perdeu. 😔"
|
| 326 |
st.rerun()
|
| 327 |
|
|
|
|
| 328 |
move_map = {format_move_for_human(m): m for m in valid_moves}
|
| 329 |
selected_desc = st.selectbox("Sua vez! Escolha o movimento:", list(move_map.keys()))
|
| 330 |
|
|
|
|
| 336 |
st.rerun()
|
| 337 |
|
| 338 |
else:
|
|
|
|
| 339 |
with st.spinner("A AlphaCheckerZero está pensando..."):
|
| 340 |
+
time.sleep(0.2)
|
|
|
|
| 341 |
valid_moves, policy = mcts.run(np.copy(st.session_state.board), -1)
|
|
|
|
| 342 |
if not valid_moves:
|
| 343 |
st.session_state.game_over = True
|
| 344 |
st.session_state.message = "A IA travou! VOCÊ VENCEU! 🎉"
|
| 345 |
st.rerun()
|
| 346 |
|
| 347 |
move = valid_moves[np.argmax(policy)]
|
| 348 |
+
|
| 349 |
+
# AQUI É ONDE OCORRIA O ERRO ANTES:
|
| 350 |
+
# A IA retorna o movimento (que pode ser uma tupla de pulo)
|
| 351 |
+
# E agora a função 'format_move_for_human' sabe lidar com isso.
|
| 352 |
+
move_text = format_move_for_human(move)
|
| 353 |
+
|
| 354 |
st.session_state.board = game.apply_move(st.session_state.board, move)
|
| 355 |
st.session_state.player = 1
|
| 356 |
+
st.session_state.message = f"IA moveu: {move_text}. Sua vez!"
|
| 357 |
st.rerun()
|
| 358 |
|
|
|
|
| 359 |
st.markdown("---")
|
| 360 |
st.caption("**Legenda:** ⚪ Suas Peças | ⚫ Peças da IA | 👑 Dama")
|
| 361 |
st.caption("Desenvolvido por Gabriel Yogi com auxílio de Berta.")
|