zoukankan      html  css  js  c++  java
  • 蛋疼之作:99行代码的2048

    基于Python和numpy,自带基于Tk最简仿原生2048配色的图形界面。文件代码行数(Physical LOC)一共99,没有统计过逻辑行数,因为是Python。

    本来是想写个最简单的内核然后用机器学习算策略的,可是写了几行又不想写了,索性加了个GUI做个最简版的2048,所以完全没有效率和Pythonic可言。不过只是作为2048模块的话10ms和1ms能有什么区别呢。总之这就是一个蛋疼的作品,写它最主要的原因是无聊。

    对2048游戏规则的理解:

    1) 方块合并时从滑动所指方向开始合并,合并不递归,比如:

    |-|2|2|2|向右滑动后,最右两块合并,变为|-|-|2|4|

    |2|2|2|2|向右滑动后,最右和最左两块分别合并,变为|-|-|4|4|

    2) 每次滑动后如果方块发生了变化则生成新方块,新方块的取值集为{2, 4},4出现的概率是0.1,初始化游戏产生两个新方块。

    3) 游戏的分数是累加每次合并得到的新方块的值。

    代码分为两个模块:

    1) 核心模块,用一个4x4矩阵表示游戏中方块的数值,没有方块的位置值为nan。基本思想是用concatenate函数实现方块的移动,用将矩阵扩展成5x4并错开一行和原矩阵相减得到列方向相邻相同的方块位置。

    按照滑动方向先将矩阵中的非空元素全部排列到每行的最右,用numpy的concatenate函数和isnan函数实现

    将得到的已经移动过的矩阵旋转,然后分别在第一行和行尾插入新的空行,得到两个5x4的矩阵

    这两个矩阵相减之后相邻并等值的位置就会得到0,这里我们定义空值和任何值得减法仍然得到空值,在numpy中对应的是nan和任何float的加法

    注意执行完上面的合并操作后,如果是一排4个方块等值,则新合成的两个方块不相邻,所以还需要再将矩阵转回去执行一遍移动操作才能确保一排4个值都相等的情况下正确的移动并合并方块。

    模块代码如下:

     1 from numpy import random, array, zeros, append, sum, concatenate, copy, ndenumerate, isnan, rot90, nan, int, float
     2 DOWN, RIGHT, UP, LEFT = range(4)
     3 
     4 class Game2048:
     5     def __init__(self):
     6         self._grid, self._score = zeros(16) + nan, 0
     7         self._grid[random.choice(16, 2, replace=False)] = random.choice([2]*9+[4], 2, replace=False) # init with 2 tiles
     8         self._grid = self._grid.reshape((4, 4))  # create 4x4 grid
     9 
    10     @staticmethod
    11     def _merge_down(grid):
    12         merge = concatenate((grid, [zeros(4) + nan])) - concatenate(([zeros(4) + nan], grid))  # find the mergable tiles
    13         merge[2][merge[3]==0], merge[1][merge[2]==0] = nan, nan     # remove redundant 0 by 3 same tiles
    14         score = sum(grid[merge[:4] == 0])
    15         grid[merge[:4] == 0], grid[merge[1:] == 0] = grid[merge[:4] == 0] * 2, nan # fill the merged  with new number
    16         return score
    17 
    18     def _create_tiles(self):
    19         avail = isnan(self._grid)
    20         if avail[avail==True].size > 0:
    21             new_tiles = append(random.choice([20]*9+[40]), zeros(avail[avail==True].size - 1) + nan)
    22             random.shuffle(new_tiles)
    23             self._grid[avail] = new_tiles
    24 
    25     def step(self, direction):
    26         self._grid[self._grid%10==0] /= 10
    27         merge_v, merge_h, grid_copy = copy(self._grid), copy(rot90(self._grid)), copy(self._grid)
    28         map(Game2048._merge_down, [merge_v, merge_h])       # try to merge tiles along two directions
    29         if merge_v[isnan(merge_v)].size is 0 and merge_h[isnan(merge_h)].size is 0:         # Check if game is over
    30             return False
    31         self._grid = rot90(self._grid, RIGHT - direction)
    32         self._grid = array([concatenate((x[isnan(x)], x[~isnan(x)])) for x in self._grid])  # move tiles
    33         self._grid = rot90(self._grid, -1)
    34         self._score += Game2048._merge_down(self._grid)                                     # merge tiles
    35         self._grid = rot90(self._grid, 1)
    36         self._grid = array([concatenate((x[isnan(x)], x[~isnan(x)])) for x in self._grid])  # move tiles
    37         self._grid = rot90(self._grid, direction - RIGHT)
    38         if not ((self._grid == grid_copy) | (isnan(self._grid) & isnan(grid_copy))).all():
    39             self._create_tiles()
    40         return True
    41 
    42     def get_grid(self):
    43         grid = copy(self._grid)
    44         grid[grid%10==0] /= 10
    45         return grid
    46 
    47     def get_new_tiles(self):
    48         grid = zeros((4, 4), int)
    49         grid[self._grid%10==0] = 1
    50         return grid
    51 
    52     def get_score(self):
    53         return self._score

    2) GUI模块,直接捕捉键盘的上下左右4个按键移动方块,窗体标题栏显示游戏相关信息,无动画,新出现的方块用橙色表示,使用大多数Python版本中内置的GUI库Tk实现,代码如下:

     1 from Tkinter import Tk, Label, Frame, BOTH
     2 from tkFont import Font
     3 from game2048 import Game2048, UP, DOWN, LEFT, RIGHT, ndenumerate, copy, isnan
     4 
     5 key_map = {'Up': UP, 'Down': DOWN, 'Left': LEFT, 'Right': RIGHT}
     6 color_map = {2: ('#776e65', '#eee4da'), 4: ('#776e65', '#ede0c8'), 8: ('#f9f6f2', '#f2b179'), 16: ('#f9f6f2', '#f2b179'),
     7              32: ('#f9f6f2', '#f67c5f'), 64: ('#f9f6f2', '#f65e3b'), 128:('#f9f6f2', '#edcf72'), 256: ('#f9f6f2', '#edcc61'),
     8              512: ('#f9f6f2', '#edc850'), 1024: ('#f9f6f2', '#edc53f'), 2048: ('#f9f6f2', '#edc22e'), 'base': '#ccc0b3'}
     9 color_map.update(dict.fromkeys([2**x for x in range(12, 18)], ('#f9f6f2', '#3c3a32')))
    10 
    11 def input_listener(event=None, game=None, tk_root=None, labels=None):
    12     key = '{}'.format(event.keysym)
    13     if key in key_map and game and labels and tk_root:
    14         if game.step(key_map[key]):
    15             grid, new_tiles, score = game.get_grid(), game.get_new_tiles(), int(game.get_score())
    16             max_tile = int(grid[~isnan(grid)].max())
    17             tk_root.title('Move tiles to get {}! Score: {}'.format(2048 if max_tile < 2048 else max_tile * 2, score))
    18             for (i, j), value in ndenumerate(grid):
    19                 text = '{}'.format('' if isnan(grid[i][j]) else int(grid[i][j]))
    20                 font_color = color_map[32][1] if new_tiles[i][j] else color_map['base'] if isnan(value) else color_map[value][0]
    21                 labels[4*i+j].config(text=text, fg=font_color, bg=color_map['base'] if isnan(value) else color_map[value][1])
    22         else:
    23             grid, new_tiles, score = game.get_grid(), game.get_new_tiles(), int(game.get_score())
    24             max_tile = int(grid[~isnan(grid)].max())
    25             [labels[i].config(text='' if i < 4 or i > 11 else 'GAMEOVER'[i-4], bg=color_map['base']) for i in xrange(16)]
    26             tk_root.title('Game Over! Tile acheived: {}, Score: {}'.format(max_tile, score))
    27 
    28 if __name__ == '__main__':
    29     game, root, window_size = Game2048(), Tk(), 360
    30     root.title('Move tiles to get 2048! Score: 0')
    31     root.geometry('{0}x{0}+111+111'.format(window_size))
    32     root.config(background='#bbada0')
    33 
    34     grid, labels = game.get_grid(), []
    35     for (i, j), value in ndenumerate(grid):
    36         frame = Frame(root, width=window_size/4-2, height=window_size/4-2)
    37         font = Font(family='Helvetica', weight='bold', size=window_size/15)
    38         frame.pack_propagate(0)
    39         frame.place(x=j*window_size/4+1, y=i*window_size/4+1)
    40         (text, color) = ('', color_map['base']) if isnan(value) else ('{}'.format(int(value)), color_map[value][0])
    41         label = Label(frame, text=text, font=font, fg=color, bg=color_map['base'] if isnan(value) else color_map[value][1])
    42         label.pack(fill=BOTH, expand=True)
    43         labels.append(label)
    44 
    45     root.bind_all('<Key>', lambda event: input_listener(event, game=game, tk_root=root, labels=labels))
    46     root.mainloop()

    发布在github上了,还没有完全测试过,欢迎fork。

    GitHub: https://github.com/frombeijingwithlove/mini2048

  • 相关阅读:
    vmware 更换网络后不能上网
    IDEA “Cannot resolve symbol” 解决办法
    SpringBoot MyBatis druid数据库连接池
    解决分页插件ClassNotFoundException: org.springframework.boot.bind.RelaxedPropertyResolver
    纯CSS绘制不同角度的三角形
    原生js实现移动端点击、长按、左滑、右滑、上滑、下滑等事件模拟
    移动端下拉滚动刷新
    使用锚点定位不改变url同时平滑的滑动到锚点位置,不会生硬的直接到锚点位置
    简单的mock数据调试
    小程序textarea文本域字数控制---并显示已输入字数
  • 原文地址:https://www.cnblogs.com/frombeijingwithlove/p/4111016.html
Copyright © 2011-2022 走看看