Skip to content

Instantly share code, notes, and snippets.

@dkilmer
Created June 1, 2017 00:43
Show Gist options
  • Save dkilmer/90556fe6f6a8a26d6f89dbe87601870c to your computer and use it in GitHub Desktop.
Save dkilmer/90556fe6f6a8a26d6f89dbe87601870c to your computer and use it in GitHub Desktop.
Creates random 32x32 tiles that can be connected together to make a pattern
/*
* Generates 32x32 tiles that can be connected with conduits on one or
* more sides. The basic process is:
*
* 1. Draw a bunch of random boxes with color X, none of which touch the outer perimeter.
* 2. draw some conduits from the edges -- each conduit is two color-X lines going toward
* the center until they hit a color-X pixel
* 3. flood fill from the four corners of the perimeter with color Y.
* 4. erase all X-color pixels that don't have an adjacent Y-color pixel in the four
* cardinal directions.
* 5. fix the inside corner pixels that got erased in the previous step (see completeLines()
* for the logic of what's an inside corner).
* 6. iteratively draw along the inside of the shape, using a different pixel color each
* time (see contractLines() for the logic of finding the inside of the shape).
*
* One thing lacking in this code is that drawing some random number of boxes does not
* guarantee a single contiguous shape. To make it dependable, I'd need to write a test
* to see if the shape is contiguous. A naive implementation of that would be to see if
* flood-filling from any corner caused another corner to be filled. That would be really
* inefficient, though.
*/
int xoff = 20;
int yoff = 20;
int bwidth = 10;
int FRAME_INTERVAL = 30;
int IMG_WIDTH = 672;
int IMG_HEIGHT = 720;
int ROW_PIX = 48 * IMG_WIDTH;
int COL_PIX = 42;
int X_OFFSET = 8;
int Y_OFFSET = 14;
int[] tiles = new int[1024];
int dt;
int time;
int frames;
int evt_idx = 0;
int lc_idx = 0;
int conds = 1;
int cond_cnt = 0;
int tile_idx = 0;
ArrayList<Ev> events = new ArrayList<Ev>();
int FILL_COLOR = #E0E0E0;
int MARK_COLOR = #0000FF;
int NONE_COLOR = 0;
int[] LINE_COLOR = new int[4];
// directions - defined to allow bit-masking
int LEFT = 1;
int UP = 2;
int RIGHT = 4;
int DOWN = 8;
// events
int E_DRAW_BOX = 0;
int E_FLOOD_FILL = 1;
int E_CULL = 2;
int E_CONDUIT = 3;
int E_CONTRACT_LINES = 4;
int E_WAIT = 5;
int E_RESET = 6;
void setup() {
size(360, 360);
randomSeed(142857);
clear();
frames = 0;
LINE_COLOR[0] = #4a7423;
LINE_COLOR[1] = #588b29;
LINE_COLOR[2] = #639c2e;
LINE_COLOR[3] = #72b434;
// create a list of events to perform repeatedly
events.add(new Ev(E_DRAW_BOX, rndInt(7, 16), 5));
events.add(new Ev(E_CONDUIT, 1, 10));
events.add(new Ev(E_FLOOD_FILL, 1, 10));
events.add(new Ev(E_CULL, 1, 10));
events.add(new Ev(E_CONTRACT_LINES, 20, 10));
events.add(new Ev(E_WAIT, 4, 10));
resetEvents();
time = millis();
makeAll();
}
// clear the 32x32 array of tiles
void clear() {
for (int i=0; i<1024; i++) {
tiles[i] = NONE_COLOR;
}
}
// after all the events have been processed, reset so we can do it again.
void resetEvents() {
evt_idx = 0;
lc_idx = 0;
for (int i=0; i<events.size(); i++) {
Ev evt = events.get(i);
evt.cnt = 0;
if (evt.etype == E_DRAW_BOX) {
evt.num_times = rndInt(7, 16);
}
}
FRAME_INTERVAL = events.get(0).frame_dur;
}
// remove a single color from the array of tiles
void clearColor(int rgb) {
for (int i=0; i<1024; i++) {
if (tiles[i] == rgb) tiles[i] = NONE_COLOR;
}
}
// replace one color with another in the array of tiles
void replaceColor(int orgb, int nrgb) {
for (int i=0; i<1024; i++) {
if (tiles[i] == orgb) tiles[i] = nrgb;
}
}
// draw criss-crossy lines so we can see where the tiles are.
void drawGrid() {
noFill();
stroke(150);
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int xx = xoff+(x*bwidth);
int yy = yoff+(y*bwidth);
rect(xx, yy, bwidth, bwidth);
}
}
}
// draw a single tile in the desired color
void drawTile(int x, int y, int rgb) {
int xx = xoff+(x*bwidth)+1;
int yy = yoff+(y*bwidth)+1;
stroke(rgb);
fill(rgb);
rect(xx, yy, bwidth-2, bwidth-2);
}
// draw all the tiles in the array
void drawTiles() {
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int idx = (y*32)+x;
if (tiles[idx] != NONE_COLOR) {
drawTile(x, y, tiles[idx]);
}
}
}
}
// probably a dumb implementation
int rndInt(int min, int max) {
return round(random(min, max));
}
// set a tile in the array to a particular color
void setTile(int x, int y, int rgb) {
tiles[(y*32)+x] = rgb;
}
// get the color of a tile in the array, returning -1 if off the edge.
int getTile(int x, int y) {
if (x<0 || x>31) return -1;
if (y<0 || y>31) return -1;
return tiles[(y*32)+x];
}
// put a random box in the tile array, avoiding the outer edge
void randomBox() {
int x1 = rndInt(3, 24);
int y1 = rndInt(3, 24);
int x2 = rndInt(x1+3, 29);
int y2 = rndInt(y1+3, 29);
for (int x=x1; x<=x2; x++) {
setTile(x, y1, LINE_COLOR[0]);
setTile(x, y2, LINE_COLOR[0]);
}
for (int y=y1+1; y<y2; y++) {
setTile(x1, y, LINE_COLOR[0]);
setTile(x2, y, LINE_COLOR[0]);
}
}
// create a conduit in the tile array from a particular direction
void conduit(int dir) {
int x1, y1, x2, y2, inc;
if (dir == UP || dir == DOWN) {
x1 = 11;
x2 = 20;
if (dir == UP) {
y1 = 0;
inc = 1;
} else {
y1 = 31;
inc = -1;
}
int y = y1;
while (getTile(x1, y) == NONE_COLOR) {
//while (y != 16) {
setTile(x1, y, LINE_COLOR[0]);
y += inc;
}
y = y1;
while (getTile(x2, y) == NONE_COLOR) {
//while (y != 16) {
setTile(x2, y, LINE_COLOR[0]);
y += inc;
}
} else {
y1 = 11;
y2 = 20;
if (dir == LEFT) {
x1 = 0;
inc = 1;
} else {
x1 = 31;
inc = -1;
}
int x = x1;
while (getTile(x, y1) == NONE_COLOR) {
//while (x != 16) {
setTile(x, y1, LINE_COLOR[0]);
x += inc;
}
x = x1;
while (getTile(x, y2) == NONE_COLOR) {
//while (x != 16) {
setTile(x, y2, LINE_COLOR[0]);
x += inc;
}
}
}
// flood fill the tile array from point (x,y) with color crgb, stopping at color brgb
void floodFill(int x, int y, int brgb, int crgb) {
int rgb = getTile(x, y);
if (rgb != brgb) return;
setTile(x, y, crgb);
floodFill(x-1, y, brgb, crgb);
floodFill(x+1, y, brgb, crgb);
floodFill(x, y-1, brgb, crgb);
floodFill(x, y+1, brgb, crgb);
}
// check whether a tile in the array should be an inside corner of a line
boolean isInsideCorner(int x, int y, int crgb) {
if (getTile(x-1, y) == crgb && getTile(x, y-1) == crgb && getTile(x-1, y-1) != crgb) {
return true;
}
if (getTile(x+1, y) == crgb && getTile(x, y-1) == crgb && getTile(x+1, y-1) != crgb) {
return true;
}
if (getTile(x-1, y) == crgb && getTile(x, y+1) == crgb && getTile(x-1, y+1) != crgb) {
return true;
}
if (getTile(x+1, y) == crgb && getTile(x, y+1) == crgb && getTile(x+1, y+1) != crgb) {
return true;
}
return false;
}
// remove all tiles of color crgb that aren't next to a tile of color nrgb
void cull(int crgb, int nrgb) {
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int rgb = getTile(x, y);
if (rgb == crgb) {
if (getTile(x-1, y) != nrgb && getTile(x+1, y) != nrgb && getTile(x, y-1) != nrgb && getTile(x, y+1) != nrgb) {
setTile(x, y, NONE_COLOR);
}
}
}
}
}
// fill in all the inside corners
void completeLines(int crgb, int nrgb) {
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int rgb = getTile(x, y);
if (rgb == NONE_COLOR && isInsideCorner(x, y, crgb)) {
setTile(x, y, MARK_COLOR);
}
}
}
replaceColor(MARK_COLOR, crgb);
}
// for a tile along the perimeter of the shape, determine what direction
// points to the inside of the shape. This assumes the outside of the shape
// is filled with some other color while the inside is all set to NONE_COLOR.
int getDirOfInsideTile(int x, int y) {
if (getTile(x-1, y) == NONE_COLOR) return LEFT;
if (getTile(x+1, y) == NONE_COLOR) return RIGHT;
if (getTile(x, y-1) == NONE_COLOR) return UP;
if (getTile(x, y+1) == NONE_COLOR) return DOWN;
return -1;
}
// draw lines along the inside of a shape, where the edge to draw along
// is color crgb and the new color to draw is nrgb
boolean contractLines(int crgb, int nrgb) {
boolean contracted = false;
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int rgb = getTile(x, y);
if (rgb == crgb) {
int dir = getDirOfInsideTile(x, y);
while (dir >=0) {
if (dir == LEFT) {
setTile(x-1, y, nrgb);
contracted = true;
} else if (dir == UP) {
setTile(x, y-1, nrgb);
contracted = true;
} else if (dir == RIGHT) {
setTile(x+1, y, nrgb);
contracted = true;
} else if (dir == DOWN) {
setTile(x, y+1, nrgb);
contracted = true;
}
dir = getDirOfInsideTile(x, y);
}
}
}
}
return contracted;
}
// save the array to an actual image for saving to file
void saveTileImage(PImage img) {
int tx = tile_idx % 16;
int ty = tile_idx / 16;
for (int y=0; y<32; y++) {
for (int x=0; x<32; x++) {
int rgb = getTile(x, y);
int off = ((ty * ROW_PIX) + (Y_OFFSET * IMG_WIDTH) + (y * IMG_WIDTH)) + ((tx * COL_PIX) + X_OFFSET + x);
if (rgb == NONE_COLOR || rgb == FILL_COLOR) {
img.pixels[off] = color(0,0,0,0);
} else {
int r = (rgb & 0xFF0000) >> 16;
int g = (rgb & 0xFF00) >> 8;
int b = (rgb & 0xFF);
img.pixels[off] = color(r,g,b,255);
}
}
}
}
// this just does everything at once, eschewing the whole drawing-to-the-screen
// thing. it creates 16 random shapes for each of the 15 possible combinations
// of conduits and puts them all into an image that has 15 rows of 16 32x32 shapes.
void makeAll() {
PImage img = createImage(IMG_WIDTH,IMG_HEIGHT, ARGB);
img.loadPixels();
int dir = 1;
int dcnt = 0;
for (tile_idx=0; tile_idx<240; tile_idx++) {
// draw random boxes
int num_boxes = rndInt(7, 16);
for (int i=0; i<num_boxes; i++) {
randomBox();
}
// draw conduits
if ((dir & LEFT) == LEFT) conduit(LEFT);
if ((dir & UP) == UP) conduit(UP);
if ((dir & RIGHT) == RIGHT) conduit(RIGHT);
if ((dir & DOWN) == DOWN) conduit(DOWN);
// flood fill the outside
floodFill(0, 0, 0, FILL_COLOR);
floodFill(31, 0, 0, FILL_COLOR);
floodFill(0, 31, 0, FILL_COLOR);
floodFill(31, 31, 0, FILL_COLOR);
// cull the inside and complete the lines
cull(LINE_COLOR[0], FILL_COLOR);
completeLines(LINE_COLOR[0], FILL_COLOR);
// contract and draw inner lines
boolean contracted = true;
int clr_idx = 0;
while (contracted) {
int crgb = LINE_COLOR[clr_idx];
clr_idx++;
if (clr_idx > 3) clr_idx = 0;
int nrgb = LINE_COLOR[clr_idx];
contracted = contractLines(crgb, nrgb);
completeLines(nrgb, FILL_COLOR);
}
saveTileImage(img);
clear();
dcnt++;
if (dcnt > 15) {
dcnt = 0;
dir++;
}
}
img.updatePixels();
img.save("mosses.png");
}
// execute the current event in the queue and advance to the next event
void execEvent() {
Ev evt = events.get(evt_idx);
FRAME_INTERVAL = evt.frame_dur;
if (evt.etype == E_DRAW_BOX) {
randomBox();
} else if (evt.etype == E_CONDUIT) {
int dir = rndInt(1, 15);
if ((dir & LEFT) == LEFT) conduit(LEFT);
if ((dir & UP) == UP) conduit(UP);
if ((dir & RIGHT) == RIGHT) conduit(RIGHT);
if ((dir & DOWN) == DOWN) conduit(DOWN);
} else if (evt.etype == E_FLOOD_FILL) {
floodFill(0, 0, 0, FILL_COLOR);
floodFill(31, 0, 0, FILL_COLOR);
floodFill(0, 31, 0, FILL_COLOR);
floodFill(31, 31, 0, FILL_COLOR);
} else if (evt.etype == E_CULL) {
cull(LINE_COLOR[0], FILL_COLOR);
completeLines(LINE_COLOR[0], FILL_COLOR);
} else if (evt.etype == E_CONTRACT_LINES) {
int crgb = LINE_COLOR[lc_idx];
lc_idx++;
if (lc_idx > 3) lc_idx = 0;
int nrgb = LINE_COLOR[lc_idx];
boolean contracted = contractLines(crgb, nrgb);
completeLines(nrgb, FILL_COLOR);
if (!contracted) evt.cnt = evt.num_times;
} else if (evt.etype == E_WAIT) {
}
evt.inc();
if (evt.done()) evt_idx++;
if (evt_idx >= events.size()) {
resetEvents();
clear();
}
}
// draw a frame to the screen
void draw() {
int t = millis();
dt = t - time;
time = t;
frames++;
if (frames == FRAME_INTERVAL) {
frames = 0;
execEvent();
}
background(255);
drawGrid();
drawTiles();
}
// the event class. it has a type, a number of times to execute before
// moving to the next event, a count of how many times it has executed so
// far, and how many frames to wait after completing before starting on
// the next event.
class Ev {
int etype;
int num_times;
int cnt;
int frame_dur;
Ev(int t, int nt, int fd) {
etype = t;
num_times = nt;
frame_dur = fd;
clear();
}
void clear() {
cnt = 0;
}
boolean inc() {
cnt++;
return (cnt == num_times);
}
boolean done() {
return (cnt >= num_times);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment