x01.weiqi.14: 跨平台 python 实现
大多数时间,都在使用 deepin 系统,原来的 WPF 实现似乎越来越遥远了。这几天在家学习了一下 tkinter,顺便予以重写。
内在逻辑是一样的,就不重复,但具体实现层面,如图像不能改变大小等不一而足。由于 AI 出现,再去探讨怎么落子已变得毫无意义,所以只实现了最基本的吃子,打劫,倒扑,悔棋功能。
main.py 代码如下:
import os, sys, enum import tkinter as tk import tkinter.filedialog as fdlg import tkinter.messagebox as mbox from core import Board, StepBoard, Sgf, R CurrDir = os.path.dirname(__file__) sys.path.append(CurrDir) class App(tk.Tk): def __init__(self): super().__init__() menu = tk.Menu(self) weiqi_menu = tk.Menu(menu, tearoff=0) weiqi_menu.add_command(label='开始', command=self.start) weiqi_menu.add_command(label='加载SGF', command=self.load) weiqi_menu.add_command(label='保存SGF', command=self.save) weiqi_menu.add_command(label='退出', command=self.destroy) menu.add_cascade(label='围棋', menu=weiqi_menu) help_menu = tk.Menu(menu, tearoff=0) help_menu.add_command(label='测试', command=self.test) help_menu.add_command(label='关于', command=self.about) menu.add_cascade(label="帮助", menu=help_menu) self.config(menu=menu) self.boards = [Board(self), StepBoard(self)] self.board = self.boards[0] self.board.pack() self.x = (self.winfo_screenwidth()-self.board.w)//2 self.set_geometry(self.board.w,self.board.h,self.x) self.title('x01.weiqi') self.configure(background=R.BgColor) def set_geometry(self,w=800,h=600,x=300,y=20): self.geometry('{}x{}+{}+{}'.format(w,h,x,y)) def test(self): self.board.test_steps() def about(self): mbox.showinfo("关于 x01.weiqi", "为跨平台而采用 python 实现的围棋程序 x01.weiqi, 版权属于x01(黄雄)所有。") def start(self): self.board.back_all() self.board.pack_forget() self.board = self.boards[0] self.board.pack() self.set_geometry(self.board.w,self.board.h,self.x) def load(self): self.board.back_all() self.board.pack_forget() self.board = self.boards[1] self.board.pack() types = (('SGF files', '*.sgf'), ('All files', '*.*')) filename = fdlg.askopenfilename(title='打开棋谱', filetypes=types) if filename: self.board.load(filename) self.set_geometry(self.board.w,self.board.h,self.x) def save(self): steps = self.board.wqcore.SgfSteps sgf = Sgf() types = (('SGF files', '*.sgf'), ('All files', '*.*')) filename = fdlg.asksaveasfilename(title='保存棋谱', filetypes=types) if filename: sgf.Save(steps, filename) mbox.showinfo('保存棋谱','[' + filename + '] 保存成功') if __name__ == "__main__": App().mainloop()
core.py 代码如下:
import os import tkinter as tk import tkinter.ttk as ttk CurrDir = os.path.dirname(__file__) class Board(tk.Frame): def __init__(self, master): super().__init__(master) self.w = self.h = R.StoneSize * R.CellNumber+10 self.offset = R.StoneSize/2+5 self.canvas = tk.Canvas(self, bg=R.BgColor,width=self.w,height=self.h) self.canvas.bind(R.ButtonLeft, self.next_one) self.canvas.bind(R.ButtonRight, self.back_one) self.canvas.pack() self.draw_lines() self.draw_stars() self.black_stone = tk.PhotoImage(file=os.path.join(CurrDir,'res/black-{}.png'.format(R.StoneSize))) self.white_stone = tk.PhotoImage(file=os.path.join(CurrDir,'res/white-{}.png'.format(R.StoneSize))) self.isBlack = True self.poses = [] self.stepCount = 0 self.wqcore = WqCore() self.once = Record() # 打劫 self.deads = {} # 悔棋 self.configure(background=R.BgColor) def draw_lines(self): for i in range(R.CellNumber): self.canvas.create_line(self.offset,i*R.StoneSize + self.offset,self.w-self.offset,i*R.StoneSize+self.offset) self.canvas.create_line(i*R.StoneSize+self.offset,self.offset,i*R.StoneSize+self.offset,self.h-self.offset) def draw_stars(self): stars = [(3,3), (3,9), (3,15),(9,3,),(9,9),(9,15),(15,3),(15,9),(15,15)] r = 3 for i in range(R.StarNumber): x,y = self.offset + stars[i][0]*R.StoneSize,self.offset + stars[i][1]*R.StoneSize self.canvas.create_oval(x-r, y-r, x+r, y+r, fill="black") def next_one(self, e): x,y = e.x//R.StoneSize, e.y // R.StoneSize color = 1 if self.isBlack else -1 self.draw_stone((x,y), color) self.kill() def draw_stone(self, pos, color): if color==0: return record = self.wqcore.GetRecord(pos) if record is None or record.color != 0: return x,y = pos self.stepCount += 1 self.poses.append(pos) self.wqcore.Steps.append(Record(row=y,col=x,count=self.stepCount, color=color)) self.wqcore.SgfSteps.append(Record(y,x,self.stepCount,color)) self.wqcore.UpdateRecords() stone = self.black_stone if color == 1 else self.white_stone self.canvas.create_image(self.offset + x*R.StoneSize, self.offset + y*R.StoneSize, image=stone) self.isBlack = not self.isBlack def kill(self): deads = self.wqcore.KillOther() steps = self.wqcore.Steps[:] if deads is not None: if len(deads) == 1: col,row = deads[0] if self.once.count == self.stepCount - 1 and (col,row) in self.wqcore.LinkPoses((self.once.col,self.once.row)): self.back_one(None) return self.once = Record(row, col, self.stepCount,0) dead_records = [ self.wqcore.GetRecord(d) for d in deads] self.deads[self.stepCount] = [Record(d.row,d.col,d.count,-self.wqcore.Current().color) for d in dead_records] for d in deads: r = self.wqcore.GetRecord(d) r = Record(r.row,r.col,r.count, -self.wqcore.Current().color) self.delete_stone(d) for i,s in enumerate(steps): if (s.col,s.row) == d and r.count == s.count: self.wqcore.Steps[i].color = 0 else: deads = self.wqcore.KillSelf() if deads is not None: dead_records = [ self.wqcore.GetRecord(d) for d in deads] self.deads[self.stepCount] = [Record(d.row,d.col,d.count,self.wqcore.Current().color) for d in dead_records] for d in deads: r = self.wqcore.GetRecord(d) r = Record(r.row,r.col,r.count,self.wqcore.Current().color) self.delete_stone(d) for i,s in enumerate(steps): if (s.col,s.row) == d and r.count == s.count: self.wqcore.Steps[i].color = 0 self.wqcore.DeadPoses.clear() self.wqcore.UpdateRecords() def test_steps(self): print(self.wqcore.SgfSteps) def back_all(self): for step in self.wqcore.Steps[:]: self.back_one(None) def back_one(self, e): if len(self.poses) == 0: return pos = self.poses.pop() self.delete_stone(pos) self.show_deads() p = self.wqcore.Steps.pop() self.wqcore.SgfSteps.pop() for i, r in enumerate(self.wqcore.Records[:]): if (p.col, p.row) == (r.col,r.row): self.wqcore.Records[i].count = -1 self.wqcore.Records[i].color = 0 self.isBlack = not self.isBlack self.stepCount -= 1 def last_deads(self): if self.stepCount in self.deads.keys(): deads = self.deads[self.stepCount][:] del self.deads[self.stepCount] return deads return None def get_deads(self): deads = self.last_deads() if deads is None: return None self.wqcore.GetDeadBlocks(deads) result = [r for r in deads if (r.col,r.row) in self.wqcore.DeadPoses] self.wqcore.DeadPoses.clear() return result[:] def show_deads(self): deads = self.get_deads() if deads is None: return for d in deads: for i, s in enumerate(self.wqcore.Steps[:]): if s.count == d.count: self.wqcore.Steps[i].color = d.color stone = self.black_stone if d.color == 1 else self.white_stone self.canvas.create_image(self.offset + d.col*R.StoneSize, self.offset + d.row*R.StoneSize, image=stone) self.wqcore.DeadPoses.clear() self.wqcore.UpdateRecords() def delete_stone(self, pos): r = self.wqcore.GetRecord(pos) if r is None or r.color == 0: return col,row = pos x,y = self.offset + col*R.StoneSize, self.offset + row*R.StoneSize stone = self.canvas.find_closest(x,y) if stone: self.canvas.delete(stone) class WqCore: def __init__(self): self.Steps = [] # 棋步 self.SgfSteps = [] self.Records = [Record(r,c) for r in range(0,19) for c in range(0,19)] # 棋谱 self.DeadPoses = [] def UpdateRecords(self): records = self.Records[:] for i,r in enumerate(records): for s in self.Steps: if (s.col,s.row) == (r.col,r.row): self.Records[i] = s def Current(self): if len(self.Steps) == 0: return None else: return self.Steps[-1] def KillOther(self): c = self.Current() if c is None: return None poses = self.LinkPoses((c.col,c.row)) records = [r for r in self.Records if (r.col,r.row) in poses and r.color != c.color and r.color != 0] result = [] for temp in records: self.DeadPoses.clear() deads = self.GetDeadPoses((temp.col, temp.row)) if deads is not None: [result.append(d) for d in deads] return None if len(result) == 0 else result[:] def KillSelf(self): c = self.Current() if c is None: return None poses = self.LinkPoses((c.col,c.row)) records = [r for r in self.Records if (r.col,r.row) in poses and r.color == c.color and r.color != 0] for temp in records: self.DeadPoses.clear() deads = self.GetDeadPoses((temp.col, temp.row)) if deads is not None: return deads return None def GetRecord(self, pos): for r in self.Records: if (r.col, r.row) == pos: return r return None def GetDeadPoses(self, pos): record = self.GetRecord(pos) self.DeadPoses.append(pos) poses = set(self.LinkPoses(pos)).difference(set(self.DeadPoses)) records = [r for r in self.Records if (r.col,r.row) in poses] result = [] for t in records: if t.color == 0: self.DeadPoses.clear() return None elif t.color == record.color: deads = self.GetDeadPoses((t.col,t.row)) if deads is not None: continue else: return None return self.DeadPoses def GetDeadBlocks(self, deads): self.DeadPoses.clear() curr = self.Current() col,row,color = curr.col, curr.row,curr.color poses = self.LinkPoses((col,row)) for pos in poses: for d in deads: if (d.col,d.row) == pos and d.color == -color: self.GetDeadBlock(d,deads) def GetDeadBlock(self, record, deads): col,row,color = record.col,record.row,record.color self.DeadPoses.append((col,row)) links = set(self.LinkPoses((col,row))).difference(set(self.DeadPoses)) poses = [(r.col,r.row) for r in deads if (r.col,r.row) in links and r.color == color] for x,y in poses: r = self.GetRecord((x,y)) r.color = color self.GetDeadBlock(r, deads) def IsValid(self, pos): x,y = pos if 0 <= x < 19 and 0 <= y < 19: return True else: return False def LinkPoses(self, pos): poses = [] x,y = pos for i in range(-1,2): for j in range(-1,2): if i==j or i==-j: continue if self.IsValid(pos): poses.append((x+j,y+i)) poses.append(pos) return poses def RoundPoses(self, pos): poses = [] x,y = pos for i in range(-1,2): for j in range(-1,2): if self.IsValid(pos): poses.append((x+j,y+i)) return poses class Record: def __init__(self, row=-1,col=-1,count=-1,color=0): self.row = row self.col = col self.count = count self.color = color # black:1,white:-1,empty=0 def __repr__(self): return 'Record({},{},{},{})'.format(self.row,self.col,self.count,self.color) class R: # stone and board StoneSize = 32 CellNumber = 19 StarNumber = 9 BgColor = '#ec5' # bind event names ButtonLeft = '<Button-1>' ButtonRight = '<Button-3>' class Sgf: # # 根属性 # CA = 'CA' # 字符集 # FF = 'FF' # 文件格式(1-4) # GM = 'GM' # 对局类别:1 围棋, 3 国际象棋,4 五子棋,7 中国象棋 # SZ = 'SZ' # 棋盘大小 # AP = 'AP' # 应用软件 # # 对局信息 # GN = 'GN' # 棋谱名称 # GC = 'GC' # 棋谱备注 # PB ='PB' # 黑方 # PW = 'PW' # 白方 # BR = 'BR' # 黑方段位 # WR = 'WR' # 白方段位 # RE = 'RE' # 结果:0 和棋,B+3 黑胜3目,B+R 黑中盘胜 # KM = 'KM' # 贴目 # HA = 'HA' # 让子 # TM = 'TM' # 对局时限 # DT = 'DT' # 日期 # SO = 'SO' # 来源 # OT = 'OT' # 读秒方式 # RU = 'RU' # 规则 # RM = 'RM' # US = 'US' # 编写者 # CP = 'CP' # 版权 # AN = 'AN' # 注解 # EV = 'EV' # 赛事 # RO = 'RO' # 回合 # PC = 'PC' # 地点 # # 走子 # B = 'B' # 黑走子 # W = 'W' # 白走子 def __init__(self, pb='black', pw='white',dt='2020-3-11', gn='Test'): self.Begin = '(;GM[1]PB[{}]PW[{}]BR[2D]WR[2D]RE[]KM[7.5]HA[0]TM[{}]DT[600]GN[{}]SO[x01.weiqi]OT[3/0.5]SZ[19]FF[4]CA[UTF-8]RU[zh]RM[]'.format( pb,pw,dt,gn ) self.End = ')' def ToSgf(self, steps): body = '' a = ord('a') for step in steps: color = ';B[' if step.color == 1 else ';W[' body += color + chr(a+step.col) + chr(a + step.row) + ']' return self.Begin+body+self.End def ToSteps(self, sgf): sgfs = sgf.split(';') sgfs = sgfs[2:-1] count = 0 steps = [] a = ord('a') for s in sgfs: color = 1 if s[0:1] == 'B' else -1 col = ord(s[2:3]) - a row = ord(s[3:4]) - a count += 1 steps.append(Record(row,col,count,color)) return steps def Load(self, filename): sgf = '' with open(filename) as f: for line in f: line = line.strip() sgf += line return self.ToSteps(sgf) def Save(self, steps, filename): with open(filename, 'w') as fd: sgf = self.ToSgf(steps) fd.write(sgf) class StepBoard(Board): def __init__(self, master): super().__init__(master) self.sgf = Sgf() self.sgf_steps = None self.creat_operatebar() def next_one(self, e): if self.sgf_steps is None: return step = self.sgf_steps[self.stepCount] self.draw_stone((step.col,step.row), step.color) self.kill() def load(self, filename): self.sgf_steps = self.sgf.Load(filename) def creat_operatebar(self): bar = tk.Frame(self, height=40, width=self.w, bg=R.BgColor) btnStart = tk.Button(bar, text='|<', command=lambda : self.back_all(), bg=R.BgColor) btnPrevFive = tk.Button(bar, text='<<', command=lambda : [self.back_one(None) for i in range(5)],bg=R.BgColor) btnPrev = tk.Button(bar, text='<', command=lambda : self.back_one(None),bg=R.BgColor) btnNext = tk.Button(bar,text='>', command=lambda : self.next_one(None),bg=R.BgColor) btnNextFive = tk.Button(bar,text='>>', command=lambda : [self.next_one(None) for i in range(5)],bg=R.BgColor) btnEnd = tk.Button(bar,text='>|', command=lambda : [self.next_one(None) for i in range(len(self.sgf_steps)-self.stepCount)],bg=R.BgColor) btnStart.pack(side=tk.LEFT, padx=6) btnPrevFive.pack(side=tk.LEFT, padx=6) btnPrev.pack(side=tk.LEFT, padx=6) btnNext.pack(side=tk.LEFT, padx=6) btnNextFive.pack(side=tk.LEFT, padx=6) btnEnd.pack(side=tk.LEFT, padx=6) bar.pack() self.h += 40 if __name__ == "__main__": print(range(0)) pass
码云链接:x01.weiqi