Pipe 管道拼图小游戏 (tkinter)

Jcinta ·
更新时间:2024-11-14
· 738 次阅读

最近沉迷股市,研究期权,导致好久没来写博客了,果然资本最容易让人腐朽。
应好友拜托,帮他在澳洲大学的女友写编程作业,所以正好顺便写写博客。。。

简介 目的:编写一个 Pipe 管道拼图游戏 Pipe 是一个单人的管道拼图游戏(不是私募基金!2333),就是把管道用鼠标从头到尾连起来。大家应该都玩过,游戏如图:

Pipe

玩法 选取:鼠标左键点取或放置,右键取消或删除。 把左边可选取的管道放置在空的格子内。 初始提供 的开始,结束,与初始的管道 不可移动、删除或旋转。 重新开始:重开一盘 开始新游戏:创建一个新的棋盘 获胜:弹出新提示获胜窗口

桌面

import re import csv # 初始命名 EMPTY_TILE = "tile" START_PIPE = "start" END_PIPE = "end" LOCKED_TILE = "locked" # 起始管道 不可移动 SPECIAL_TILES = { "S": START_PIPE, "E": END_PIPE, "L": LOCKED_TILE } # 管道类型 PIPES = { "ST": "straight", # 横或竖的管子 "CO": "corner", # 弯的管子 "CR": "cross", # 十字管 "JT": "junction-t", # T型管 "DI": "diagonals", # 平行管 "OU": "over-under" # 天桥管 } 管道类 Tile 接下来创建一个管道地类(铺设管道的地面),负责给地面道命名,与赋予是否可选状态 铺设管道的地面 (地面名 与 是否可选)

get_name(self) -> str: 获取名字

get_id(self) -> str: 获取类型名

set_select(self, select: bool): 重新设置可选状态

can_select(self) -> bool: 获取可选状态

class Tile: """ >>> locked = Tile('locked', False) >>> locked.get_name() 'locked' >>> locked.get_id() 'tile' >>> locked.can_select() False >>> str(locked) "Tile('locked', False)" """ def __init__(self, name, selectable=True): self.name = name self.selectable = selectable def get_name(self) -> str: return self.name def get_id(self) -> str: return self.__class__.__name__.lower() def set_select(self, select: bool): self.selectable = select return None def can_select(self) -> bool: return self.selectable def __str__(self) -> str: return f"{self.__class__.__name__}({self.name},{self.selectable})" def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name},{self.selectable})" Pipe 类 管道类,负责管道的名称 与 管道的方向 与 是否可选

get_connected(self, side: str) -> list: 取得输入的连接方,若无返回空

rotate(self, direction: int): 旋转,区间在【0,3】,所以使用 %4

get_orientation(self) -> int: 返回方向

管道

PROP_PIPE = {('start',0):{None: ['N'],'N': ['N'], 'S': ['N'],'E': ['N'], 'W': ['N']}, ('start',1):{None: ['E'],'N': ['E'], 'S': ['E'],'E': ['E'], 'W': ['E']}, ('start',2):{None: ['S'],'N': ['S'], 'S': ['S'],'E': ['S'], 'W': ['S']}, ('start',3):{None: ['W'],'N': ['W'], 'S': ['W'],'E': ['W'], 'W': ['W']}, ('end',0):{None: ['S'],'N': ['S'], 'S': ['S'],'E': ['S'], 'W': ['S']}, ('end',1):{None: ['W'],'N': ['W'], 'S': ['W'],'E': ['W'], 'W': ['W']}, ('end',2):{None: ['N'],'N': ['N'], 'S': ['N'],'E': ['N'], 'W': ['N']}, ('end',3):{None: ['E'],'N': ['E'], 'S': ['E'],'E': ['E'], 'W': ['E']}, ('straight', 0) : {'N': ['S'], 'S': ['N'],'E': [], 'W': []} , ('straight', 1) : {'E': ['W'], 'W': ['E'],'N': [], 'S': []} , ('straight', 2) : {'N': ['S'], 'S': ['N'],'E': [], 'W': []} , ('straight', 3) : {'E': ['W'], 'W': ['E'],'N': [], 'S': []} , ('corner', 0) : {'N': ['E'], 'E': ['N'],'S':[],'W':[]} , ('corner', 1) : {'E': ['S'], 'S': ['E'],'N':[],'W':[]} , ('corner', 2) : {'W': ['S'], 'S': ['W'],'N':[],'E':[]} , ('corner', 3) : {'W': ['N'], 'N': ['W'],'S':[],'E':[]} , ('cross', 0) : {'N': ['S', 'W', 'E'], 'S': ['N', 'W', 'E'], 'W': ['N', 'S', 'E'], 'E': ['N', 'S', 'W']} , ('cross', 1) : {'N': ['S', 'W', 'E'], 'S': ['N', 'W', 'E'], 'W': ['N', 'S', 'E'], 'E': ['N', 'S', 'W']} , ('cross', 2) : {'N': ['S', 'W', 'E'], 'S': ['N', 'W', 'E'], 'W': ['N', 'S', 'E'], 'E': ['N', 'S', 'W']} , ('cross', 3) : {'N': ['S', 'W', 'E'], 'S': ['N', 'W', 'E'], 'W': ['N', 'S', 'E'], 'E': ['N', 'S', 'W']} , ('junction-t', 0) : {'S': ['W', 'E'], 'W': ['S', 'E'], 'E': ['S', 'W'],'N':[]} , ('junction-t', 1) : {'N': ['S', 'W'], 'S': ['N', 'W'], 'W': ['N', 'S'],'E':[]} , ('junction-t', 2) : {'N': ['W', 'E'], 'W': ['N', 'E'], 'E': ['N', 'W'],'S':[]} , ('junction-t', 3) : {'N': ['S', 'E'], 'S': ['N', 'E'], 'E': ['N', 'S'],'W':[]} , ('diagonals',0) :{'N': ['E'], 'E': ['N'], 'S': ['W'], 'W': ['S']}, ('diagonals',1) :{'N': ['W'], 'E': ['S'], 'S': ['E'], 'W': ['N']}, ('diagonals',2) :{'N': ['E'], 'E': ['N'], 'S': ['W'], 'W': ['S']}, ('diagonals',3) :{'N': ['W'], 'E': ['S'], 'S': ['E'], 'W': ['N']}, ('over-under',0) :{'N': ['S'], 'E': ['W'], 'S': ['N'], 'W': ['E']}, ('over-under',1) :{'N': ['S'], 'E': ['W'], 'S': ['N'], 'W': ['E']}, ('over-under',2) :{'N': ['S'], 'E': ['W'], 'S': ['N'], 'W': ['E']}, ('over-under',3) :{'N': ['S'], 'E': ['W'], 'S': ['N'], 'W': ['E']}} class Pipe(Tile): """ >>> new_pipe = Pipe("straight", 1) >>> new_pipe.get_connected("E") ['W'] >>> new_pipe.get_id() 'pipe' """ def __init__(self, name, orientation=0, selectable=True): self.name = name self.orientation = orientation self.selectable = selectable def get_connected(self, side:str=None) -> list: return PROP_PIPE[(self.name,self.orientation)][side] def rotate(self, direction: int): self.orientation = (self.orientation+direction)%4 return None def get_orientation(self) -> int: return self.orientation def __str__(self) -> str: return f"{self.__class__.__name__}({self.name}, {self.orientation})" def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name}, {self.orientation})" Special_Pipe 起始、结束等 特殊固定管道 特殊管道 固定 selectable = False class Special_Pipe(Pipe): def __init__(self,name='special_pipe', orientation=0, selectable=False): self.name = name self.orientation = orientation self.selectable = selectable def get_id(self) -> str: return 'special_pipe' def set_select(self, select: bool): return None def __str__(self) -> str: return f"{self.__class__.__name__}({self.orientation})" def __repr__(self) -> str: return f"{self.__class__.__name__}({self.orientation})" class StartPipe(Special_Pipe): """ >>> start = StartPipe() >>> str(start) "StartPipe(0)" >>> start.get_id() 'special_pipe' >>> repr(start) "StartPipe(0)" >>> start.get_orientation() 0 >>> start.get_connected() ['N'] """ def __init__(self,orientation=0): self.name = 'start' self.orientation = orientation self.selectable = False class EndPipe(Special_Pipe): """ >>> end = EndPipe() >>> str(end) "EndPipe(0)" >>> repr(end) "EndPipe(0)" >>> end.get_id() 'special_pipe' >>> end.get_connected() ['S'] """ def __init__(self,orientation=0): self.name = 'end' self.orientation = orientation self.selectable = False PipeGame 创建游戏 初始化 __init__

(game_file) 桌面保存的文件名

self.board_layout 游戏桌面 self.playable_pipes 可移动的棋子 self.start 开始位置 self.end 结束位置 self.max_rowself.max_col 最大边长 读取 桌面 load_file

提供的是 csv 文件, 所以 要在 文首 import csv

文件格式如图:

因此 读取时 要把符号转换成 类,用函数 convert 转换 string -> class

因为读取的 信息 有字母与数字 运用 正则表达 提取字母与数字

最后一排 为可移动棋子数, 所以把它另外提取出来,然后给各个棋子具体的数量

get_board_layout(self)get_playable_pipes(self) 为读取桌面 与棋子的函数

移动更改删除 棋子 change_playable_amount:更改可移动的数量 (增加与减少numberget_pipe: 获取 位置position 的信息 set_pipe: 放置棋子在桌面上 并移除一个 它的可移动数量 pipe_in_position: 检测 该位置 的管道 valid_position: 是否在可棋盘内 remove_pipe: 在桌面上移除一个棋子 并增加一个 可移动数量 获取对应方向位置的信息 position_in_direction: 若在位置(1,0)选择方向('E') 那它对应的为 'W',(1,1) 检测 是否获胜 check_win class PipeGame: """ A game of Pipes. """ def __init__(self, game_file='game_1.csv'): """ Construct a game of Pipes from a file name. Parameters: game_file (str): name of the game file. """ self.start, self.end = None, None self.board_layout,self.playable_pipes = self.load_file(game_file) self.start = self.get_starting_position() self.end = self.get_ending_position() self.max_row = len(self.board_layout) self.max_col = len(self.board_layout[0]) def load_file(self, filename: str): with open(filename,'r') as csvfile: reader = csv.reader(csvfile) board= [row for row in reader] pipes = board.pop() playable_pipes = dict() board_layout = [] for i,name in enumerate(['straight', 'corner', 'cross', 'junction-t', 'diagonals', 'over-under']): playable_pipes.update({name:int(pipes[i])}) for row in board: rows = [] for col in row: rows.append(self.convert(col)) board_layout.append(rows) return board_layout, playable_pipes def convert(self, tile): if tile == '#': return Tile('tile', True) orientation = int(re.findall("\d+",tile)[0]) if re.findall("\d+",tile)!=[] else 0 name = re.findall("[A-Z]+", tile)[0] if name == 'S': return StartPipe(orientation) elif name == 'E': return EndPipe(orientation) elif name == 'L': return Tile('locked', False) elif name in PIPES: return Pipe(PIPES[name],orientation,False) def get_board_layout(self): return self.board_layout def get_playable_pipes(self): return self.playable_pipes def change_playable_amount(self, pipe_name: str, number: int): add_num = self.playable_pipes[pipe_name]+number self.playable_pipes.update({pipe_name:add_num}) return None def get_pipe(self, position): return self.board_layout[position[0]][position[1]] def set_pipe(self, pipe: Pipe, position): self.change_playable_amount(pipe.name,-1) self.board_layout[position[0]][position[1]] = pipe return None def pipe_in_position(self, position) -> Pipe: if position == None: return None elif self.valid_position(position): pipe = self.board_layout[position[0]][position[1]] if (pipe.get_id() == 'pipe') or (pipe.get_id() == 'special_pipe'): return pipe else: return None def remove_pipe(self, position): self.change_playable_amount(self.pipe_in_position(position).name,1) self.board_layout[position[0]][position[1]] = Tile('tile', True) return None def valid_position(self,position): if position[0]=self.max_row or position[1]=self.max_col: return False else: return True def position_in_direction(self, direction, position): tile = self.board_layout[position[0]][position[1]] if tile == None: return None else: if direction == 'N' and self.valid_position((position[0]-1,position[1])): return ('S',(position[0]-1,position[1])) elif direction == 'S'and self.valid_position((position[0]+1,position[1])): return ('N',(position[0]+1,position[1])) elif direction == 'E'and self.valid_position((position[0],position[1]+1)): return ('W',(position[0],position[1]+1)) elif direction == 'W'and self.valid_position((position[0],position[1]-1)): return ('E',(position[0],position[1]-1)) return None def get_starting_position(self): if self.start != None: return self.start for row_num, row in enumerate(self.board_layout): for col_num,col in enumerate(row): if col.__class__.__name__=='StartPipe': return (row_num,col_num) def get_ending_position(self): if self.end != None: return self.end for row_num, row in enumerate(self.board_layout): for col_num,col in enumerate(row): if col.__class__.__name__=='EndPipe': return (row_num,col_num) def check_win(self): """ (bool) Returns True if the player has won the game False otherwise. """ position = self.get_starting_position() pipe = self.pipe_in_position(position) queue = [(pipe, None, position)] discovered = [(pipe, None)] while queue: pipe, direction, position = queue.pop() for direction in pipe.get_connected(direction): if self.position_in_direction(direction, position) is None: new_direction = None new_position = None else: new_direction, new_position = self.position_in_direction(direction, position) if new_position == self.get_ending_position() and direction == self.pipe_in_position(new_position).get_connected()[0]: return True pipe = self.pipe_in_position(new_position) if pipe is None or (pipe, new_direction) in discovered: continue discovered.append((pipe, new_direction)) queue.append((pipe, new_direction, new_position)) return False 用 tkinter 创建游戏

以下代码为老师提供

import tkinter as tk from tkinter import messagebox from tkinter import simpledialog 左边的选择框 与可选棋子 class SelectionPanel(tk.Canvas): """ Sidebar display of the selectable pipes. Shows all the types of pipes that can be placed within the game and how many are left to be placed. """ def __init__(self, master, playable_pipes, panel_selection=None, selected=None, *args, **kwargs): """ Construct a new selection panel canvas. Parameters: master (tk.Widget): Widget within which to place the selection panel. playable_pipes (dict): Mapping of types of pipes to amount of pipes remaining. panel_selection (callable): Function or method to call when a pipe is selected. selected (str): Type of pipe that is currently selected. """ super().__init__(master, *args, **kwargs) self._master = master self._selected = selected self._playable = playable_pipes self._panel_selection = panel_selection # map pipe types to their pipe display and remaining count self._pipes = {} self.draw_pipes() self.redraw() def draw_pipes(self): """Draw all the pipes in the selection panel""" for pipe_type in self._playable: image = get_image(f"images/{pipe_type}0") pipe_frame = tk.Frame(self, highlightthickness=2) selection = tk.Label(pipe_frame, image=image) selection.image = image selection.pack(side=tk.TOP) selection.bind("", lambda e, pipe=pipe_type: self._handle_click(pipe)) number = tk.Label(pipe_frame, text=f"{self._playable[pipe_type]}") number.pack(side=tk.TOP) pipe_frame.pack(side=tk.TOP) pipe_frame.bind("", lambda e, pipe=pipe_type: self._handle_click(pipe)) self._pipes[pipe_type] = (pipe_frame, number) def redraw(self, selected=None): """Update the pipes with current information - Sets the outline of selected pipes to red - Updates the amount of remaining pipes """ for pipe, (frame, number) in self._pipes.items(): if pipe == selected and self._playable[selected] > 0: border = "red" else: border = "white" frame.config(highlightbackground=border) number.config(text=f"{self._playable[pipe]}") def _handle_click(self, pipe): """Called when a pipe is clicked, handling calling the callback panel_selection method""" if self._panel_selection is not None: self._panel_selection(pipe) 主桌面 试图 class BoardView(tk.Canvas): """View of the Pipe game board""" def __init__(self, master, board_layout, place_pipe=None, remove_pipe=None, *args, **kwargs): """Construct a board view from a board_layout. Parameters: master (tk.Widget): Widget within which the board is placed. board_layout (list<list>): 2D array of tiles in a board. place_pipe (callable): Callable to call when a pipe is being placed. remove_pipe (callable): Callable to call when a pipe is being removed. """ super().__init__(master, *args, **kwargs) self._master = master self._board_layout = board_layout self.place_pipe = place_pipe self.remove_pipe = remove_pipe self._board = self.load_board() def load_board(self): """(list<list>) Create a 2D array of labels representing the board to display.""" labels = [] for y, row in enumerate(self._board_layout): board_row = [] for x, tile in enumerate(row): placement = tk.Label(self, text="T") placement.grid(column=x, row=y, ipady=4, ipadx=4) self.bind_clicks(placement, tile, (y, x)) board_row.append(placement) labels.append(board_row) return labels def redraw(self): """Redraw the game board by updating the images displayed in each grid""" for y, row in enumerate(self._board_layout): for x, tile in enumerate(row): position = (y, x) image = self._load_tile_image(tile) placement = self._board[y][x] placement.config(image=image) placement.image = image self.bind_clicks(placement, tile, position) def bind_clicks(self, label, tile, position): """Bind clicks on a label to the left and right click handlers. Parameters: label (tk.Widget): Label which clicks should bound to. tile (Tile): Tile to pass as a parameter to the handlers. position (tuple): Position to pass as a parameter to the handlers. """ # bind left click label.bind("", lambda e, tile=tile, position=position: self._handle_left_click(tile, position)) # bind right click # right click can be either Button-2 or Button-3 depending on operating system for i in range(2, 4): label.bind(f"", lambda e, tile=tile, position=position: self._handle_right_click(tile, position)) def _handle_left_click(self, pipe, position): """Handle left clicking on a tile to place a pipe. Calls the provided place_pipe method if available and pipe is selectable. """ if self.place_pipe is not None and pipe.can_select(): self.place_pipe(position) def _handle_right_click(self, pipe, position): """Handle right clicking on a tile""" if self.remove_pipe is not None and pipe.get_id() == "pipe" and pipe.can_select(): if pipe.get_name() in PIPES.values(): self.remove_pipe(position) def _load_tile_image(self, tile): """Load the PhotoImage to use for a given tile. If the tile class has not been fully implemented defaults to images/tile """ try: if tile.get_id() != "tile": image = get_image(f"images/{tile.get_name()}{tile.get_orientation()}") else: image = get_image(f"images/{tile.get_name()}") except AttributeError: print("get_name(), get_orientation() and get_id() methods need to be implemented correctly.", "\n", "Make sure all class attributes are defined correctly.", "\n") image = get_image("images/tile") return image 游戏主代码 class GameApp: """Game application that manages communication between the selection panel, board view and game model.""" def __init__(self, master): """Create a new game app within a master widget""" self._master = master self._level = "" self._game = PipeGame() self._selected = None # initialise GUI variables that are assigned in the draw method self._selection, self._board_view, self._button_frame = None, None, None self.draw() def select_pipe(self, pipe): """Select a pipe to be placed from the selection panel. Parameters: pipe (Pipe): The selected pipe. """ # ignore selection if not enough pipes available if self._game.get_playable_pipes()[pipe] <= 0: return # unselect if pipe is clicked twice if self._selected == pipe: pipe = None self._selected = pipe self._selection.redraw(selected=self._selected) def place_pipe(self, position): """Place the selected pipe on the game board. Parameters: position (tuple): The position to place the pipe within the board. """ selected = self._selected # tile at the placing position tile = self._game.get_pipe(position) if tile.can_select() and selected is not None and tile.get_id() == "tile": self._game.set_pipe(Pipe(selected), position) # rotate already placed pipes if tile.get_id() == "pipe": tile.rotate(1) # unselect when placed self._selected = None self._selection.redraw() self._board_view.redraw() self.check_game_over() def remove_pipe(self, position): """Remove the pipe at the given position Parameters: position (tuple): The position to remove the pipe from. """ self._game.remove_pipe(position) self._board_view.redraw() self._selection.redraw() def check_game_over(self): """Check if the game is over and exit if so""" if self._game.check_win(): messagebox.showinfo("Game Over", "You won! :D") self._master.destroy() def new_game(self): """Start a new level from the user inputted selection.""" self._level = simpledialog.askstring("Input", "What level would you like to play?", parent=self._master) if self._level is not None: self.reset_game() def reset_game(self): """Restart the game on the current level.""" if self._level == "": self._game = PipeGame() else: self._game = PipeGame(self._level) self.redraw() def redraw(self): """Redraw the whole game window.""" self._selection.destroy() self._board_view.destroy() self._button_frame.destroy() self.draw() def draw(self): """Draw the game to the master widget.""" try: self._selection = SelectionPanel(self._master, self._game.get_playable_pipes(), self.select_pipe) self._selection.pack(side=tk.LEFT) except AttributeError: print("get_playable_pipes() method needs to be implemented correctly.", "\n") try: self._board_view = BoardView(self._master, self._game.get_board_layout(), self.place_pipe, self.remove_pipe) self._board_view.redraw() self._board_view.pack(side=tk.LEFT) except AttributeError: print("get_board_layout() method needs to be implemented correctly.", "\n") self._button_frame = tk.Frame(self._master) restart_button = tk.Button(self._button_frame, text="Restart", command=self.reset_game) restart_button.pack(side=tk.TOP) new_button = tk.Button(self._button_frame, text="New Game", command=self.new_game) new_button.pack(side=tk.TOP) self._button_frame.pack(side=tk.LEFT) def get_image(image_name): """(tk.PhotoImage) Get a image file based on capability. If a .png doesn't work, default to the .gif image. """ try: image = tk.PhotoImage(file=image_name + ".png") except tk.TclError: image = tk.PhotoImage(file=image_name + ".gif") return image def main(): root = tk.Tk() root.title("Game") GameApp(root) root.update() root.mainloop() 最后 就可以开心的玩游戏啦 !!! main()

游戏


作者:Varalpha



小游戏 tkinter pipe 管道

需要 登录 后方可回复, 如果你还没有账号请 注册新账号