#!python # -*- mode: python; Encoding: utf-8; coding: utf-8 -*- # Last updated: <2021/10/13 20:02:57 +0900> """ Genarate road tilemap, middle and large. cursor or WASD key : BG scroll G key : Toggle grid display S key : Save tilemap image Author: mieki256 License: CC0 / Public Domain Windows10 x64 21H1 + Python 3.9.7 64bit + Pygame 2.0.1 """ import json import pygame import sys import random import datetime from pygame import draw from pygame.version import PygameVersion SCRW, SCRH = 1024, 768 map_w = 128 map_h = 96 tile_w = 8 tile_h = 8 imgname = "road_chip.png" block_json_m = "block_road_middle.json" block_json_l = "block_road_large.json" class RoadRollerM: HLINE = 1 VLINE = 2 CORNER_DR = 3 CROSS_DLR = 4 CORNER_DL = 5 CROSS_UDR = 6 CROSS_TILE = 7 CROSS_UDL = 8 CORNER_UR = 9 CROSS_ULR = 10 CORNER_UL = 11 CURVE_DL = 12 CURVE_UL = 13 CURVE_UR = 14 CURVE_DR = 15 CURVE_L_UR = 16 CURVE_L_DR = 17 CURVE_U_DL = 18 CURVE_U_DR = 19 CURVE_R_DL = 20 CURVE_R_UL = 21 CURVE_D_UL = 22 CURVE_D_UR = 23 CURVE_DL_UR = 24 CURVE_UL_DR = 25 CURVE_UR_DL = 26 CURVE_UL_DR = 27 CURVE_UR_DL = 28 CURVE_DR_UL = 29 CURVE_DR_UL = 30 CURVE_DL_UR = 31 CONNECT_FLAG = { # id: udlr connect 0: 0b00000, HLINE: 0b00011, VLINE: 0b01100, CORNER_DR: 0b00101, CROSS_DLR: 0b00111, CORNER_DL: 0b00110, CROSS_UDR: 0b01101, CROSS_TILE: 0b01111, CROSS_UDL: 0b01110, CORNER_UR: 0b01001, CROSS_ULR: 0b01011, CORNER_UL: 0b01010, CURVE_DL: 0b00110, CURVE_UL: 0b01010, CURVE_UR: 0b01001, CURVE_DR: 0b00101, CURVE_L_UR: 0b01011, CURVE_L_DR: 0b00111, CURVE_U_DL: 0b01110, CURVE_U_DR: 0b01101, CURVE_R_DL: 0b00111, CURVE_R_UL: 0b01011, CURVE_D_UL: 0b01110, CURVE_D_UR: 0b01101, CURVE_DL_UR: 0b01111, CURVE_UL_DR: 0b01111, CURVE_UR_DL: 0b01111, CURVE_UL_DR: 0b01111, CURVE_UR_DL: 0b01111, CURVE_DR_UL: 0b01111, CURVE_DR_UL: 0b01111, CURVE_DL_UR: 0b01111, } CORNER = { 0: { 6: CORNER_UL, 7: CURVE_UL, 1: CURVE_DL, 2: CORNER_DL }, 1: { 0: CURVE_UR, 2: CURVE_DL, }, 2: { 0: CORNER_UR, 1: CURVE_UR, 3: CURVE_UL, 4: CORNER_UL, }, 3: { 2: CURVE_DR, 4: CURVE_UL, }, 4: { 2: CORNER_DR, 3: CURVE_DR, 5: CURVE_UR, 6: CORNER_UR, }, 5: { 4: CURVE_DL, 6: CURVE_UR, }, 6: { 4: CORNER_DL, 5: CURVE_DL, 7: CURVE_DR, 0: CORNER_DR, }, 7: { 6: CURVE_UL, 0: CURVE_DR, } } STOP_ID_TBL = { 0: [HLINE], 1: [CURVE_DL, CURVE_UR], 2: [VLINE], 3: [CURVE_UL, CURVE_DR], 4: [HLINE], 5: [CURVE_UR, CURVE_DL], 6: [VLINE], 7: [CURVE_DR, CURVE_UL] } JUNCTION = { # udlr bit, id 0b01111: CROSS_TILE, 0b01110: CROSS_UDL, 0b01101: CROSS_UDR, 0b01100: VLINE, 0b01011: CROSS_ULR, 0b01010: CURVE_UL, 0b01001: CURVE_UR, # 0b01000: VLINE, 0b00111: CROSS_DLR, 0b00110: CURVE_DL, 0b00101: CURVE_DR, # 0b00100: VLINE, 0b00011: HLINE, # 0b00010: HLINE, # 0b00001: HLINE, # 0b00000: 0, } CURVE_JUNCTION = { CURVE_DL: { 0b01110: CURVE_U_DL, 0b00111: CURVE_R_DL, 0b01111: CURVE_UR_DL, }, CURVE_UL: { 0b01011: CURVE_R_UL, 0b01110: CURVE_D_UL, 0b01111: CURVE_DR_UL, }, CURVE_UR: { 0b01011: CURVE_L_UR, 0b01101: CURVE_D_UR, 0b01111: CURVE_DL_UR, }, CURVE_DR: { 0b00111: CURVE_L_DR, 0b01101: CURVE_U_DR, 0b01111: CURVE_UL_DR, }, CURVE_L_UR: { 0b01111: CURVE_DL_UR, }, CURVE_L_DR: { 0b01111: CURVE_UL_DR, }, CURVE_U_DL: { 0b01111: CURVE_UR_DL, }, CURVE_U_DR: { 0b01111: CURVE_UL_DR, }, CURVE_R_DL: { 0b01111: CURVE_UR_DL, }, CURVE_R_UL: { 0b01111: CURVE_DR_UL, }, CURVE_D_UL: { 0b01111: CURVE_DR_UL, }, CURVE_D_UR: { 0b01111: CURVE_DL_UR, } } MOVE_TBL = { # right 0: [ # x, y, ID, next Direc [1, 0, HLINE, 0] ], 1: [ [1, 0, CURVE_DL, 2], [0, 1, CURVE_UR, 0], ], # down 2: [ [0, 1, VLINE, 2] ], 3: [ [0, 1, CURVE_UL, 4], [-1, 0, CURVE_DR, 2], ], # left 4: [ [-1, 0, HLINE, 4] ], 5: [ [-1, 0, CURVE_UR, 6], [0, -1, CURVE_DL, 4], ], # up 6: [ [0, -1, VLINE, 6] ], 7: [ [0, -1, CURVE_DR, 0], [1, 0, CURVE_UL, 6], ] } def __init__(self, x, y, direc, bgmap, chrmap, blocks, bw, bh): self.active = True self.x = x self.y = y self.oldx = x self.oldy = y self.direc = direc self.old_direc = direc self.cmap = chrmap self.bgmap = bgmap self.blocks = blocks self.bw = bw self.bh = bh self.mw = len(self.cmap[0]) self.mh = len(self.cmap) self.map_w = self.mw self.map_h = self.mh self.count = random.randint(2, 8) self.chg_cnt = 2 self.move_idx = 0 self.set_tile_first(x, y, self.direc) def in_area(self, x, y): return (x >= 0 and y >= 0 and x < self.map_w and y < self.map_h) def tileid(self, x, y): if self.in_area(x, y): return self.cmap[y][x] return 0 def get_udlr_id(self, x, y): u = self.tileid(x, y - 1) d = self.tileid(x, y + 1) l = self.tileid(x - 1, y) r = self.tileid(x + 1, y) return u, d, l, r def get_connect_flag(self, id, dirstr): flag = RoadRollerM.CONNECT_FLAG[id] MASK = { "u": 0b01000, "d": 0b00100, "l": 0b00010, "r": 0b00001, } if dirstr in MASK: return ((flag & MASK[dirstr]) != 0) sys.exit("Not found dir str = %s" % dirstr) def get_udlr_connect(self, x, y): u, d, l, r = self.get_udlr_id(x, y) fg = 0 if self.get_connect_flag(u, "d"): fg += 0b01000 if self.get_connect_flag(d, "u"): fg += 0b00100 if self.get_connect_flag(l, "r"): fg += 0b00010 if self.get_connect_flag(r, "l"): fg += 0b00001 return fg def put_tile(self, x, y, id): if self.in_area(x, y): self.cmap[y][x] = id def set_tile_first(self, x, y, direc): id = RoadRollerM.STOP_ID_TBL[direc][0] self.put_tile(x, y, id) def set_tile(self, x, y, direc): udlr = self.get_udlr_connect(x, y) if False: oid = self.tileid(x, y) if oid >= RoadRollerM.CURVE_DL: if oid in RoadRollerM.CURVE_JUNCTION: tbl = RoadRollerM.CURVE_JUNCTION[oid] if udlr in tbl: id = tbl[udlr] self.put_tile(x, y, id) return if udlr in RoadRollerM.JUNCTION: id = RoadRollerM.JUNCTION[udlr] self.put_tile(x, y, id) def update(self): if not self.active: return self.oldx = self.x self.oldy = self.y # move d = RoadRollerM.MOVE_TBL[self.direc] i = self.move_idx dx = d[i][0] dy = d[i][1] self.nid = d[i][2] self.ndir = d[i][3] self.move_idx = (self.move_idx + 1) % len(d) self.x += dx self.y += dy if not self.in_area(self.x, self.y): # area over. I am dead self.set_tile(self.oldx, self.oldy, self.old_direc) self.active = False return oid = self.tileid(self.x, self.y) if oid != 0: if self.count <= 2: self.count += 2 # existing tile. Should I die? if random.random() > 0.85: self.active = False elif oid in RoadRollerM.STOP_ID_TBL[self.direc]: self.active = False self.set_tile(self.x, self.y, self.direc) self.set_tile(self.oldx, self.oldy, self.old_direc) else: self.put_tile(self.x, self.y, self.nid) self.set_tile(self.x, self.y, self.direc) self.set_tile(self.oldx, self.oldy, self.old_direc) self.old_direc = self.direc if not self.active: return self.count -= 1 if self.count > 0: return if self.chg_cnt <= 0: return self.chg_cnt -= 1 # change direction od = self.direc r = random.random() if (od % 2) == 1: self.direc = self.ndir else: dd = 0 if random.random() < 0.7: if random.random() < 0.5: dd = -2 else: dd = 2 else: if random.random() < 0.5: dd = -1 else: dd = 1 self.direc = (self.direc + dd) % 8 if (self.direc % 2) == 1: self.count = random.randint(1, 3) else: self.count = random.randint(2, 8) # print("old_dir, dir = %d, %d" % (od, self.direc)) # set corner tile d = RoadRollerM.CORNER if od in d: if self.direc in d[od]: self.cmap[self.y][self.x] = d[od][self.direc] self.move_idx = 0 if od + 1 == self.direc: if self.direc % 2 == 1: self.move_idx = 1 def put_block(self, bx, by, id): m = self.bgmap blk = self.blocks[id] i = 0 for y in range(self.bh): for x in range(self.bw): m[by + y][bx + x] = blk[i] i += 1 def chr_to_bgmap(self): cw = len(self.cmap[0]) ch = len(self.cmap) for y in range(ch): for x in range(cw): id = self.cmap[y][x] self.put_block(x * self.bw, y * self.bh, id) class GenMap: def __init__(self, map_w, map_h, kind, jsonfile): self.kind = kind self.map_w = map_w self.map_h = map_h self.cw = self.map_w // 8 self.ch = self.map_h // 8 # load block json file with open(jsonfile, 'r') as f: self.blocks = json.load(f) # init bgmap, chrmap self.bgmap = [[0 for x in range(self.map_w)] for y in range(self.map_h)] self.chrmap = [[0 for x in range(self.cw)] for y in range(self.ch)] self.init() def init(self): self.clear_map_all() self.bidx = 0 self.obj = None if self.kind == "middle": self.gen_list = self.make_gen_pos_list_m() elif self.kind == "large": self.gen_list = self.make_gen_pos_list_l() else: sys.exit("Error : init : kind = %s" % self.kind) def update(self): if self.bidx >= len(self.gen_list): return True if self.obj is None: # born road Roller x, y, direc = self.gen_list[self.bidx] self.obj = RoadRollerM(x, y, direc, self.bgmap, self.chrmap, self.blocks, 8, 8) self.obj.chr_to_bgmap() else: # move road roller self.obj.update() self.obj.chr_to_bgmap() if not self.obj.active: self.obj = None self.bidx += 1 return False def clear_map(self, cmap): mw, mh = len(cmap[0]), len(cmap) for y in range(mh): for x in range(mw): cmap[y][x] = 0 def clear_map_all(self): self.clear_map(self.bgmap) self.clear_map(self.chrmap) def make_gen_pos_list_m(self): """Make road roller generate position list.""" RIGHT_DIR = 0 DOWN_DIR = 2 LEFT_DIR = 4 UP_DIR = 6 mw, mh = len(self.chrmap[0]), len(self.chrmap) x0, x1 = 0, mw - 1 y0, y1 = 0, mh - 1 l = [] y = random.randint(1, 3) while y < mh: l.append((x0, y, RIGHT_DIR)) y += random.randint(2, 5) x = random.randint(1, 3) while x < mw: l.append((x, y0, DOWN_DIR)) x += random.randint(2, 5) y = random.randint(1, 3) while y < mh: l.append((x1, y, LEFT_DIR)) y += random.randint(2, 5) x = random.randint(1, 3) while x < mw: l.append((x, y1, UP_DIR)) x += random.randint(2, 5) return l def make_gen_pos_list_l(self): """Make road roller generate position list.""" RIGHT_DIR = 0 DOWN_DIR = 2 LEFT_DIR = 4 UP_DIR = 6 mw, mh = len(self.chrmap[0]), len(self.chrmap) x0, x1 = 0, mw - 1 y0, y1 = 0, mh - 1 l = [] dd = 4 d = mh // dd for i in range(dd - 1): y = d * (i + 1) if random.random() < 0.5: l.append((x0, y, RIGHT_DIR)) else: l.append((x1, y, LEFT_DIR)) dd = 4 d = mw // dd for i in range(dd - 1): x = d * (i + 1) if random.random() < 0.5: l.append((x, y0, DOWN_DIR)) else: l.append((x, y1, UP_DIR)) return l def split_image(img, tilew, tileh): """Split image.""" imgs = [] w, h = tilew, tileh xc = img.get_width() // w yc = img.get_height() // h for yi in range(yc): for xi in range(xc): x, y = xi * w, yi * h imgs.append(img.subsurface(pygame.Rect(x, y, w, h))) return imgs def combine_bgmap(mapm, mapl): mw, mh = len(mapm[0]), len(mapm) newmap = [[0 for x in range(mw)] for y in range(mh)] for y in range(mh): for x in range(mw): if mapl[y][x] != 0: newmap[y][x] = mapl[y][x] elif mapm[y][x] != 0: newmap[y][x] = mapm[y][x] return newmap def draw_tilemap(screen, bgmap, imgs, xofs, yofs, scrw, scrh, map_w, map_h, tile_w, tile_h): xc = scrw // tile_w yc = scrh // tile_h for yi in range(yc): for xi in range(xc): x = xi + xofs y = yi + yofs if x >= map_w or y >= map_h: continue id = bgmap[y][x] im = imgs[id] px = xi * tile_w py = yi * tile_h screen.blit(im, (px, py)) def write_tilemap(screen, bgmap, imgs, tile_w, tile_h): screen.fill((0, 0, 0, 0)) mw, mh = len(bgmap[0]), len(bgmap) for y in range(mh): for x in range(mw): im = imgs[bgmap[y][x]] px = x * tile_w py = y * tile_h screen.blit(im, (px, py)) def draw_grid(screen, w, h): col = (255, 0, 0) for x in range(0, w, 64): pygame.draw.line(screen, col, (x, 0), (x, h), 1) for y in range(0, h, 64): pygame.draw.line(screen, col, (0, y), (w, y), 1) def get_now_str(): dtnow = datetime.datetime.now() return dtnow.strftime("%Y%m%d_%H%M%S") def main(): pygame.init() screen = pygame.display.set_mode((SCRW, SCRH), pygame.DOUBLEBUF) # load bg chip image img = pygame.image.load(imgname).convert_alpha() # split bg chip image imgs = split_image(img, tile_w, tile_h) xofs, yofs = 0, 0 xofs_max = max(0, map_w - (SCRW // tile_w)) yofs_max = max(0, map_h - (SCRH // tile_h)) mapobjm = GenMap(map_w, map_h, "middle", block_json_m) mapobjl = GenMap(map_w, map_h, "large", block_json_l) combmap = None bgw, bgh = map_w * tile_w, map_h * tile_h bgimg = pygame.Surface((bgw, bgh), pygame.SRCALPHA | pygame.SWSURFACE) running = True recreate = False grid_display = False job_fix = False clock = pygame.time.Clock() # Main loop while running: # update # check event for ev in pygame.event.get(): if ev.type == pygame.QUIT: running = False if ev.type == pygame.KEYDOWN: if ev.key == pygame.K_ESCAPE or ev.key == pygame.K_q: # Push ESC or Q key running = False if ev.key == pygame.K_SPACE: recreate = True if ev.key == pygame.K_g: grid_display = not grid_display if ev.key == pygame.K_s: if job_fix: fn = get_now_str() + ".png" pygame.image.save(bgimg, fn) print("Save : %s" % fn) if recreate: # clear map and start recreate mapobjm.init() mapobjl.init() combmap = None job_fix = False recreate = False # create map if not job_fix: if mapobjm.update(): if mapobjl.update(): job_fix = True combmap = combine_bgmap(mapobjm.bgmap, mapobjl.bgmap) write_tilemap(bgimg, combmap, imgs, tile_w, tile_h) # scroll BG pygame.event.pump() pressed = pygame.key.get_pressed() if pressed[pygame.K_LEFT] or pressed[pygame.K_a]: xofs -= 1 if xofs < 0: xofs = 0 if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]: xofs += 1 if xofs > xofs_max: xofs = xofs_max if pressed[pygame.K_UP] or pressed[pygame.K_w]: yofs -= 1 if yofs < 0: yofs = 0 if pressed[pygame.K_DOWN] or pressed[pygame.K_s]: yofs += 1 if yofs > yofs_max: yofs = yofs_max # draw start # clear screen # screen.fill((40, 60, 200)) screen.fill((64, 192, 0)) # draw tilemap BG if not job_fix: draw_tilemap(screen, mapobjm.bgmap, imgs, xofs, yofs, SCRW, SCRH, map_w, map_h, tile_w, tile_h) draw_tilemap(screen, mapobjl.bgmap, imgs, xofs, yofs, SCRW, SCRH, map_w, map_h, tile_w, tile_h) else: px = -xofs * tile_w py = -yofs * tile_h screen.blit(bgimg, (px, py)) # draw grid if grid_display: draw_grid(screen, SCRW, SCRH) pygame.display.flip() clock.tick_busy_loop(60) cap = "Tilemap BG - %5.2f FPS" % (clock.get_fps()) pygame.display.set_caption(cap) pygame.quit() sys.exit() if __name__ == "__main__": main()