局面編集ができるようになり、その局面から対局も可能となりました。対局も人間対人間、人間対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()
