ChatGTPとのコラボレーションで作成しました。改良を何回も重ね、pygame-ce, pytorch, GPUを使用することで満足のいく出来になりました。AIは最高級に強くなりました。以下にそのプログラムの全文を掲載いたします。関心のある方はご覧ください。
othello_pygame_gpu_tc.py (GPU負荷軽減版・停止防止フォールバック強化)
————————————————————
・止まらない安定探索(時間制限/ノード上限/ビーム幅)
・Tensor Core(FP16/AMP)+バッチ並列で子局面近似評価(CUDAなしはCPUフォールバック)
・GPU負荷軽減のためのチューニング定数(1)〜4) 反映)
・棋譜保存(S/終局時自動)・◀◀/◀/▶/▶▶(早戻し/戻し/進め/早送り)
・右パネル下段にボタン配置(テキストと重ならない)
・Rでリセット&サイド選択、H(ヒント) L(スローモーション) C(候補手) A(AI候補手)
・終局時はファンファーレ風ビープ
————————————————————
import sys, math, time, struct, threading, random, datetime, copy
import pygame
===== GPU tuning (reduce load) ← 1) 追加 =====
USE_GPU = True # GPU近似評価を使うか(この版ではコード内フラグのみ)
GPU_AMP = True # AMP(FP16)をOFFにするとさらに軽く
GPU_BATCH_MAX = 12 # 一度に評価する子局面の最大数(小さくすると静か)
GPU_USE_EVERY_N = 2 # 探索の進捗あたり N回に1回だけGPU近似評価を使う(間引き)
GPU_POS_SCALE = 0.45 # 近似評価の反映係数(小さいほどGPU寄与を弱め収束早く)
========== Optional: PyTorch for CUDA / Tensor Core ==========
TORCH_AVAILABLE = False
_torch_device = None
AMP_ENABLED = False # 2) GPU_AMPに追従
try:
import torch
TORCH_AVAILABLE = True
if torch.cuda.is_available():
_torch_device = torch.device(“cuda”)
torch.set_float32_matmul_precision(“high”)
AMP_ENABLED = GPU_AMP # ← 固定TrueでなくGPU_AMPに連動(2))
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
========== Simple Beeper ==========
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.005self.sr); rel=int(0.030self.sr) sus=max(atk,n-rel); amp=int(32767self.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)) env = i/max(1,atk) if isus else 1.0) val=int(ampenvmath.sin(2math.pifft)); 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=880; self.ai=587
def human_place(self): self.t.play(self.human,100)
def ai_place(self): self.t.play(self.ai,100)
def pass_beep(self):
def _run():
self.t.play(560,130,520); time.sleep(0.03); self.t.play(660,180,620)
threading.Thread(target=_run,daemon=True).start()
def end_beep(self):
def play_fanfare():
seq=[(880,160),(988,160),(1047,180),(1319,220),(1175,200),(1568,320),(0,120),(1568,220),(0,100),(1760,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 constants ==========
EMPTY, BLACK, WHITE = 0,1,-1
BOARD_SIZE=8; CELL=80
BOARD_PIX=CELL*BOARD_SIZE; PANEL_W=280
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=16
BTN_ROW_Y=H-80; BTN_ROW_X=BOARD_PIX+24
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)
========== Weights(局面フェーズ別)==========
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]]
========== Base logic ==========
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):
mb,=get_valid_moves(b,BLACK); mw,=get_valid_moves(b,WHITE)
return (not mb) and (not mw)
def coord_to_notation(x,y): return f”{COLS[x]}{y+1}”
def clone_board(b): return [row[:] for row in b]
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 tuned)\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 constants / Zobrist / TT ==========
AI_TIME_BUDGET = 1.2
AI_MAX_DEPTH = 9
MAX_NODES = 500_000
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 batch evaluators (Tensor Core/AMP) ==========
def _phase_weights_tensor(empties):
if empties>=46: W=W_OPEN
elif empties>=18: W=W_MID
else: W=W_END
if not TORCH_AVAILABLE: return None
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) # [own, opp]
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 y in range(8):
for x in range(8):
if b[y][x]==color:
for dy,dx in DIRS:
nx,ny=x+dx,y+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)
========== Search ==========
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)
# 3) 近似評価(GPU)を“間引き+バッチ制限”に変更
gpu_sc = None
try:
use_gpu_now = (
TORCH_AVAILABLE and _torch_device and
USE_GPU and
(time.time() < deadline - 0.02) and
(nodes[0] < MAX_NODES * 0.80)
)
if use_gpu_now:
gpu_sc = [0.0] * len(nb_list)
if (nodes[0] // 100) % GPU_USE_EVERY_N == 0:
start_idx = 0
while start_idx < len(nb_list):
end_idx = min(start_idx + GPU_BATCH_MAX, len(nb_list))
part = nb_list[start_idx:end_idx]
part_sc = gpu_evaluate_batch(part, to_move)
if part_sc is None:
gpu_sc = None
break
gpu_sc[start_idx:end_idx] = part_sc
start_idx = end_idx
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)
if time.time()>deadline-0.20: beam=max(4,beam-2)
if nodes[0]>MAX_NODES*0.6: beam=max(4,beam-2)
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
4) best_move の超堅牢フォールバック
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
========== Light candidates for UI ==========
CANDS_TOP_N=3
def candidate_moves_light(b,c,top_n=CANDS_TOP_N):
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
========== Rendering / Animations ==========
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):
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,(iCELL,0),(iCELL,BOARD_PIX),2)
pygame.draw.line(screen,GRID_COLOR,(0,iCELL),(BOARD_PIX,iCELL),2)
for y in range(8):
for x in range(8):
v=b[y][x]
if v!=EMPTY:
cx=xCELL+CELL//2; cy=yCELL+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=lxCELL+CELL//2; cy=lyCELL+CELL//2
pygame.draw.circle(screen,LAST_RING,(cx,cy),CELL//2-10,3)
if turn==HUMAN and hist_idx==-1:
if hint_mode in (HINT_VALID_ONLY,HINT_BOTH):
moves,_=get_valid_moves(b,HUMAN)
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,HUMAN)
if moves:
mv=ordered_moves(b,HUMAN,moves,mp,time.time()+0.01,[0])[0]
pygame.draw.circle(screen,HINT_BEST,(mv[0]*CELL+CELL//2,mv[1]*CELL+CELL//2),CELL//2-14,3)
def draw_cands(color,col):
cands=candidate_moves_light(b,color,CANDS_TOP_N)
rank=["①","②","③"]
for i,(mv,_) in enumerate(cands):
x,y=mv; cx=x*CELL+CELL//2; cy=y*CELL+CELL//2
pygame.draw.circle(screen,col,(cx,cy),12,3)
img=cfont.render(rank[i],True,col); screen.blit(img,(cx-img.get_width()//2,cy-img.get_height()//2))
if show_cands and turn==HUMAN and hist_idx==-1:
draw_cands(HUMAN,(250,160,60))
if cands_for_ai: draw_cands(opp(HUMAN),(120,160,240))
pygame.draw.rect(screen,PANEL_BG,(BOARD_PIX,0,PANEL_W,H))
bl,wh=score(b)
side_b="You" if HUMAN==BLACK else "AI"
side_w="You" if HUMAN==WHITE else "AI"
lines=[
"Othello (GPU+TC tuned)",
"",
f"Side : Black={side_b}, White={side_w}",
f"Turn : {'BLACK' if turn==BLACK else 'WHITE'} ({'You' if turn==HUMAN else 'AI'})",
"AI is thinking..." if ai_thinking 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 Kifu",
" R=Reset & choose side",
" ◀◀/◀/▶/▶▶ or PageUp/Home/End/PageDown"
]
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,dir,color=(230,230,230)):
if dir=='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,dir):
pad=6
if dir=='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)
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 show_pass_overlay(screen,font,b,hint_mode,turn,last,HUMAN,slowmo_on,who,history,hist_idx):
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)
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):
if not mv: return
x,y=mv; cx=xCELL+CELL//2; cy=yCELL+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)
overlay.fill((0,0,0,int(140(0.6+0.4math.sin(phmath.pi2)))))
pygame.draw.circle(overlay,(0,0,0,0),(cx,cy),CELL//2+26); screen.blit(overlay,(0,0))
r=int((CELL//2+18)+4math.sin(phmath.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):
ts=SLOWMO_TS if slowmo_on else 1.0; clock=pygame.time.Clock()
temp=[r[:] for r in b]; drop=int(240ts); t0=pygame.time.get_ticks() while pygame.time.get_ticks()-t0CELL+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
========== Side select ==========
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,520,220); 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”,True,(255,255,255))
l1=info.render(“Press 1 : You play BLACK (first)”,True,(230,230,230))
l2=info.render(“Press 2 : You play WHITE (second)”,True,(230,230,230))
l3=info.render(“ESC : cancel (keep current side)”,True,(190,190,200))
screen.blit(tx,(box.x+(box.w-tx.get_width())//2,box.y+20))
screen.blit(l1,(box.x+32,box.y+90)); screen.blit(l2,(box.x+32,box.y+120)); screen.blit(l3,(box.x+32,box.y+160))
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==pygame.K_1: return BLACK
if e.key==pygame.K_2: return WHITE
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 _torch_device.type==”cuda”:
try: title_dev = torch.cuda.get_device_name(0)
except Exception: title_dev=”CUDA”
pygame.display.set_caption(f”Othello (GPU+TC tuned) – {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
b,turn,last,HUMAN,end_fired = start(BLACK)
hint_mode=HINT_BOTH; slowmo_on=True; show_cands=False; cands_for_ai=False
history=[]; moves_san=[]; hist_idx=-1
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":clone_board(bd),"turn":trn,"last":lst_mv})
hist_idx=-1
push_history(b,turn,last)
def step_to(index):
nonlocal b,turn,last,hist_idx
index=max(0,min(index,len(history)-1)); snap=history[index]
b=clone_board(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)
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): running=False
elif 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:
who_black="You" if HUMAN==BLACK else "AI"
who_white="You" if HUMAN==WHITE else "AI"
fname=f"othello_kifu_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.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:
b,turn,last,HUMAN,end_fired = start(HUMAN)
history.clear(); moves_san.clear(); hist_idx=-1
push_history(b,turn,last)
draw_board(screen,font,b,hint_mode,turn,last,False,HUMAN,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx)
pygame.display.flip()
choice=side_select_modal(screen,font)
if choice is not None: HUMAN=choice; turn=BLACK
elif e.type==pygame.MOUSEBUTTONDOWN and e.button==1:
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
if hist_idx!=-1: continue
if turn==HUMAN:
mx,my=e.pos
if 0<=mx<BOARD_PIX and 0<=my<BOARD_PIX:
x,y=mx//CELL,my//CELL
moves,mp=get_valid_moves(b,HUMAN)
if (x,y) in moves:
b=animate_move(screen,font,b,HUMAN,HUMAN,hint_mode,last,(x,y),mp[(x,y)],slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx)
b,_=apply_move(b,x,y,HUMAN,mp)
moves_san.append(coord_to_notation(x,y))
push_history(b,opp(HUMAN),(x,y))
beep.human_place(); last=(x,y); turn=opp(HUMAN)
# AI turn (LIVE only)
if hist_idx==-1 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)
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)
pygame.display.flip()
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)
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)
b,_=apply_move(b,mv[0],mv[1],opp(HUMAN),mp_ai)
moves_san.append(coord_to_notation(mv[0],mv[1]))
push_history(b,HUMAN,mv); beep.ai_place(); last=mv; turn=HUMAN
# 終局処理(LIVEのみ)
if hist_idx==-1:
if game_over(b):
if not end_fired:
beep.end_beep(); end_fired=True
bl,wh=score(b); res=f"Black {bl} - White {wh}"
who_black="You" if HUMAN==BLACK else "AI"
who_white="You" if HUMAN==WHITE else "AI"
fname=f"othello_kifu_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
save_kifu_txt(fname, moves_san, who_black, who_white, res); print(f"[KIFU] Saved: {fname}")
else:
mv_now,_=get_valid_moves(b,turn)
if not mv_now:
beep.pass_beep()
who="You" if turn==HUMAN else "AI"
show_pass_overlay(screen,font,b,hint_mode,turn,last,HUMAN,slowmo_on,who,history,hist_idx)
turn=opp(turn)
draw_board(screen,font,b,hint_mode,turn,last,False,HUMAN,slowmo_on,show_cands,cands_for_ai,cfont,history,hist_idx)
pygame.display.flip()
pygame.quit(); sys.exit()
if name==”main“:
main()

