オセロゲーム(改良版)

未分類

局面編集ができるようになり、その局面から対局も可能となりました。対局も人間対人間、人間対AIの選択もできるようになりました。山型のアイコンをクリックすると最初の局面に戻ったり最後の局面に行きつくことも可能となりました。棋譜は終局と同時に自動的に保存されます。以下に、このゲームのコードを提示いたします。興味のある方はご覧ください。


# othello_pygame_gpu_tc_editor_hvh_endgame.py

# ————————————————————

# Human vs AI / Human vs Human(交互) + 終盤ソルバで最善ヒント精度UP

# GPU近似評価(PyTorch任意)/ バッチ / AMP / 負荷間引き

# 棋譜ナビ(◀◀/◀/▶/▶▶/▲TOP/▲END)/ 保存 / 盤面編集→適用(手番→モード→サイド)

# PASS/終局演出、Beep効果音(人間高低/AI/パス/終局ファンファーレ)

# ————————————————————

import sys

import math

import time

import struct

import threading

import random

import datetime

import pygame

# ===== GPU tuning(控えめ) =====

USE_GPU = True

GPU_AMP = True

GPU_BATCH_MAX = 12

GPU_USE_EVERY_N = 2

GPU_POS_SCALE = 0.45

# ===== PyTorch(任意) =====

TORCH_AVAILABLE = False

_torch_device = None

AMP_ENABLED = False

try:

    import torch

    TORCH_AVAILABLE = True

    if torch.cuda.is_available():

        _torch_device = torch.device(“cuda”)

        try:

            torch.set_float32_matmul_precision(“high”)

        except Exception:

            pass

        AMP_ENABLED = GPU_AMP

    else:

        _torch_device = torch.device(“cpu”)

except Exception:

    TORCH_AVAILABLE = False

    _torch_device = None

    AMP_ENABLED = False

# ===== Pygame init =====

pygame.mixer.pre_init(44100, -16, 1, 512)

pygame.init()

pygame.font.init()

try:

    pygame.mixer.init(44100, -16, 1, 512)

except Exception:

    pass

# ===== Tone / Beep =====

class ToneSynth:

    def __init__(self, sr=44100, vol=0.9):

        self.sr = sr

        self.vol = vol

        self.cache = {}

    def _mk(self, f, d, g=None):

        key = (int(f), int(d), int(g) if g else None)

        if key in self.cache:

            return self.cache[key]

        n = max(1, int(self.sr * (d / 1000.0)))

        atk = int(0.005 * self.sr)

        rel = int(0.030 * self.sr)

        sus = max(atk, n – rel)

        amp = int(32767 * self.vol)

        data = bytearray()

        f0 = float(f)

        f1 = float(g) if g else f0

        for i in range(n):

            t = i / self.sr

            ff = f0 + (f1 – f0) * (i / max(1, n – 1))

            if i < atk:

                env = i / max(1, atk)

            elif i > sus:

                env = max(0.0, (n – i) / max(1, rel))

            else:

                env = 1.0

            val = int(amp * env * math.sin(2 * math.pi * ff * t))

            data += struct.pack(“<h”, val)

        try:

            snd = pygame.mixer.Sound(buffer=data)

        except Exception:

            class _D:

                def play(self, *a, **k): pass

            snd = _D()

        self.cache[key] = snd

        return snd

    def play(self, f, d, g=None):

        try:

            self._mk(f, d, g).play()

        except Exception:

            pass

class Beeper:

    def __init__(self):

        self.t = ToneSynth()

        self.human_hi = 880

        self.human_lo = 740

        self.ai = 587

    def human_place_black(self): self.t.play(self.human_lo, 100)

    def human_place_white(self): self.t.play(self.human_hi, 100)

    def ai_place(self): self.t.play(self.ai, 100)

    def pass_beep(self):

        def _run():

            self.t.play(520, 140, 480); time.sleep(0.05); self.t.play(600, 180, 560)

        threading.Thread(target=_run, daemon=True).start()

    def end_beep(self):

        def play_fanfare():

            seq = [(784,160),(880,160),(988,180),(1175,200),(1047,160),(1319,320),(0,100),(1319,200),(0,100),(1568,420)]

            for f,d in seq:

                if f>0: self.t.play(f,d)

                time.sleep(d/1000.0+0.05)

        threading.Thread(target=play_fanfare, daemon=True).start()

# ===== UI / 色 =====

EMPTY, BLACK, WHITE = 0, 1, -1

BOARD_SIZE = 8

CELL = 80

BOARD_PIX = CELL * BOARD_SIZE

PANEL_W = 300

W, H, FPS = BOARD_PIX + PANEL_W, BOARD_PIX, 60

BG_COLOR = (32, 96, 32)

GRID_COLOR = (22, 70, 22)

BLACK_COLOR = (20, 20, 20)

WHITE_COLOR_RGB = (235, 235, 235)

TEXT_COLOR = (245, 245, 245)

PANEL_BG = (40, 40, 48)

HINT_VALID = (80, 240, 80)

HINT_BEST = (240, 80, 80)

LAST_RING = (255, 215, 0)

SLOWMO_TS = 3.0

HINT_OFF, HINT_VALID_ONLY, HINT_BEST_ONLY, HINT_BOTH = 0, 1, 2, 3

# ナビボタン

BTN_SIZE = 40

BTN_GAP = 14

BTN_ROW_Y = H – 84

BTN_ROW_X = BOARD_PIX + 20

BTN_FPREV = pygame.Rect(BTN_ROW_X, BTN_ROW_Y, BTN_SIZE, BTN_SIZE)

BTN_PREV  = pygame.Rect(BTN_ROW_X + (BTN_SIZE + BTN_GAP), BTN_ROW_Y, BTN_SIZE, BTN_SIZE)

BTN_NEXT  = pygame.Rect(BTN_ROW_X + 2*(BTN_SIZE + BTN_GAP), BTN_ROW_Y, BTN_SIZE, BTN_SIZE)

BTN_FNEXT = pygame.Rect(BTN_ROW_X + 3*(BTN_SIZE + BTN_GAP), BTN_ROW_Y, BTN_SIZE, BTN_SIZE)

BTN2_Y = BTN_ROW_Y – (BTN_SIZE + 12)

BTN_TOP = pygame.Rect(BTN_ROW_X, BTN2_Y, BTN_SIZE*2 + BTN_GAP, BTN_SIZE)

BTN_END = pygame.Rect(BTN_ROW_X + 2*(BTN_SIZE + BTN_GAP), BTN2_Y, BTN_SIZE*2 + BTN_GAP, BTN_SIZE)

# 編集

BTN_EDIT   = pygame.Rect(BOARD_PIX + 20, 16 + 8*18 + 12, 80, 30)

BTN_APPLY  = pygame.Rect(BTN_EDIT.x + BTN_EDIT.w + 10, BTN_EDIT.y, 80, 30)

BTN_CANCEL = pygame.Rect(BTN_APPLY.x + BTN_APPLY.w + 10, BTN_APPLY.y, 80, 30)

# ===== 重み =====

W_OPEN = [

    [120,-40,-8,-4,-4,-8,-40,120],

    [-40,-60,-6,-6,-6,-6,-60,-40],

    [-8,-6,-4,-2,-2,-4,-6,-8],

    [-4,-6,-2, 0, 0,-2,-6,-4],

    [-4,-6,-2, 0, 0,-2,-6,-4],

    [-8,-6,-4,-2,-2,-4,-6,-8],

    [-40,-60,-6,-6,-6,-6,-60,-40],

    [120,-40,-8,-4,-4,-8,-40,120],

]

W_MID = [

    [100,-20,10,5,5,10,-20,100],

    [-20,-50,-2,-2,-2,-2,-50,-20],

    [10,-2,-1,-1,-1,-1,-2,10],

    [5,-2,-1, 0, 0,-1,-2,5],

    [5,-2,-1, 0, 0,-1,-2,5],

    [10,-2,-1,-1,-1,-1,-2,10],

    [-20,-50,-2,-2,-2,-2,-50,-20],

    [100,-20,10,5,5,10,-20,100],

]

W_END = [

    [100,-10,6,4,4,6,-10,100],

    [-10,-30,-1,-1,-1,-1,-30,-10],

    [6,-1,0,0,0,0,-1,6],

    [4,-1,0,1,1,0,-1,4],

    [4,-1,0,1,1,0,-1,4],

    [6,-1,0,0,0,0,-1,6],

    [-10,-30,-1,-1,-1,-1,-30,-10],

    [100,-10,6,4,4,6,-10,100],

]

# ===== 盤面/基本関数 =====

DIRS = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]

CORNERS={(0,0),(7,0),(0,7),(7,7)}

XSQ={(1,1),(6,1),(1,6),(6,6)}

CSQ={(0,1),(1,0),(6,0),(7,1),(0,6),(1,7),(6,7),(7,6)}

COLS=”abcdefgh”

def new_board():

    b=[[0]*8 for _ in range(8)]

    b[3][3]=WHITE; b[4][4]=WHITE

    b[3][4]=BLACK; b[4][3]=BLACK

    return b

def on_board(x,y): return 0<=x<8 and 0<=y<8

def opp(c): return -c

def flips_for_move(b,x,y,c):

    if not on_board(x,y) or b[y][x]!=EMPTY: return []

    flips=[]

    for dy,dx in DIRS:

        nx,ny=x+dx,y+dy

        line=[]

        while on_board(nx,ny) and b[ny][nx]==opp(c):

            line.append((nx,ny)); nx+=dx; ny+=dy

        if line and on_board(nx,ny) and b[ny][nx]==c:

            flips+=line

    return flips

def get_valid_moves(b,c):

    moves=[]; mp={}

    for y in range(8):

        for x in range(8):

            fl=flips_for_move(b,x,y,c)

            if fl:

                moves.append((x,y)); mp[(x,y)]=fl

    return moves, mp

def apply_move(b,x,y,c,mp=None):

    nb=[row[:] for row in b]

    fl=mp.get((x,y),[]) if mp else flips_for_move(nb,x,y,c)

    if not fl: return None,0

    nb[y][x]=c

    for fx,fy in fl: nb[fy][fx]=c

    return nb,len(fl)

def score(b):

    bl=sum(v==BLACK for row in b for v in row)

    wh=sum(v==WHITE for row in b for v in row)

    return bl,wh

def game_over(b):

    return not get_valid_moves(b,BLACK)[0] and not get_valid_moves(b,WHITE)[0]

def coord_to_notation(x,y): return f”{COLS[x]}{y+1}”

def save_kifu_txt(path, moves_list, who_black=”You/AI”, who_white=”You/AI”, result_str=””):

    with open(path,”w”,encoding=”utf-8″) as f:

        f.write(“Othello Kifu (GPU+TC editor HvH + Endgame)\n”)

        f.write(f”Date: {datetime.datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)}\n”)

        f.write(f”Black: {who_black}\nWhite: {who_white}\n”)

        if result_str: f.write(f”Result: {result_str}\n”)

        f.write(“\nMoves:\n”)

        for i in range(0,len(moves_list),2):

            n=i//2+1

            m1=moves_list[i] if i<len(moves_list) else “”

            m2=moves_list[i+1] if i+1<len(moves_list) else “”

            f.write(f”{n}. {m1}{(‘ ‘+m2) if m2 else ”}\n”)

# ===== AI / TT =====

AI_TIME_BUDGET = 1.0

AI_MAX_DEPTH   = 8

MAX_NODES      = 400_000

# === Endgame solver settings ===

ENDGAME_SOLVE_EMPTIES = 12   # 空点がこの数以下なら完全読了を試みる

ENDGAME_TIME_BUDGET   = 2.0  # その持ち時間(秒)

TT_END = {}                  # 終盤専用TT

random.seed(20251030)

_ZOB=[[[random.getrandbits(64) for _ in range(3)] for _ in range(8)] for _ in range(8)]

TT={}

def zob(b):

    h=0

    for y in range(8):

        for x in range(8):

            v=b[y][x]

            idx=0 if v==EMPTY else (1 if v==BLACK else 2)

            h^=_ZOB[y][x][idx]

    return h

# ===== GPU evaluate(簡易) =====

def _phase_weights_tensor(empties):

    if not TORCH_AVAILABLE: return None

    W = W_OPEN if empties>=46 else (W_MID if empties>=18 else W_END)

    dtype = torch.float16 if AMP_ENABLED and _torch_device and _torch_device.type==”cuda” else torch.float32

    return torch.tensor(W, dtype=dtype, device=_torch_device)

def _boards_tensor(batch_boards, root_color):

    if not TORCH_AVAILABLE: return None

    dtype = torch.float16 if AMP_ENABLED and _torch_device and _torch_device.type==”cuda” else torch.float32

    n=len(batch_boards)

    t=torch.zeros((n,2,8,8),dtype=dtype,device=_torch_device)

    for i,b in enumerate(batch_boards):

        for y in range(8):

            for x in range(8):

                v=b[y][x]

                if v==root_color: t[i,0,y,x]=1

                elif v==opp(root_color): t[i,1,y,x]=1

    return t

def gpu_evaluate_batch(batch_boards, root_color):

    if not (TORCH_AVAILABLE and _torch_device and USE_GPU): return None

    if len(batch_boards)==0: return []

    empties=sum(1 for v in (v for row in batch_boards[0] for v in row) if v==EMPTY)

    Wt=_phase_weights_tensor(empties)

    X=_boards_tensor(batch_boards, root_color)

    if Wt is None or X is None: return None

    with torch.autocast(device_type=”cuda”, dtype=torch.float16, enabled=AMP_ENABLED):

        own=X[:,0:1]; oppo=X[:,1:2]; empty=1.0-(own+oppo)

        Wmap=Wt.view(1,1,8,8).to(X.dtype)

        pos=(own*Wmap – oppo*Wmap).sum(dim=(1,2,3))

        k=torch.ones((1,1,3,3),dtype=X.dtype,device=_torch_device)

        near_empty_own=torch.nn.functional.conv2d(empty,k,padding=1)*own

        near_empty_op =torch.nn.functional.conv2d(empty,k,padding=1)*oppo

        mob_own=(near_empty_own>0).to(X.dtype).sum(dim=(1,2,3))

        mob_op =(near_empty_op >0).to(X.dtype).sum(dim=(1,2,3))

        mob=mob_own-mob_op

        frontier = mob_own – mob_op

        mask_corner=torch.zeros_like(Wmap)

        for (cx,cy) in [(0,0),(7,0),(0,7),(7,7)]: mask_corner[0,0,cy,cx]=1.0

        corner_boost=(own*mask_corner – oppo*mask_corner).sum(dim=(1,2,3))*50.0

        sc=(12.0*mob)+(0.04*pos)+(-4.0*frontier)+corner_boost

    return sc.detach().float().cpu().tolist()

# ===== CPU evaluate =====

def evaluate(b, root_color):

    empties=sum(1 for row in b for v in row if v==EMPTY)

    W=W_OPEN if empties>=46 else (W_MID if empties>=18 else W_END)

    my,_=get_valid_moves(b,root_color)

    op,_=get_valid_moves(b,opp(root_color))

    mobility=len(my)-len(op)

    pos=0

    for y in range(8):

        for x in range(8):

            if b[y][x]==root_color: pos+=W[y][x]

            elif b[y][x]==opp(root_color): pos-=W[y][x]

    corner=0; xpen=0; cadj=0

    for (cx,cy) in CORNERS:

        if b[cy][cx]==root_color: corner+=1

        elif b[cy][cx]==opp(root_color): corner-=1

        if b[cy][cx]==EMPTY:

            for ax,ay in [(cx+1,cy),(cx,cy+1),(cx-1,cy),(cx,cy-1)]:

                if on_board(ax,ay) and (ax,ay) in CSQ and b[ay][ax]==root_color:

                    cadj-=1

    for (xx,yy) in XSQ:

        if b[yy][xx]==root_color: xpen-=1

        elif b[yy][xx]==opp(root_color): xpen+=1

    def frontier(color):

        f=0

        for yy in range(8):

            for xx in range(8):

                if b[yy][xx]==color:

                    for dy,dx in DIRS:

                        nx,ny=xx+dx,yy+dy

                        if on_board(nx,ny) and b[ny][nx]==EMPTY:

                            f+=1; break

        return f

    front=frontier(root_color)-frontier(opp(root_color))

    bl,wh=score(b)

    disc=(bl-wh) if root_color==BLACK else (wh-bl)

    return (12*mobility)+(0.04*pos)+(50*corner)+(10*cadj)+(-4*front)+(0.3*disc)+(6*xpen)

# ===== 探索 =====

class Timeout(Exception): pass

def pump_events_nonblock(): pygame.event.pump()

def ordered_moves(b,to_move,moves,mp,deadline,nodes):

    nb_list=[]; prelim=[]

    empties=sum(1 for row in b for v in row if v==EMPTY)

    empty_corners=any(b[cy][cx]==EMPTY for (cx,cy) in CORNERS)

    for (x,y) in moves:

        sc=0

        if (x,y) in CORNERS: sc+=4000

        if empty_corners:

            if (x,y) in XSQ: sc-= (1400 if empties>=48 else 700)

            if (x,y) in CSQ: sc-= (1600 if empties>=48 else 900)

        nb,_=apply_move(b,x,y,to_move,mp)

        opm,_=get_valid_moves(nb,opp(to_move))

        if any(m in CORNERS for m in opm): sc-=1800

        sc+=max(0, 8-len(opm))

        nb_list.append(nb); prelim.append(sc)

    gpu_sc=None

    try:

        use_gpu_now = (TORCH_AVAILABLE and _torch_device and USE_GPU and (time.time()<deadline-0.02))

        if use_gpu_now:

            gpu_sc=[0.0]*len(nb_list)

            if (nodes[0]//100)%GPU_USE_EVERY_N==0:

                i=0

                while i<len(nb_list):

                    j=min(i+GPU_BATCH_MAX,len(nb_list))

                    part=nb_list[i:j]

                    part_sc=gpu_evaluate_batch(part,to_move)

                    if part_sc is None: gpu_sc=None; break

                    gpu_sc[i:j]=part_sc; i=j

    except Exception:

        gpu_sc=None

    lst=[]

    if gpu_sc is not None:

        for i,(x,y) in enumerate(moves):

            lst.append(((x,y), prelim[i] + gpu_sc[i]*GPU_POS_SCALE))

    else:

        Wt=W_OPEN if empties>=46 else (W_MID if empties>=18 else W_END)

        for i,(x,y) in enumerate(moves):

            nb=nb_list[i]; pos=0

            for yy in range(8):

                for xx in range(8):

                    if nb[yy][xx]==to_move: pos+=Wt[yy][xx]

                    elif nb[yy][xx]==opp(to_move): pos-=Wt[yy][xx]

            lst.append(((x,y), prelim[i] + 0.04*pos))

    lst.sort(key=lambda t:(-t[1],t[0][1],t[0][0]))

    beam = 12 if empties>=40 else (10 if empties>=18 else 8)

    return [mv for mv,_ in lst[:beam]]

def negamax(b,to_move,root_color,depth,alpha,beta,deadline,nodes):

    pump_events_nonblock()

    if time.time()>deadline or nodes[0]>=MAX_NODES: raise Timeout

    nodes[0]+=1

    if depth==0 or game_over(b): return evaluate(b,root_color), None

    key=(zob(b),to_move)

    if key in TT:

        d,flag,sc,bm=TT[key]

        if d>=depth:

            if flag==’EXACT’: return sc,bm

            if flag==’LOWER’ and sc>alpha: alpha=sc

            elif flag==’UPPER’ and sc<beta: beta=sc

            if alpha>=beta: return sc,bm

    moves,mp=get_valid_moves(b,to_move)

    if not moves:

        sc,_=negamax(b,opp(to_move),root_color,depth-1,-beta,-alpha,deadline,nodes)

        return -sc,None

    best=-1e18; bestm=None

    for (x,y) in ordered_moves(b,to_move,moves,mp,deadline,nodes):

        if time.time()>deadline or nodes[0]>=MAX_NODES: raise Timeout

        nb,_=apply_move(b,x,y,to_move,mp)

        val,_=negamax(nb,opp(to_move),root_color,depth-1,-beta,-alpha,deadline,nodes)

        val=-val

        if val>best: best=val; bestm=(x,y)

        if best>alpha: alpha=best

        if alpha>=beta: break

    flag=’EXACT’

    if best<=alpha: flag=’UPPER’

    elif best>=beta: flag=’LOWER’

    TT[key]=(depth,flag,best,bestm)

    return best,bestm

def best_move(b,color):

    moves,_=get_valid_moves(b,color)

    if not moves: return None

    for m in moves:

        if m in CORNERS: return m

    deadline=time.time()+AI_TIME_BUDGET

    nodes=[0]; best=None; depth=1

    try:

        while depth<=AI_MAX_DEPTH:

            sc,mv=negamax(b,color,color,depth,-1e18,1e18,deadline,nodes)

            if mv is not None: best=mv

            if time.time()>deadline-0.10 or nodes[0]>MAX_NODES*0.8: break

            depth+=1

    except Timeout:

        pass

    if best is None or best not in moves:

        moves2,mp2=get_valid_moves(b,color)

        if not moves2: return None

        try:

            ordered=ordered_moves(b,color,moves2,mp2,time.time()+0.02,[0])

            if ordered: return ordered[0]

        except Exception:

            pass

        return moves2[0]

    return best

# ===== 終盤ソルバ(完全読了) =====

def terminal_score_for(b, root_color):

    bl,wh=score(b)

    return (bl-wh) if root_color==BLACK else (wh-bl)

def endgame_best_move(b, to_move, root_color, time_limit=ENDGAME_TIME_BUDGET):

    empties=sum(1 for row in b for v in row if v==EMPTY)

    if empties>ENDGAME_SOLVE_EMPTIES: return None

    deadline=time.time()+time_limit

    def _negamax_end(bb, mover, alpha, beta):

        if time.time()>deadline: raise Timeout

        if game_over(bb): return terminal_score_for(bb,root_color), None

        moves,mp=get_valid_moves(bb,mover)

        if not moves:

            sc,_=_negamax_end(bb, opp(mover), -beta, -alpha)

            return -sc, None

        key=(zob(bb),mover)

        hit=TT_END.get(key)

        if hit is not None: return hit

        best=-10**9; bestm=None

        ordered=[]

        for mv in moves:

            bonus=0

            if mv in CORNERS: bonus+=5000

            if mv in XSQ: bonus-=2000

            if mv in CSQ: bonus-=1500

            ordered.append((bonus,mv))

        ordered.sort(reverse=True)

        for _,(x,y) in ordered:

            nb,_=apply_move(bb,x,y,mover,mp)

            sc,_=_negamax_end(nb, opp(mover), -beta, -alpha)

            sc=-sc

            if sc>best: best, bestm = sc, (x,y)

            if best>alpha: alpha=best

            if alpha>=beta: break

        TT_END[key]=(best,bestm)

        return best,bestm

    try:

        _,mv=_negamax_end(b,to_move,-10**9,10**9)

        return mv

    except Timeout:

        return None

# ===== 軽量候補手 =====

CANDS_TOP_N=3

def candidate_moves_light(b,c, top_n=CANDS_TOP_N):

    empties=sum(1 for row in b for v in row if v==EMPTY)

    if empties<=ENDGAME_SOLVE_EMPTIES:

        mv=endgame_best_move(b,c,c,ENDGAME_TIME_BUDGET)

        if mv is not None: return [(mv,1000)]

    moves,mp=get_valid_moves(b,c)

    if not moves: return []

    ords=ordered_moves(b,c,moves,mp,time.time()+0.01,[0])

    out=[]

    for i,mv in enumerate(ords[:top_n]): out.append((mv,1000-i))

    return out

# ===== 描画/アニメ =====

def draw_board(screen,font,b,hint_mode,turn,last_move,ai_thinking,HUMAN,

               slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,

               edit_mode=False, mouse_pos=None, play_mode=”HvAI”):

    screen.fill((0,0,0))

    pygame.draw.rect(screen,BG_COLOR,(0,0,BOARD_PIX,BOARD_PIX))

    for i in range(9):

        pygame.draw.line(screen,GRID_COLOR,(i*CELL,0),(i*CELL,BOARD_PIX),2)

        pygame.draw.line(screen,GRID_COLOR,(0,i*CELL),(BOARD_PIX,i*CELL),2)

    if edit_mode and mouse_pos:

        mx,my=mouse_pos

        if 0<=mx<BOARD_PIX and 0<=my<BOARD_PIX:

            x,y=mx//CELL,my//CELL

            pygame.draw.rect(screen,(255,255,0),(x*CELL,y*CELL,CELL,CELL),3)

    for y in range(8):

        for x in range(8):

            v=b[y][x]

            if v!=EMPTY:

                cx=x*CELL+CELL//2; cy=y*CELL+CELL//2

                col=BLACK_COLOR if v==BLACK else WHITE_COLOR_RGB

                pygame.draw.circle(screen,col,(cx,cy),CELL//2-6)

                pygame.draw.circle(screen,(0,0,0),(cx,cy),CELL//2-6,2)

    if last_move:

        lx,ly=last_move

        cx=lx*CELL+CELL//2; cy=ly*CELL+CELL//2

        pygame.draw.circle(screen,LAST_RING,(cx,cy),CELL//2-10,3)

    # ヒント対象色

    hint_color=None

    if not edit_mode and hist_idx==-1:

        if play_mode==”HvAI”:

            if turn==HUMAN: hint_color=HUMAN

        else:

            hint_color=turn

    # 有効手点

    if hint_color is not None:

        if hint_mode in (HINT_VALID_ONLY,HINT_BOTH):

            moves,_=get_valid_moves(b,hint_color)

            for (x,y) in moves:

                pygame.draw.circle(screen,HINT_VALID,(x*CELL+CELL//2,y*CELL+CELL//2),7)

        if hint_mode in (HINT_BEST_ONLY,HINT_BOTH):

            moves,mp=get_valid_moves(b,hint_color)

            if moves:

                empties=sum(1 for row in b for v in row if v==EMPTY)

                mv=None

                if empties<=ENDGAME_SOLVE_EMPTIES:

                    mv=endgame_best_move(b,hint_color,hint_color,ENDGAME_TIME_BUDGET)

                if mv is None:

                    mv=ordered_moves(b,hint_color,moves,mp,time.time()+0.02,[0])[0]

                pygame.draw.circle(screen,HINT_BEST,(mv[0]*CELL+CELL//2, mv[1]*CELL+CELL//2), CELL//2-14, 3)

    pygame.draw.rect(screen,PANEL_BG,(BOARD_PIX,0,PANEL_W,H))

    bl,wh=score(b)

    if play_mode==”HvAI”:

        side_b=”You” if HUMAN==BLACK else “AI”

        side_w=”You” if HUMAN==WHITE else “AI”

        who_turn=(‘You’ if turn==HUMAN else ‘AI’)

    else:

        side_b=”You”; side_w=”You”; who_turn=’You’

    lines=[

        “Othello (GPU+TC HvH+Endgame)”,

        f”Mode : {play_mode}”,

        “”,

        f”Side : Black={side_b}, White={side_w}”,

        f”Turn : {‘BLACK’ if turn==BLACK else ‘WHITE’} ({who_turn})” if not edit_mode else “Turn : (EDIT MODE)”,

        “AI is thinking…” if (ai_thinking and not edit_mode and play_mode==”HvAI”) else “”,

        “”,

        f”Score  Black({side_b}): {bl}”,

        f”       White({side_w}): {wh}”,

        “”,

        f”Hint: {[‘OFF’,’Valid’,’Best’,’Both’][hint_mode]}”,

        f”SlowMo: {‘ON’ if slowmo_on else ‘OFF’} (ts={SLOWMO_TS})”,

        f”Cands: {‘ON’ if show_cands else ‘OFF’} / AI Cands: {‘ON’ if cands_for_ai else ‘OFF’}”,

        “”,

        “Keys:”,

        ”  Click=Place / H=Hint / L=SlowMo”,

        ”  C=Cands / A=AI Cands / S=Save”,

        ”  R=Reset & choose mode/side”,

        ”  ◀◀/◀/▶/▶▶ or PageUp/Home/End/PageDown”,

        ”  ▲TOP/▲END buttons”,

        ”  E=Edit toggle”,

    ]

    if edit_mode:

        lines+=[“”,”=== Edit Mode ===”,

                “左クリック: 空→黒→白→空…”,

                “右クリック: 逆回し”,

                “ENTER: 適用(手番→モード→※HvAIなら人間担当)”,

                “ESC: キャンセル(編集破棄)”]

    x0=BOARD_PIX+16; y0=16; p=18

    for i,t in enumerate(lines):

        if t: screen.blit(font.render(t,True,TEXT_COLOR),(x0,y0+i*p))

    def draw_tri(rect, direction, color=(230,230,230)):

        if direction==’left’:

            pts=[(rect.right-6, rect.top+6),(rect.left+6,rect.centery),(rect.right-6,rect.bottom-6)]

        else:

            pts=[(rect.left+6,rect.top+6),(rect.right-6,rect.centery),(rect.left+6,rect.bottom-6)]

        pygame.draw.polygon(screen,color,pts); pygame.draw.rect(screen,(120,120,140),rect,2,border_radius=6)

    def draw_dtri(rect, direction):

        pad=6

        if direction==’left’:

            pts1=[(rect.centerx+4,rect.top+pad),(rect.left+pad,rect.centery),(rect.centerx+4,rect.bottom-pad)]

            pts2=[(rect.right-pad,rect.top+pad),(rect.centerx+10,rect.centery),(rect.right-pad,rect.bottom-pad)]

        else:

            pts1=[(rect.centerx-4,rect.top+pad),(rect.right-pad,rect.centery),(rect.centerx-4,rect.bottom-pad)]

            pts2=[(rect.left+pad,rect.top+pad),(rect.centerx-10,rect.centery),(rect.left+pad,rect.bottom-pad)]

        pygame.draw.polygon(screen,(230,230,230),pts1); pygame.draw.polygon(screen,(230,230,230),pts2)

        pygame.draw.rect(screen,(120,120,140),rect,2,border_radius=6)

    def draw_upbtn(rect,label):

        pygame.draw.rect(screen,(68,68,88),rect,border_radius=6)

        pygame.draw.rect(screen,(120,120,140),rect,2,border_radius=6)

        tri=[(rect.centerx,rect.top+8),(rect.left+10,rect.bottom-10),(rect.right-10,rect.bottom-10)]

        pygame.draw.polygon(screen,(230,230,230),tri)

        txt=font.render(label,True,(230,230,230))

        screen.blit(txt,(rect.centerx-txt.get_width()//2, rect.centery-txt.get_height()//2+8))

    draw_upbtn(BTN_TOP,”TOP”); draw_upbtn(BTN_END,”END”)

    draw_dtri(BTN_FPREV,’left’); draw_tri(BTN_PREV,’left’); draw_tri(BTN_NEXT,’right’); draw_dtri(BTN_FNEXT,’right’)

    idx_text=”LIVE” if hist_idx==-1 else f”{hist_idx+1}/{len(history)}”

    screen.blit(font.render(f”Kifu: {idx_text}”,True,TEXT_COLOR),(BTN_ROW_X, BTN_ROW_Y+BTN_SIZE+8))

    def draw_small_button(rect,label,active=False):

        bg=(90,120,160) if active else (68,68,88)

        pygame.draw.rect(screen,bg,rect,border_radius=6)

        pygame.draw.rect(screen,(120,120,140),rect,2,border_radius=6)

        screen.blit(font.render(label,True,(230,230,230)),

                    (rect.centerx-font.size(label)[0]//2, rect.centery-font.size(label)[1]//2))

    draw_small_button(BTN_EDIT,”EDIT”,edit_mode)

    draw_small_button(BTN_APPLY,”APPLY”,edit_mode)

    draw_small_button(BTN_CANCEL,”CANCEL”,edit_mode)

def show_pass_overlay(screen,font,b,hint_mode,turn,last,HUMAN,slowmo_on,who,history,hist_idx,play_mode):

    dur=900; t0=pygame.time.get_ticks()

    title=pygame.font.SysFont(None,120,True); sub=pygame.font.SysFont(None,36)

    layer=pygame.Surface((BOARD_PIX,BOARD_PIX),pygame.SRCALPHA)

    while pygame.time.get_ticks()-t0<dur:

        el=pygame.time.get_ticks()-t0

        k=el/(dur/2) if el<(dur/2) else (dur-el)/(dur/2); k=max(0,min(1,k))

        draw_board(screen,font,b,hint_mode,turn,last,False,HUMAN,slowmo_on,False,False,sub,history,hist_idx,play_mode=play_mode)

        layer.fill((0,0,0,0))

        band=140

        pygame.draw.rect(layer,(0,0,0,int(180*k)),(0,BOARD_PIX//2-band//2,BOARD_PIX,band))

        tx=title.render(“PASS”,True,(255,255,255)); sx=sub.render(f”({who})”,True,(255,255,255))

        layer.blit(tx,((BOARD_PIX-tx.get_width())//2, BOARD_PIX//2-tx.get_height()//2-14))

        layer.blit(sx,((BOARD_PIX-sx.get_width())//2, BOARD_PIX//2+16))

        screen.blit(layer,(0,0)); pygame.display.flip()

        pygame.event.pump(); pygame.time.delay(16)

def spotlight_ai_move(screen,font,b,mv,HUMAN,hint_mode,last,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,play_mode):

    if not mv: return

    x,y=mv; cx=x*CELL+CELL//2; cy=y*CELL+CELL//2

    overlay=pygame.Surface((BOARD_PIX,BOARD_PIX),pygame.SRCALPHA)

    t0=pygame.time.get_ticks(); dur=600

    while pygame.time.get_ticks()-t0<dur:

        ph=(pygame.time.get_ticks()-t0)/dur

        draw_board(screen,font,b,hint_mode,opp(HUMAN),last,True,HUMAN,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,play_mode=play_mode)

        overlay.fill((0,0,0,int(140*(0.6+0.4*math.sin(ph*math.pi*2)))))

        pygame.draw.circle(overlay,(0,0,0,0),(cx,cy),CELL//2+26)

        screen.blit(overlay,(0,0))

        r=int((CELL//2+18)+4*math.sin(ph*math.pi*2))

        pygame.draw.circle(screen,(255,230,90),(cx,cy),r,4)

        pygame.display.flip(); pygame.event.pump(); pygame.time.delay(16)

def animate_move(screen,font,b,HUMAN,mover,hint_mode,last,mv,flips,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,play_mode):

    ts=SLOWMO_TS if slowmo_on else 1.0

    clock=pygame.time.Clock(); temp=[r[:] for r in b]

    drop=int(240*ts); t0=pygame.time.get_ticks()

    while pygame.time.get_ticks()-t0<drop:

        ph=(pygame.time.get_ticks()-t0)/max(1,drop)

        draw_board(screen,font,temp,hint_mode,opp(mover),last,False,HUMAN,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,play_mode=play_mode)

        cx=mv[0]*CELL+CELL//2; cy=mv[1]*CELL+CELL//2

        r=int((CELL//2-6)*ph); col=WHITE_COLOR_RGB if mover==WHITE else BLACK_COLOR

        pygame.draw.circle(screen,col,(cx,cy),max(1,r))

        pygame.draw.circle(screen,(0,0,0),(cx,cy),max(1,r),2)

        pygame.display.flip(); pygame.event.pump(); clock.tick(90)

    temp[mv[1]][mv[0]]=mover

    return temp

# ===== モーダル =====

def side_select_modal(screen, base_font):

    title=pygame.font.SysFont(None,64,True); info=pygame.font.SysFont(None,32)

    shade=pygame.Surface((W,H),pygame.SRCALPHA); shade.fill((0,0,0,160))

    box=pygame.Rect(0,0,560,240); box.center=(W//2,H//2)

    while True:

        screen.blit(shade,(0,0)); pygame.draw.rect(screen,(30,30,36),box,border_radius=14)

        pygame.draw.rect(screen,(100,100,120),box,2,border_radius=14)

        tx=title.render(“Choose your side (You)”,True,(255,255,255))

        l1=info.render(“Press 1 : You play BLACK”,True,(230,230,230))

        l2=info.render(“Press 2 : You play WHITE”,True,(230,230,230))

        l3=info.render(“ESC : cancel (keep current)”,True,(190,190,200))

        screen.blit(tx,(box.x+(box.w-tx.get_width())//2, box.y+24))

        screen.blit(l1,(box.x+32, box.y+100)); screen.blit(l2,(box.x+32, box.y+132)); screen.blit(l3,(box.x+32, box.y+174))

        pygame.display.flip()

        for e in pygame.event.get():

            if e.type==pygame.QUIT: pygame.quit(); sys.exit()

            if e.type==pygame.KEYDOWN:

                if e.key in (pygame.K_1, pygame.K_KP1): return BLACK

                if e.key in (pygame.K_2, pygame.K_KP2): return WHITE

                if e.key==pygame.K_ESCAPE: return None

        pygame.time.delay(16)

                # — イベント処理が終わった直後あたりに追記 —

        auto_pass_if_no_moves()

def mover_select_modal(screen, base_font):

    title=pygame.font.SysFont(None,64,True); info=pygame.font.SysFont(None,32)

    shade=pygame.Surface((W,H),pygame.SRCALPHA); shade.fill((0,0,0,160))

    box=pygame.Rect(0,0,560,240); box.center=(W//2,H//2)

    while True:

        screen.blit(shade,(0,0)); pygame.draw.rect(screen,(30,30,36),box,border_radius=14)

        pygame.draw.rect(screen,(100,100,120),box,2,border_radius=14)

        tx=title.render(“Who moves first?”,True,(255,255,255))

        l1=info.render(“Press 1 : BLACK to move”,True,(230,230,230))

        l2=info.render(“Press 2 : WHITE to move”,True,(230,230,230))

        l3=info.render(“ESC : cancel”,True,(190,190,200))

        screen.blit(tx,(box.x+(box.w-tx.get_width())//2, box.y+24))

        screen.blit(l1,(box.x+32, box.y+100)); screen.blit(l2,(box.x+32, box.y+132)); screen.blit(l3,(box.x+32, box.y+174))

        pygame.display.flip()

        for e in pygame.event.get():

            if e.type==pygame.QUIT: pygame.quit(); sys.exit()

            if e.type==pygame.KEYDOWN:

                if e.key in (pygame.K_1, pygame.K_KP1): return BLACK

                if e.key in (pygame.K_2, pygame.K_KP2): return WHITE

                if e.key==pygame.K_ESCAPE: return None

        pygame.time.delay(16)

def mode_select_modal(screen, base_font):

    title=pygame.font.SysFont(None,64,True); info=pygame.font.SysFont(None,32)

    shade=pygame.Surface((W,H),pygame.SRCALPHA); shade.fill((0,0,0,160))

    box=pygame.Rect(0,0,620,260); box.center=(W//2,H//2)

    while True:

        screen.blit(shade,(0,0)); pygame.draw.rect(screen,(30,30,36),box,border_radius=14)

        pygame.draw.rect(screen,(100,100,120),box,2,border_radius=14)

        tx=title.render(“Select Play Mode”,True,(255,255,255))

        l1=info.render(“Press 1 : Human vs AI”,True,(230,230,230))

        l2=info.render(“Press 2 : Human vs Human”,True,(230,230,230))

        l3=info.render(“ESC : cancel”,True,(190,190,200))

        screen.blit(tx,(box.x+(box.w-tx.get_width())//2, box.y+24))

        screen.blit(l1,(box.x+32, box.y+100)); screen.blit(l2,(box.x+32, box.y+132)); screen.blit(l3,(box.x+32, box.y+174))

        pygame.display.flip()

        for e in pygame.event.get():  

            if e.type==pygame.QUIT: pygame.quit(); sys.exit()

            if e.type==pygame.KEYDOWN:

                if e.key in (pygame.K_1, pygame.K_KP1): return “HvAI”

                if e.key in (pygame.K_2, pygame.K_KP2): return “HvH”

                if e.key==pygame.K_ESCAPE: return None

        pygame.time.delay(16)

# ===== Main =====

def main():

    title_dev=”CPU”

    if TORCH_AVAILABLE and _torch_device and getattr(_torch_device,’type’,None)==”cuda”:

        try: title_dev=torch.cuda.get_device_name(0)

        except Exception: title_dev=”CUDA”

    pygame.display.set_caption(f”Othello (GPU+TC HvH+Endgame) – {title_dev}”)

    screen=pygame.display.set_mode((W,H))

    clock=pygame.time.Clock()

    font=pygame.font.SysFont(None,24)

    cfont=pygame.font.SysFont(None,20,True)

    beep=Beeper()

    def start(hc): return new_board(), BLACK, None, hc, False

    PLAY_MODE = mode_select_modal(screen,font) or “HvAI”

    HUMAN=BLACK

    if PLAY_MODE==”HvAI”:

        choice=side_select_modal(screen,font)

        if choice is not None: HUMAN=choice

    b,turn,last,HUMAN,end_fired = start(HUMAN)

    hint_mode=HINT_BOTH

    slowmo_on=True

    show_cands=False

    cands_for_ai=False

    history=[]; moves_san=[]; hist_idx=-1

    edit_mode=False; edit_board=None

    def push_history(bd,trn,lst_mv):

        nonlocal history, hist_idx

        if hist_idx!=-1 and hist_idx<len(history)-1:

            history=history[:hist_idx+1]

        history.append({“board”:[row[:] for row in bd],”turn”:trn,”last”:lst_mv})

        hist_idx=-1

    push_history(b,turn,last)

    def auto_pass_if_no_moves():

        “””現在手番に合法手がなければパス→相手番へ(最大2連続まで)。

           編集直後・通常進行・レビュー解除直後すべてで有効。”””

        nonlocal b, turn, last, end_fired, hist_idx, edit_mode

        changed = False

        for _ in range(2):  # 連続パス(両者パス)まで処理

            if edit_mode or hist_idx != -1:      # 編集中 / レビュー中はパスしない

                break

            if game_over(b):

                break

            moves, _ = get_valid_moves(b, turn)

            if moves:

                break

            # 誰がパスしたか表示用の文字

            if PLAY_MODE == “HvAI”:

                who = “You” if turn == HUMAN else “AI”

            else:

                who = “Black” if turn == BLACK else “White”

            # パス演出&音

            beep.pass_beep()

            show_pass_overlay(screen, font, b, hint_mode, turn, last,

                              HUMAN, slowmo_on, who, history, hist_idx, PLAY_MODE)

            # 手番を相手へ

            turn = -turn

            changed = True

            # 盤面は変わらないが、「パスして手番が変わった」状態を履歴に積む

            push_history(b, turn, last)

            auto_pass_if_no_moves()

    def auto_pass_if_no_moves():

        “””現在手番に合法手が無ければ自動でパス→相手番へ(最大2連続)。

           編集直後/レビュー解除直後/通常進行すべてで有効。”””

        nonlocal b, turn, last, end_fired, hist_idx, edit_mode

        changed = False

        for _ in range(2):  # 両者パスまで対応

            if edit_mode or hist_idx != -1:

                break

            if game_over(b):

                break

            moves, _ = get_valid_moves(b, turn)

            if moves:  # 打てる手がある

                break

            # 誰がパスしたか(表示用)— HvH なら Black/White を表示

            if PLAY_MODE == “HvAI”:

                who = “You” if turn == HUMAN else “AI”

            else:

                who = “Black” if turn == BLACK else “White”

            # パス演出と音

            beep.pass_beep()

            show_pass_overlay(screen, font, b, hint_mode, turn, last,

                              HUMAN, slowmo_on, who, history, hist_idx, PLAY_MODE)

            # 手番を相手へ

            turn = -turn

            changed = True

            # 盤面は変えず「手番が変わった」状態を履歴へ

            push_history(b, turn, last)

            auto_pass_if_no_moves()

        return changed

        # 連続パスで終局していれば、終局ハンドラに任せる(end_firedは触らない)

        return changed

    def auto_pass_if_no_moves():

        “””現在手番に合法手が無ければ自動でパス→相手番へ(最大2連続)。

           編集直後/レビュー解除直後/通常進行すべてで有効。”””

        nonlocal b, turn, last, end_fired, hist_idx, edit_mode

        changed = False

        for _ in range(2):  # 両者パスまで対応

            # 編集中/レビュー中はパス処理をしない

            if edit_mode or hist_idx != -1:

                break

            # すでに終局なら何もしない

            if game_over(b):

                break

            moves, _ = get_valid_moves(b, turn)

            if moves:  # 打てる手がある

                break

            # 誰がパスしたか(表示用)

            if PLAY_MODE == “HvAI”:

                who = “You” if turn == HUMAN else “AI”

            else:

                who = “Black” if turn == BLACK else “White”

            # パス演出と音

            beep.pass_beep()

            show_pass_overlay(screen, font, b, hint_mode, turn, last,

                              HUMAN, slowmo_on, who, history, hist_idx, PLAY_MODE)

            # 手番を相手へ

            turn = -turn

            changed = True

            # 盤面は変わらないが「手番が変わった」状態を履歴に積む(再生系と整合)

            push_history(b, turn, last)

            auto_pass_if_no_moves()

        return changed

    def step_to(index):

        nonlocal b,turn,last,hist_idx

        index=max(0,min(index,len(history)-1))

        snap=history[index]

        b=[row[:] for row in snap[“board”]]; turn=snap[“turn”]; last=snap[“last”]; hist_idx=index

    def step_prev():

        if len(history)>=1:

            if hist_idx==-1: step_to(len(history)-1)

            else: step_to(hist_idx-1)

    def step_next():

        nonlocal hist_idx

        if hist_idx==-1: return

        if hist_idx < len(history)-1: step_to(hist_idx+1)

        else: hist_idx=-1

    def step_prev_k(k=10):

        nonlocal hist_idx

        if len(history)==0: return

        if hist_idx==-1: step_to(len(history)-1)

        step_to(max(0,hist_idx-k))

    def step_next_k(k=10):

        nonlocal hist_idx

        if len(history)==0: return

        if hist_idx==-1: step_to(len(history)-1); return

        step_to(min(len(history)-1,hist_idx+k))

    def step_home():

        if len(history)>0: step_to(0)

    def step_end():

        if len(history)>0: step_to(len(history)-1)

    running=True

    while running:

        clock.tick(FPS)

        mouse_pos=pygame.mouse.get_pos()

        for e in pygame.event.get():

            if e.type==pygame.QUIT:

                running=False

            elif e.type==pygame.KEYDOWN:

                if e.key in (pygame.K_ESCAPE, pygame.K_q):

                    if edit_mode:

                        edit_mode=False; edit_board=None

                    else:

                        running=False

                elif e.key==pygame.K_e:

                    if not edit_mode:

                        edit_mode=True; edit_board=[row[:] for row in b]

                    else:

                        edit_mode=False; edit_board=None

                elif e.key==pygame.K_RETURN:

                    if edit_mode and edit_board is not None:

                        mover=mover_select_modal(screen,font)

                        if mover is not None:

                            mode=mode_select_modal(screen,font) or “HvAI”

                            who=None

                            if mode==”HvAI”:

                                who=side_select_modal(screen,font) or BLACK

                            PLAY_MODE=mode

                            HUMAN=who if who is not None else BLACK

                            b=[row[:] for row in edit_board]

                            turn=mover; last=None; end_fired=False

                            history.clear(); moves_san.clear(); hist_idx=-1

                            push_history(b,turn,last)

                            edit_mode=False; edit_board=None

                elif not edit_mode:

                    if e.key==pygame.K_LEFT: step_prev()

                    elif e.key==pygame.K_RIGHT: step_next()

                    elif e.key==pygame.K_PAGEUP: step_prev_k(10)

                    elif e.key==pygame.K_PAGEDOWN: step_next_k(10)

                    elif e.key==pygame.K_HOME: step_home()

                    elif e.key==pygame.K_END: step_end()

                    elif e.key==pygame.K_s:

                        if PLAY_MODE==”HvAI”:

                            who_black=”You” if HUMAN==BLACK else “AI”

                            who_white=”You” if HUMAN==WHITE else “AI”

                        else:

                            who_black=”You”; who_white=”You”

                        fname=f”othello_kifu_{datetime.datetime.now().strftime(‘%Y%m%d_%H%M%S_%f’)}.txt”

                        save_kifu_txt(fname, moves_san, who_black, who_white)

                        print(f”[KIFU] Saved: {fname}”)

                    elif e.key==pygame.K_h: hint_mode=(hint_mode+1)%4

                    elif e.key==pygame.K_l: slowmo_on=not slowmo_on

                    elif e.key==pygame.K_c: show_cands=not show_cands

                    elif e.key==pygame.K_a: cands_for_ai=not cands_for_ai

                    elif e.key==pygame.K_r:

                        mode=mode_select_modal(screen,font) or “HvAI”

                        PLAY_MODE=mode

                        if PLAY_MODE==”HvAI”:

                            choice=side_select_modal(screen,font)

                            if choice is not None: HUMAN=choice

                        else:

                            HUMAN=BLACK

                        b,turn,last,HUMAN,end_fired=start(HUMAN)

                        history.clear(); moves_san.clear(); hist_idx=-1

                        push_history(b,turn,last)

            elif e.type==pygame.MOUSEBUTTONDOWN:

                # ▲TOP/▲END

                if BTN_TOP.collidepoint(e.pos): step_home(); continue

                if BTN_END.collidepoint(e.pos): step_end();  continue

                # Edit buttons

                if BTN_EDIT.collidepoint(e.pos):

                    if not edit_mode: edit_mode=True; edit_board=[row[:] for row in b]

                    else: edit_mode=False; edit_board=None

                    continue

                if BTN_APPLY.collidepoint(e.pos) and edit_mode and edit_board is not None:

                    auto_pass_if_no_moves()

                    mover=mover_select_modal(screen,font)

                    if mover is not None:

                        mode=mode_select_modal(screen,font) or “HvAI”

                        who=None

                        if mode==”HvAI”: who=side_select_modal(screen,font) or BLACK

                        PLAY_MODE=mode

                        HUMAN=who if who is not None else BLACK

                        b=[row[:] for row in edit_board]

                        turn=mover; last=None; end_fired=False

                        history.clear(); moves_san.clear(); hist_idx=-1

                        push_history(b,turn,last)

                        edit_mode=False; edit_board=None

                    continue

                if BTN_CANCEL.collidepoint(e.pos) and edit_mode:

                    edit_mode=False; edit_board=None; continue

                # ◀◀/◀/▶/▶▶

                if BTN_FPREV.collidepoint(e.pos): step_prev_k(10); continue

                if BTN_PREV.collidepoint(e.pos):  step_prev();    continue

                if BTN_NEXT.collidepoint(e.pos):  step_next();    continue

                if BTN_FNEXT.collidepoint(e.pos): step_next_k(10);continue

                # 盤面クリック

                mx,my=e.pos

                if 0<=mx<BOARD_PIX and 0<=my<BOARD_PIX:

                    x,y=mx//CELL,my//CELL

                    if edit_mode:

                        if e.button==1:

                            v=edit_board[y][x]; nv={EMPTY:BLACK, BLACK:WHITE, WHITE:EMPTY}[v]

                            edit_board[y][x]=nv

                        elif e.button==3:

                            v=edit_board[y][x]; nv={EMPTY:WHITE, WHITE:BLACK, BLACK:EMPTY}[v]

                            edit_board[y][x]=nv

                    else:

                        if hist_idx!=-1: continue

                        mover_can = (PLAY_MODE==”HvH”) or (turn==HUMAN)

                        if mover_can:

                            moves,mp=get_valid_moves(b,turn)

                            if (x,y) in moves:

                                if turn==BLACK: beep.human_place_black()

                                else: beep.human_place_white()

                                b=animate_move(screen,font,b,HUMAN,turn,hint_mode,last,(x,y),mp[(x,y)],slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,PLAY_MODE)

                                b,_=apply_move(b,x,y,turn,mp)

                                moves_san.append(coord_to_notation(x,y))

                                last=(x,y); turn=opp(turn); end_fired=False

                                push_history(b,turn,last)

                                auto_pass_if_no_moves()

        # AIターン(HvAIのみ)

        if (not edit_mode) and hist_idx==-1 and (PLAY_MODE==”HvAI”) and (turn==opp(HUMAN)) and not game_over(b):

            moves_ai, mp_ai=get_valid_moves(b,opp(HUMAN))

            if not moves_ai:

                beep.pass_beep()

                show_pass_overlay(screen,font,b,hint_mode,turn,last,HUMAN,slowmo_on,”AI”,history,hist_idx,PLAY_MODE)

                turn=HUMAN

            else:

                draw_board(screen,font,b,hint_mode,turn,last,True,HUMAN,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,play_mode=PLAY_MODE)

                pygame.display.flip()

                # 終盤は厳密に読んで着手してもOK(ただし時間制限内)

                empties=sum(1 for row in b for v in row if v==EMPTY)

                mv=None

                if empties<=ENDGAME_SOLVE_EMPTIES:

                    mv=endgame_best_move(b,opp(HUMAN),opp(HUMAN),ENDGAME_TIME_BUDGET)

                if mv is None:

                    mv=best_move(b,opp(HUMAN))

                spotlight_ai_move(screen,font,b,mv,HUMAN,hint_mode,last,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,PLAY_MODE)

                b=animate_move(screen,font,b,HUMAN,opp(HUMAN),hint_mode,last,mv,mp_ai[mv],slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx,PLAY_MODE)

                b,_=apply_move(b,mv[0],mv[1],opp(HUMAN),mp_ai)

                beep.ai_place()

                moves_san.append(coord_to_notation(mv[0],mv[1]))

                last=mv; turn=HUMAN; end_fired=False

                push_history(b,turn,last)

        # 終局(LIVEのみ)

        if (not edit_mode) and hist_idx==-1:

            if game_over(b) and not end_fired:

                end_fired=True

                bl,wh=score(b); res=f”Black {bl} – White {wh}”

                if PLAY_MODE==”HvAI”:

                    who_black=”You” if HUMAN==BLACK else “AI”

                    who_white=”You” if HUMAN==WHITE else “AI”

                else:

                    who_black=”You”; who_white=”You”

                fname=f”othello_kifu_{datetime.datetime.now().strftime(‘%Y%m%d_%H%M%S_%f’)}.txt”

                save_kifu_txt(fname, moves_san, who_black, who_white, res)

                print(f”[KIFU] Saved: {fname}”)

                Beeper().end_beep()

        # 描画

        draw_board(screen, font,

                   (edit_board if edit_mode and edit_board is not None else b),

                   hint_mode, turn, last, False, HUMAN,

                   slowmo_on, show_cands, False, cfont, history, hist_idx,

                   edit_mode=edit_mode, mouse_pos=mouse_pos, play_mode=PLAY_MODE)

        pygame.display.flip()

    pygame.quit(); sys.exit()

if __name__==”__main__”:

    main()

タイトルとURLをコピーしました