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 # 1〜2マスの孤立した空間は、ブロックが置けなくなる原因になるためカウント 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 # 盤面評価(AIの賢さの要) 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)