| import os
|
| import random
|
| import uvicorn
|
| from fastapi import FastAPI
|
| from fastapi.staticfiles import StaticFiles
|
| from fastapi.responses import FileResponse
|
| from pydantic import BaseModel
|
| from typing import List, Optional
|
|
|
| app = FastAPI()
|
|
|
| class GameState(BaseModel):
|
| board: List[List[int]]
|
| hand: List[Optional[List[List[int]]]]
|
|
|
| def rotate_cw(piece):
|
| return [[piece[1][0], piece[0][0]],
|
| [piece[1][1], piece[0][1]]]
|
|
|
| def can_place(board, piece, r, c):
|
| for ir in range(2):
|
| for ic in range(2):
|
| br, bc = r + ir, c + ic
|
| if br < 0 or br >= 8 or bc < 0 or bc >= 8:
|
| return False
|
| if board[br][bc] >= 0:
|
| return False
|
| return True
|
|
|
| def get_connected_empty_spaces(board):
|
| visited = [[False]*8 for _ in range(8)]
|
| small_holes = 0
|
| for r in range(8):
|
| for c in range(8):
|
| if board[r][c] == -1 and not visited[r][c]:
|
| q = [(r, c)]
|
| visited[r][c] = True
|
| size = 1
|
| while q:
|
| cr, cc = q.pop(0)
|
| for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| nr, nc = cr + dr, cc + dc
|
| if 0 <= nr < 8 and 0 <= nc < 8 and board[nr][nc] == -1 and not visited[nr][nc]:
|
| visited[nr][nc] = True
|
| q.append((nr, nc))
|
| size += 1
|
|
|
| if size <= 2:
|
| small_holes += 1
|
| return small_holes
|
|
|
| def get_color_adjacency(board):
|
| adj = 0
|
| for r in range(8):
|
| for c in range(8):
|
| color = board[r][c]
|
| if 0 <= color <= 3:
|
| for dr, dc in [(1, 0), (0, 1)]:
|
| nr, nc = r + dr, c + dc
|
| if nr < 8 and nc < 8 and board[nr][nc] == color:
|
| adj += 1
|
| return adj
|
|
|
| def simulate_move(board, piece, r, c):
|
| temp_board = [row[:] for row in board]
|
| for ir in range(2):
|
| for ic in range(2):
|
| temp_board[r + ir][c + ic] = piece[ir][ic]
|
|
|
| score = 0
|
| combo = 0
|
|
|
|
|
| while True:
|
| visited = [[False]*8 for _ in range(8)]
|
| to_remove = set()
|
|
|
| for br in range(8):
|
| for bc in range(8):
|
| color = temp_board[br][bc]
|
| if 0 <= color <= 3 and not visited[br][bc]:
|
| q = [(br, bc)]
|
| visited[br][bc] = True
|
| group = [(br, bc)]
|
| while q:
|
| cr, cc = q.pop(0)
|
| for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| nr, nc = cr + dr, cc + dc
|
| if 0 <= nr < 8 and 0 <= nc < 8 and not visited[nr][nc] and temp_board[nr][nc] == color:
|
| visited[nr][nc] = True
|
| q.append((nr, nc))
|
| group.append((nr, nc))
|
| if len(group) >= 3:
|
| for gr, gc in group:
|
| to_remove.add((gr, gc))
|
|
|
| if not to_remove:
|
| break
|
|
|
| combo += 1
|
| garbage = set()
|
| for rr, cc in to_remove:
|
| for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| nr, nc = rr + dr, cc + dc
|
| if 0 <= nr < 8 and 0 <= nc < 8 and temp_board[nr][nc] == 4:
|
| garbage.add((nr, nc))
|
| to_remove.update(garbage)
|
|
|
| score += len(to_remove) * 10 * combo
|
|
|
| for rr, cc in to_remove:
|
| temp_board[rr][cc] = -1
|
|
|
|
|
| max_height = 0
|
| for bc in range(8):
|
| for br in range(8):
|
| if temp_board[br][bc] != -1:
|
| max_height = max(max_height, 8 - br)
|
| break
|
|
|
| small_holes = get_connected_empty_spaces(temp_board)
|
| adjacency = get_color_adjacency(temp_board)
|
|
|
|
|
| eval_score = score * 100
|
| eval_score -= (max_height ** 2) * 5
|
| eval_score -= small_holes * 30
|
| eval_score += adjacency * 3
|
|
|
| return eval_score + random.random() * 0.1
|
|
|
| @app.post("/api/ai_move")
|
| def ai_move(state: GameState):
|
| board = state.board
|
| hand = state.hand
|
|
|
| best_score = -float('inf')
|
| best_move = None
|
|
|
| for p_idx, original_piece in enumerate(hand):
|
| if original_piece is None:
|
| continue
|
| piece = original_piece
|
| for rot in range(4):
|
| for r in range(7):
|
| for c in range(7):
|
| if can_place(board, piece, r, c):
|
| score = simulate_move(board, piece, r, c)
|
| if score > best_score:
|
| best_score = score
|
| best_move = {"piece_idx": p_idx, "rotations": rot, "r": r, "c": c}
|
| piece = rotate_cw(piece)
|
|
|
| if best_move is None:
|
| best_move = {"piece_idx": 0, "rotations": 0, "r": 0, "c": 0}
|
|
|
| return best_move
|
|
|
| app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
| @app.get("/")
|
| def serve_index():
|
| return FileResponse("static/index.html")
|
|
|
| if __name__ == "__main__":
|
| uvicorn.run(app, host="0.0.0.0", port=7860) |