Browse Source

开发:探索随机生成

xlxin 1 year ago
parent
commit
5536741e7e
2 changed files with 442 additions and 0 deletions
  1. 226 0
      TileManor/scripts/rnn.py
  2. 216 0
      TileManor/scripts/tileBlocks.py

+ 226 - 0
TileManor/scripts/rnn.py

@@ -0,0 +1,226 @@
+#!/usr/local/bin/python3
+# -*- coding: utf-8 -*-
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torch.utils.data as tudata
+import numpy as np
+import sys
+import json
+import os
+
+#将数据输出到一个json文件,便于在tiled进行观察,可以是单层或者多层的,多层的话,层级按照从小到大排列(层级是布局层级,不是视觉层级)
+def output_to_json(slices, suffix="ex"):
+    with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0000.json", 'r') as f:
+        json_temp = json.load(f)
+        for i in range(0, len(slices)):
+            with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/slices/slice_%d_%s.json" % (i, suffix), "w") as out_f:
+                json_temp["layers"][0]["data"] = slices[i]
+                json.dump(json_temp, out_f)
+
+# 解析一个关卡文件,得到各层的数据,是按照视觉层次划分的
+def get_each_view_slice(jsonLv):
+    jsonData = ""
+    tilesByPos = {}
+    maxZ = 0
+    # 处理各层的tile数据
+    with open(jsonLv, 'r') as f:#, encoding='utf-8'
+        jsonData = json.load(f)
+        # 得到宽高信息
+        w = int(jsonData['width'])
+        h = int(jsonData['height'])
+        for item in jsonData['layers']:
+            name = item['name']
+            if not name.startswith('Tile_'):
+                # stacked
+                continue
+            z = int(name[5:])
+            data = item['data']
+            for i in range(0, len(data)):
+                if data[i] == 0:
+                    continue
+                tile_x = (int)(i % w)
+                tile_y = (int)(i / h)
+                tile_z = z
+                # 一些统计信息
+                tilesByPos[(tile_x, tile_y, tile_z)] = [1,0]    # [tile类型,zView--视觉层级,默认是0]
+                if tile_z > maxZ:
+                    maxZ = tile_z
+    # 根据上面的布局信息,计算每个tile的几个信息:
+    # 1. 每个tile的视觉层级(不同于上面z的信息,那是一个布局信息,相同的z可能出在不同的视觉层级)
+    # 从maxZ开始,逐层向下计算;对于每一个位置,如果该位置有tile,则计算该tile的视觉层级
+    # 对于每一个tile,如果其上方(从该tile的z+1层,一直到maxZ层)有tile,则其视觉层级为上方tile的视觉层级+1
+    slice_by_view_z = [[0]*w*h for i in range(maxZ+1)]
+    for z in range(0, maxZ+1):
+        z = maxZ - z
+        for x in range(0, w+1):
+            for y in range(0, h+1):
+                if (x,y,z) in tilesByPos:
+                    tile = tilesByPos[(x, y, z)]
+                    adjs = [(x,y),(x,y+1),(x,y-1),(x+1,y),(x+1,y+1),(x+1,y-1),(x-1,y),(x-1,y-1),(x-1,y+1)]
+                    # 确定视觉层级
+                    zvMax = -1
+                    for zup in range(z+1, maxZ+1):
+                        for adj in adjs:
+                            if (adj[0],adj[1],zup) in tilesByPos:
+                                zv = tilesByPos[adj[0],adj[1],zup][1]
+                                if zv > zvMax:
+                                    zvMax = zv
+                    zv = zvMax + 1
+                    tilesByPos[(x, y, z)][1] = zv
+                    # 记录到slice_by_view_z中
+                    slice_by_view_z[zv][y*w+x] = tilesByPos[(x, y, z)][0]
+    return slice_by_view_z
+
+# 测试获取视觉层级的数据是否ok
+def test_get_each_view_slice():
+    jsonLv = "/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0016.json"
+    slice_by_view_z = get_each_view_slice(jsonLv)
+    output_to_json(slice_by_view_z)
+
+##########################################
+# PyTorch RNN
+class TileMatchRNN(nn.Module):
+    def __init__(self, input_size, hidden_size, num_layers, output_size):
+        super(TileMatchRNN, self).__init__()
+        self.hidden_size = hidden_size
+        self.num_layers = num_layers
+        self.rnn = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)
+        self.fc = nn.Linear(hidden_size, output_size)
+        # 检查是否有可用的GPU
+        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+    def forward(self, x):
+        # Add an extra dimension for the batch size if necessary
+        if x.dim() == 2:
+            x = x.unsqueeze(0)
+        out, h = self.rnn(x)
+        out = self.fc(out[:, -1, :])
+        return out, h
+    
+# 全连接神经网络
+class TileMatchFullyConnectedNetwork(nn.Module):
+    def __init__(self, input_size, hidden_size, num_layers, output_size):
+        super(TileMatchFullyConnectedNetwork, self).__init__()
+        self.layer1 = nn.Linear(input_size, hidden_size)
+        self.layer2 = nn.Linear(hidden_size, output_size)
+        # 检查是否有可用的GPU
+        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+    def forward(self, x):
+        x = torch.relu(self.layer1(x))
+        x = self.layer2(x)
+        return x
+    
+# 变分自编码器
+class TileMatchVAE(nn.Module):
+    def __init__(self, input_size, hidden_size, latent_size):
+        super(TileMatchVAE, self).__init__()
+        self.encoder = nn.Sequential(
+            nn.Linear(input_size, hidden_size),
+            nn.ReLU(),
+            nn.Linear(hidden_size, latent_size * 2)  # We need mean and variance for each latent variable
+        )
+        self.decoder = nn.Sequential(
+            nn.Linear(latent_size, hidden_size),
+            nn.ReLU(),
+            nn.Linear(hidden_size, input_size),
+            nn.Sigmoid()  # To get outputs in the range [0, 1]
+        )
+
+    def reparameterize(self, mu, logvar):
+        std = torch.exp(0.5 * logvar)
+        eps = torch.randn_like(std)
+        return mu + eps * std
+
+    def forward(self, x):
+        h = self.encoder(x)
+        mu, logvar = h.chunk(2, dim=1)
+        z = self.reparameterize(mu, logvar)
+        return self.decoder(z), mu, logvar
+
+class TileMatchDataset(tudata.Dataset):
+    def __init__(self, directories):
+        self.data = []
+        for directory in directories:
+            for filename in os.listdir(directory):
+                filepath = os.path.join(directory, filename)
+                if os.path.isfile(filepath) and filepath.endswith('.json'):
+                    slices = get_each_view_slice(filepath)
+                    if len(slices) > 1:
+                        for i in range(0, len(slices)-1):
+                            self.data.append([slices[i], slices[i+1]])
+
+    def __len__(self):
+        return len(self.data)
+
+    def __getitem__(self, idx):
+        input_sequence = self.data[idx][0]
+        target_sequence = self.data[idx][1]
+        return torch.tensor(input_sequence, dtype=torch.float32), torch.tensor(target_sequence, dtype=torch.float32)
+
+# 模型的参数
+input_size = 900
+hidden_size = 50
+num_layers = 2
+output_size = 900
+
+def train_NN():
+    # Initialize model, loss function, optimizer
+    # model = TileMatchRNN(input_size, hidden_size, num_layers, output_size)
+    # criterion = nn.BCEWithLogitsLoss()  # 适合二分类问题
+    # optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
+
+    model = TileMatchVAE(input_size, 500, 20)
+    criterion = nn.BCEWithLogitsLoss()  # 适合二分类问题
+    optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
+
+    # 准备数据集
+    batch_size = 1
+    directories = ["/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/tf_templates",]
+    dataset = TileMatchDataset(directories)
+    train_loader = tudata.DataLoader(dataset, batch_size=batch_size, shuffle=True)
+
+    num_epochs = 10
+    for epoch in range(num_epochs):
+        for data in train_loader:  # Assume train_loader provides (input, target) pairs
+            inputs, targets = data
+            # outputs, _ = model(inputs)
+            x_recon, mu, logvar = model(inputs)
+            recon_loss = F.binary_cross_entropy(x_recon, inputs, reduction='sum')
+            kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
+            loss = recon_loss + kl_div
+
+            optimizer.zero_grad()
+            loss.backward()
+            optimizer.step()
+    return model
+
+if __name__ == '__main__':
+    # test_get_each_view_slice()
+    n = train_NN()
+    torch.save(n, '/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/nn_VAE.model')
+    # n = torch.load('/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/nn_VAE.model')
+    slices = get_each_view_slice("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0015.json")
+    n.eval()
+    # 准备输入数据
+    initial_input = torch.tensor(slices[0], dtype=torch.float32).unsqueeze(0)  # (1, 1, 900)
+
+    # 初始化隐藏状态
+    h0 = torch.zeros(num_layers, initial_input.size(0), hidden_size)
+
+    # 使用模型进行预测 生成6层
+    slices = []
+    for i in range(1):
+        with torch.no_grad():  # 在评估模式下,不需要计算梯度
+            # output, hn = n(initial_input)
+            output, mu, logvar = n(initial_input)
+            initial_input = output
+            # 应用阈值判断将浮点数转化为0或1
+            threshold = 0.9
+            output_layout = (torch.sigmoid(output) >= threshold).int()
+            # 将 output_layout 转化成一个[]
+            # slices.append([int(d) for d in output_layout[0]])
+            slices.append([int(d) for d in output_layout[0]])
+    output_to_json(slices, "nn_vae")

+ 216 - 0
TileManor/scripts/tileBlocks.py

@@ -0,0 +1,216 @@
+#!/usr/local/bin/python3
+# -*- coding: utf-8 -*-
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torch.utils.data as tudata
+import numpy as np
+import sys
+import json
+import os
+import sqlite3
+
+
+#将block信息保存到数据库中,方便后续查询
+class BlockDB:
+    def __init__(self):
+        self.blocks = []
+        self.conn = sqlite3.connect('blocks.db')
+        if self.conn is None:
+            print("open db failed")
+        else:
+            cursor = self.conn.cursor()
+            cursor.execute("SELECT * FROM blocks")
+            rows = cursor.fetchall()
+            for row in rows:
+                block = {}
+                block['id'] = row[0]
+                self.blocks.add(block)
+    def add_block(self, block):
+        # 检查必要信息是否存在
+        if 'id' not in block:
+            return
+        self.blocks.add(block)
+    def get_block(self, block):
+        if block in self.blocks:
+            return block
+        else:
+            return None
+    def dump(self):
+        cursor = self.conn.cursor()
+        cursor.execute("CREATE TABLE IF NOT EXISTS blocks (id TEXT)")
+        for block in self.blocks:
+            cursor.execute("INSERT INTO blocks (id) VALUES (?)", (block['id'],))
+        self.conn.commit()
+
+#将数据输出到一个json文件,便于在tiled进行观察,可以是单层或者多层的,多层的话,层级按照从小到大排列(层级是布局层级,不是视觉层级)
+def output_to_json(slices, suffix="ex"):
+    with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0000.json", 'r') as f:
+        json_temp = json.load(f)
+        for i in range(0, len(slices)):
+            with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/slices/slice_%d_%s.json" % (i, suffix), "w") as out_f:
+                json_temp["layers"][0]["data"] = slices[i]
+                json.dump(json_temp, out_f)
+
+def output_block(block, size, suffix):
+    with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0000.json", 'r') as f:
+        json_temp = json.load(f)
+        with open("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/blocks/block_%s.json" % (suffix,), "w") as out_f:    
+            for pos in block:
+                x,y,z = pos
+                # 0 是 mark
+                json_temp["layers"][z+1]["data"][y*size[1]+x] = '1'
+            json.dump(json_temp, out_f)
+
+def get_block_from(tiles_from, tiles_by_pos):
+    block = set()
+    stack = [p for p in tiles_from]
+    while len(stack) > 0:
+        tile = stack.pop()
+        block.add(tile)
+        x,y,z = tile
+        while z > 0:
+            for adj in [(0,0), (0,1), (0,-1), (1,0), (-1,0), (-1,-1), (1,1), (-1,1), (1,-1)]:
+                pos = (x+adj[0], y+adj[1], z-1)
+                if pos in tiles_by_pos:
+                    stack.append(pos)
+            z -= 1
+    return block
+
+# 将 tiles_at_top 按照连通性,划分成若干个组
+# tiles_at_top只有xy坐标,无z
+def split_shapes(tiles_at_top):
+    # 从第一个tile开始,逐个tile进行遍历,如果该tile的上下左右有tile,则将其加入到一个组中,直至所有的tile都被处理
+    shapes = []
+    for tile in tiles_at_top:
+        shape_aleary = None
+        for shape in shapes:
+            if tile in shape:
+                shape_aleary = shape
+                break
+        if shape_aleary == None:
+            shape_aleary = set()
+            shape_aleary.add(tile)
+            shapes.append(shape_aleary)
+        x,y = tile
+        for adj in [(0,2), (0,-2), (1,2), (1,-2), (-1,2), (-1,-2), (2,0), (-2,0), (2,1), (-2,1), (2,-1), (-2,-1)]:
+            adj_x = x + adj[0]
+            adj_y = y + adj[1]
+            pos = (adj_x, adj_y)
+            if pos in tiles_at_top:
+                # 如果该位置已经在某个shape里面,则将两个shape合并
+                shape1 = None
+                for shape in shapes:
+                    if pos in shape:
+                        shape1 = shape
+                        break
+                if shape1 != None and shape1 != shape_aleary:
+                    shape_aleary.update(shape1)
+                    shapes.remove(shape1)
+                else:
+                    shape_aleary.add(pos)
+    return shapes
+
+# 解析一个关卡文件,得到各层的数据,是按照视觉层次划分的
+def parse_blocks(jsonLv):
+    jsonData = ""
+    tilesByPos = {}
+    maxZ = 0
+    w = 0
+    h = 0
+    # 处理各层的tile数据
+    with open(jsonLv, 'r') as f:#, encoding='utf-8'
+        jsonData = json.load(f)
+        # 得到宽高信息
+        w = int(jsonData['width'])
+        h = int(jsonData['height'])
+        for item in jsonData['layers']:
+            name = item['name']
+            if not name.startswith('Tile_'):
+                # stacked
+                continue
+            z = int(name[5:])
+            data = item['data']
+            for i in range(0, len(data)):
+                if data[i] == 0:
+                    continue
+                tile_x = (int)(i % w)
+                tile_y = (int)(i / h)
+                tile_z = z
+                # 一些统计信息
+                tilesByPos[(tile_x, tile_y, tile_z)] = [1,0]    # [tile类型,zView--视觉层级,默认是0]
+                if tile_z > maxZ:
+                    maxZ = tile_z
+    # 根据上面的布局信息,得到视觉顶层的所有tile
+    tiles_at_top = []
+    for z in range(0, maxZ+1):
+        z = maxZ - z
+        for x in range(0, w+1):
+            for y in range(0, h+1):
+                if (x,y,z) in tilesByPos:
+                    tile = tilesByPos[(x, y, z)]
+                    adjs = [(x,y),(x,y+1),(x,y-1),(x+1,y),(x+1,y+1),(x+1,y-1),(x-1,y),(x-1,y-1),(x-1,y+1)]
+                    # 确定视觉层级
+                    zvMax = -1
+                    for zup in range(z+1, maxZ+1):
+                        for adj in adjs:
+                            if (adj[0],adj[1],zup) in tilesByPos:
+                                zv = tilesByPos[adj[0],adj[1],zup][1]
+                                if zv > zvMax:
+                                    zvMax = zv
+                    zv = zvMax + 1
+                    tilesByPos[(x, y, z)][1] = zv
+                    if zv == 0:
+                        tiles_at_top.append((x, y, z))
+    # 将 tiles_at_top 按照连通性,划分成若干个组
+    tops = [(x,y) for x,y,z in tiles_at_top]
+    print("tops:", tops)
+    shapes = split_shapes(tops)
+    print("shapes:", shapes)
+    # 对每一个组,得到其block
+    blocks = []
+    for i in range(0, len(shapes)):
+        shape = shapes[i]
+        ts = [(x,y,z) for x,y,z in tiles_at_top if (x,y) in shape]
+        block = get_block_from(ts, tilesByPos)
+        if len(block) > len(ts):
+            blocks.append(block)
+    return blocks, (w,h)
+
+# 测试获取视觉层级的数据是否ok
+def test_parse_blocks():
+    parse_blocks("/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/templates/tm_0004.json")
+
+#评估一个布局的好坏,返回一个分数
+#布局的好坏,可以从几个方面来评估:
+#1. 是否超出范围;
+#2. 是否有某种对称性;
+#3. tile的数量是否合理;
+def eva_layout():
+    pass
+
+
+# 生成一个随机的
+def generate_tops():
+    pass
+
+if __name__ == '__main__':
+    blocks_lib = set()
+    for directory in ["/Users/xulianxin/Documents/develop/game/TileMatch/TileManor.Lv/TileManor/tf_templates", ]:
+        for filename in os.listdir(directory):
+            filepath = os.path.join(directory, filename)
+            if os.path.isfile(filepath) and filepath.endswith('.json'):
+                blocks, size = parse_blocks(filepath)
+                for i in range(len(blocks)):
+                    block = blocks[i]
+                    # 将block都挪动到左上角,进行排重
+                    minx = min([x for x,y,z in block])
+                    miny = min([y for x,y,z in block])-1
+                    block = tuple((x-minx, y-miny, z) for x,y,z in block)
+                    if block not in blocks_lib:
+                        blocks_lib.add(block)
+                        output_block(block, size, filename.split(".")[0] + "_%d" % (i,))
+                    else:
+                        print("dumplicated!")
+    pass