彩色方块连连看
本篇除了介绍怎样用pyglet制作连连看游戏,还将介绍如果使用自定义库colorlib,用它来描绘游戏中多种颜色的彩色方块。自定义库colorlib的由来,另请阅读《python 教你如何创建一个自定义库 colorlib.py》,
第一步
见原文中,有一个创建随机彩色霓虹方块的代码:
import pyglet from colorlib import randcolorTuples as ctpl window = pyglet.window.Window(800, 600, caption='色块展示') color = ctpl(16) batch = pyglet.graphics.Batch() shape = [pyglet.shapes.Rectangle(180+i%4*120, 120+i//4*100, 100, 80, color=color[i], batch=batch) for i in range(len(color))] @window.event def on_draw(): window.clear() batch.draw() def change_color(e): for i,c in enumerate(ctpl(16)): shape[i].color = c pyglet.clock.schedule_interval(change_color, 0.1) pyglet.app.run()
运行效果:
第二步
创建更多方块,方块大小由参数决定;随机色由colorlib.randcolorTuples函数产生。
1.from pyglet import * from colorlib import randcolorTuples as ctpl W, H = 800, 600 window = window.Window(W, H, caption='色块展示') batch = graphics.Batch() row, col, space = 8, 10, 5 width = w = (W-space)//col-space height = h = (H-space)//row-space x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 color = ctpl(row*col) shape = [[1]*col for _ in range(row)] for r,arr in enumerate(shape): for c,num in enumerate(arr): shape[r][c] = shapes.Rectangle(x0+c*(w+space), y0+r*(h+space), w, h, color=color[c+r*len(arr)], batch=batch) @window.event def on_draw(): window.clear() batch.draw() def change_color(e): for i,c in enumerate(ctpl(row*col)): shape[i//col][i%col].color = c clock.schedule_interval(change_color, 0.1) app.run()
第三步
取消“闪烁”,方块增加边框并改写为Box类、增加一个对应存放方块状态的列表Array。
from pyglet import * from colorlib import * ctpl = randcolorTuples W, H = 800, 600 window = window.Window(W, H, caption='色块展示') gl.glClearColor(*Color('lightblue3').decimal) batch = graphics.Batch() row, col, space = 8, 10, 5 width = w = (W-space)//col-space height = h = (H-space)//row-space x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 color = ctpl(row*col) Array = [[1]*col for _ in range(row)] # 用于控制方块的状态 class Box: def __init__(self, x, y, w, h, color, batch=batch): self.rect = shapes.Rectangle(x, y, w, h, color=color, batch=batch) self.box = shapes.Box(x, y, w, h, color=(255,255,255), thickness=3, batch=batch) def visible(self, visible=True): self.box.batch = self.rect.batch = batch if visible else None Array[0][0] = Array[2][2] = Array[5][7] = 0 # 设置三个方块消失 for r,arr in enumerate(Boxes:=[_[:] for _ in Array]): for c,_ in enumerate(arr): Boxes[r][c] = Box(x0+c*(w+space), y0+r*(h+space), w, h, color[c+r*len(arr)]) Boxes[r][c].visible(Array[r][c]) @window.event def on_draw(): window.clear() batch.draw() app.run()
运行效果:
注:顺带测试一下在for...in...语句enumerate()函数中使用海象操作符:=。
for r,arr in enumerate(Boxes:=[_[:] for _ in Array]):
第四步
限制生产颜色时,每种颜色只能出现4次,即2组连连看。
COLOR = [] while len(COLOR)<row*col//4: if (c:=randcolorTuple()) not in COLOR: COLOR.append(c) COLOR = sample(COLOR*4, row*col
from pyglet import * from colorlib import * ctpl = randcolorTuples W, H = 800, 600 window = window.Window(W, H, caption='色块展示') gl.glClearColor(*Color('lightblue3').decimal) batch = graphics.Batch() row, col, space = 8, 10, 5 width = w = (W-space)//col-space height = h = (H-space)//row-space x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 COLOR = [] while len(COLOR)<row*col//4: if (c:=randcolorTuple()) not in COLOR: COLOR.append(c) COLOR = sample(COLOR*4, row*col) class Box: def __init__(self, x, y, w, h, color, batch=batch): self.rect = shapes.Rectangle(x, y, w, h, color=color, batch=batch) self.box = shapes.Box(x, y, w, h, color=(255,255,255), thickness=3, batch=batch) self.visible = True def visible(self, visible=True): self.box.batch = self.rect.batch = batch if visible else None Array, Boxes = [[[1]*col for _ in range(row)] for _ in range(2)] for r,arr in enumerate(Boxes): for c,_ in enumerate(arr): Boxes[r][c] = Box(x0+c*(w+space), y0+r*(h+space), w, h, COLOR[c+r*len(arr)]) @window.event def on_draw(): window.clear() batch.draw() app.run()
运行效果:
第五步
增加Game类,并加入点击坐标和状态,可以记录方块的位置和状态。
方块的行列坐标与窗口中实际坐标的转换:
r, c = (y-y0)//(h+space), (x-x0)//(w+space)
from pyglet import * from colorlib import * W, H = 800, 600 window = window.Window(W, H, caption='色块展示') gl.glClearColor(*Color('lightblue3').decimal) batch = graphics.Batch() row, col, space = 8, 10, 5 width = w = (W-space)//col-space height = h = (H-space)//row-space x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 COLOR = [] while len(COLOR)<row*col//4: if (c:=randcolorTuple()) not in COLOR: COLOR.append(c) COLOR = sample(COLOR*4, row*col) Array, Boxes = [[[[11]]*col for _ in range(row)] for _ in range(2)] class Box: def __init__(self, x, y, w, h, color, batch=batch): self.x, self.y = x, y self.w, self.h = w, h self.visible, self.pressed = True, False self.rect = shapes.Rectangle(x, y, w, h, color=color, batch=batch) self.box = shapes.Box(x, y, w, h, color=(255,255,255), thickness=3, batch=batch) def show(self, visible=True): self.visible, self.pressed = visible, False self.box.batch = self.rect.batch = batch if visible else None def hide(self): self.show(False) def on_mouse_over(self, x, y): return self.x<=x<=self.x+self.w and self.y<=y<=self.y+self.h def on_mouse_click(self): if self.visible: self.pressed = True if Color('RED').equal(self.box.color): self.hide() else: self.pressed = False self.show() self.box.color = Color('RED' if self.pressed else 'WHITE').rgba for r,arr in enumerate(Boxes): for c,_ in enumerate(arr): Boxes[r][c] = Box(x0+c*(w+space), y0+r*(h+space), w, h, COLOR[c+r*len(arr)]) class Game: def __init__(self): self.array = Array self.boxes = Boxes def on_mouse_click(self, x, y): r, c = (y - y0)//(h+space), (x - x0)//(w+space) if r in range(row) and c in range(col) and self.boxes[r][c].on_mouse_over(x, y): self.boxes[r][c].on_mouse_click() self.array[r][c] = self.boxes[r][c].visible*10+self.boxes[r][c].pressed return r, c, self.array[r][c] game = Game() @window.event def on_mouse_press(x, y, dx, dy): window.set_caption(f'色块展示——坐标和状态:{game.on_mouse_click(x, y)}') @window.event def on_draw(): window.clear() batch.draw() app.run()
运行效果:
第六步
增加连线功能,用shapes.Line画一根淡金色的直线来表示:
self.line = shapes.Line(0, 0, 0, 0, width=5, color=Color('light gold').rgba)
再增加一个属性self.click来存储一组连线方块的坐标;增加方法getxy(),以获取方块在窗口中的实际坐标:
def getxy(self, row, col):
return x0+col*(w+space)+w//2, y0+row*(h+space)+h//2
from pyglet import * from colorlib import * W, H = 800, 600 window = window.Window(W, H, caption='彩色色块连连看') gl.glClearColor(*Color('lightblue3').decimal) batch = graphics.Batch() group = graphics.Group() row, col, space = 8, 10, 5 width = w = (W-space)//col-space height = h = (H-space)//row-space x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 COLOR = [] while len(COLOR)<row*col//4: if (c:=randcolorTuple()) not in COLOR: COLOR.append(c) COLOR = sample(COLOR*4, row*col) Array, Boxes = [[[1]*col for _ in range(row)] for _ in range(2)] class Box: def __init__(self, x, y, w, h, color, batch=batch): self.x, self.y = x, y self.w, self.h = w, h self.pressed = False self.rect = shapes.Rectangle(x, y, w, h, color=color, batch=batch) self.box = shapes.Box(x, y, w, h, color=Color('WHITE').rgba, thickness=3, batch=batch) self.box.group = group def hide(self): self.box.batch = self.rect.batch = None def on_mouse_over(self, x, y): return self.x<=x<=self.x+self.w and self.y<=y<=self.y+self.h for r,arr in enumerate(Boxes): for c,_ in enumerate(arr): Boxes[r][c] = Box(x0+c*(w+space), y0+r*(h+space), w, h, COLOR[c+r*len(arr)]) class Game: def __init__(self): self.array = Array self.boxes = Boxes self.click = [] self.last = None self.row, self.col = 0, 0 self.line = shapes.Line(0, 0, 0, 0, width=5, color=Color('light gold').rgba) self.line.batch = batch self.line.group = group self.line.visible = False def on_mouse_click(self, x, y): if self.line.visible: return r, c = (y-y0)//(h+space), (x-x0)//(w+space) if r in range(row) and c in range(col) and self.boxes[r][c].on_mouse_over(x, y) and self.array[r][c]: self.row, self.col = r, c if len(self.click)==0: self.click.append((r,c)) self.last = r, c, self.boxes[r][c] self.boxes[r][c].box.color = Color('RED').rgba elif len(self.click)==1: if (r,c) in self.click: r,c = self.click.pop() self.boxes[r][c].box.color = Color('WHITE').rgba else: self.click.append((r,c)) self.boxes[r][c].box.color = Color('RED').rgba r2, c2 = self.click[0][0], self.click[0][1] self.line.x, self.line.y = self.getxy(r, c) self.line.x2, self.line.y2 = self.getxy(r2, c2) self.array[r2][c2] = self.array[r][c] = 0 self.line.visible = True self.click.clear() clock.schedule_interval(self.update, 0.3) return r, c, self.array[r][c] def getxy(self, row, col): return x0+col*(w+space)+w//2, y0+row*(h+space)+h//2 def update(self, event): self.line.visible = False one, another = self.last, self.boxes[self.row][self.col] if one[-1].rect.color==another.rect.color: one[-1].hide(); another.hide() self.last = None else: self.array[self.row][self.col] = self.array[one[0]][one[1]] = 1 one[-1].box.color = another.box.color = Color('WHITE').rgba clock.unschedule(self.update) @window.event def on_mouse_press(x, y, dx, dy): window.set_caption(f'彩色色块连连看——坐标和状态:{game.on_mouse_click(x, y)}') @window.event def on_draw(): window.clear() batch.draw() game = Game() app.run()
运行效果:
第七步
继续优化代码,放弃self.click记录方块的坐标,改为直接用self.last和self.last2来记录一组方块,同样也能操作它们的位置和状态;增加判断任务完成的方法。
def success(self):
return sum(sum(self.array,[]))==0
再增加一个记录行列坐标的类,方便代码的书写:
class RC: def __init__(self, x=0, y=0): self.r, self.c = x, y def __eq__(self, other): return self.rc == other.rc @property def rc(self): return self.r, self.c
from pyglet import * from colorlib import * W, H = 800, 600 window = window.Window(W, H, caption='彩色色块连连看') gl.glClearColor(*Color('lightblue3').decimal) batch, group = graphics.Batch(),graphics.Group() row, col, space = 8, 10, 5 w, h = W//col-space*2, H//row-space*2 x0, y0 = (W-(w+space)*col+space)//2, (H-(h+space)*row+space)//2 COLOR = [] while len(COLOR)<row*col//4: if (c:=randcolorTuple()) not in COLOR: COLOR.append(c) COLOR = sample(COLOR*4, row*col) Array, Boxes = [[[1]*col for _ in range(row)] for _ in range(2)] class Box: def __init__(self, x, y, w, h, color, batch=batch): self.x, self.y, self.w, self.h = x, y, w, h self.rect = shapes.Rectangle(x, y, w, h, color=color, batch=batch) self.box = shapes.Box(x, y, w, h, color=Color('WHITE').rgba, thickness=3, batch=batch) self.box.group = group def hide(self): self.box.batch = self.rect.batch = None def on_mouse_over(self, x, y): return self.x<=x<=self.x+self.w and self.y<=y<=self.y+self.h for r,arr in enumerate(Boxes): for c,_ in enumerate(arr): Boxes[r][c] = Box(x0+c*(w+space), y0+r*(h+space), w, h, COLOR[c+r*len(arr)]) class RC: def __init__(self, x=0, y=0): self.x, self.y = x, y def __eq__(self, other): return self.rc == other.rc @property def rc(self): return self.x, self.y class Game: def __init__(self): self.array = Array self.boxes = Boxes self.rc, self.rc2 = RC(), RC() self.last, self.last2 = None, None self.line = shapes.Line(0, 0, 0, 0, width=5, color=Color('light gold').rgba, batch=batch, group=group) self.line.visible = False def on_mouse_click(self, x, y): if self.line.visible or self.success(): return r, c = (y-y0)//(h+space), (x-x0)//(w+space) if r in range(row) and c in range(col) and self.boxes[r][c].on_mouse_over(x, y) and self.array[r][c]: if self.last is None and self.last2 is None: self.rc, self.last = RC(r, c), self.boxes[r][c] self.last.box.color = Color('RED').rgba elif self.last is not None and self.last2 is None: self.rc2, self.last2 = RC(r, c), self.boxes[r][c] self.last2.box.color = Color('RED').rgba if self.rc == self.rc2: self.last.box.color = Color('WHITE').rgba self.last, self.last2 = None, None else: self.line.x, self.line.y = self.getxy(r, c) self.line.x2, self.line.y2 = self.getxy(self.rc.x, self.rc.y) self.line.visible = True clock.schedule_interval(self.update, 0.3) return (r, c), Color(self.boxes[r][c].rect.color).name def getxy(self, row, col): return x0+col*(w+space)+w//2, y0+row*(h+space)+h//2 def update(self, event): self.line.visible = False clock.unschedule(self.update) if self.last.rect.color==self.last2.rect.color: self.last.hide(); self.last2.hide() self.array[self.rc.x][self.rc.y] = self.array[self.rc2.x][self.rc2.y] = 0 else: self.last.box.color = self.last2.box.color = Color('WHITE').rgba self.last, self.last2 = None, None if game.success(): window.set_caption('彩色色块连连看——任务完成!') def success(self): return sum(sum(self.array,[]))==0 @window.event def on_draw(): window.clear() batch.draw() @window.event def on_mouse_press(x, y, dx, dy): ret = game.on_mouse_click(x, y) if ret and not game.success(): window.set_caption(f'彩色色块连连看——坐标:{ret[0]} 颜色:{ret[1]}') game = Game() app.run()
运行效果:
动态效果展示
为节省截图时间把第9行代码的8行10列暂改为4行5列。
小结
至此,一个代码不到100行的简单的“彩色方块连连看”游戏完成了。下一期文章将继续探讨“连连看”的直线规则,即不能有斜线连接方块,只能由水平和垂直的直线相连方块且直线最多转弯2次。
待续......
附录
colorlib库Color类的主代码介绍
class Color: ''' Color(3-tuple: tuple) -> Color # 3-tuple as (r, g, b) Color(4-tuple: tuple) -> Color # 4-tuple as (r, g, b, a) Color(color_name: str) -> Color # color_name as 'Red','Blue',... Color(color_string: str) -> Color # color_string as '#rrggbb' Object for color representations. ''' def __init__(self, r=0, g=0, b=0, a=None): self.__alpha = (a is not None) and isinstance(a, (int, float)) if all(map(lambda r:isinstance(r, (int, float)),(r,g,b))): self.r, self.g, self.b = map(lambda n:int(n)%256,(r,g,b)) elif isinstance(r, (tuple, list)) and len(r) in (3, 4): self.r, self.g, self.b, self.a = *[int(c)%256 for c in r[:3]], 255 if len(r)==4: a, self.__alpha = int(r[3])%256, True elif isinstance(r, str) and len(r)==7 and r.startswith('#'): self.r, self.g, self.b = str2tuple(r) elif isinstance(r, str): if (rgb := ColorDict.get(r, None)) is None: raise ValueError("Invalid Color Name") self.r, self.g, self.b, a = *rgb, 255 else: raise ValueError("Invalid argument for class Color") self.a = a if self.__alpha else 255 self.rgb = self.r, self.g, self.b self.rgba = self.r, self.g, self.b, self.a self.value = self.rgba if self.__alpha else self.rgb self.string = tuple2str(self.value[:3]) self.decimal = tuple(map(lambda x:x/255, self.rgba)) self.name = {v:k for k,v in ColorDict.items()}.get(self.rgb, 'Noname') def __repr__(self): rgba = 'RGBA(' if self.__alpha else 'RGB(' return ', '.join(map(lambda x:str(x).rjust(3),self.value)).join((rgba,')')) def randcolor(self): '''Convert the Color to any random color in ColorDict.keys().''' rgb = randcolorTuple() return Color(*rgb, self.a) if self.__alpha else Color(rgb) def random(self): '''Convert rgb to a 3-tuple of random integer between 0 and 255.''' rgb = randint(0,255), randint(0,255), randint(0,255) return Color(*rgb, self.a) if self.__alpha else Color(rgb) def alpha(self, a=255): '''Set alpha value of the Color, or change RGB to RGBA.''' self.__init__(*self.rgb, a) def equal(self, other): '''Compare self.rgba with another color's RGBA tuple.''' return self.rgba == other
其中,属性 self.decimal 用在了窗口背景设置的代码上:
gl.glClearColor(*Color('lightblue3').decimal)
游戏代码中还用到以下属性:
self.rgba = self.r, self.g, self.b, self.a
self.name = {v:k for k,v in ColorDict.items()}.get(self.rgb, 'Noname')
完