Skip to content

Instantly share code, notes, and snippets.

@marcus-downing
Last active February 10, 2020 22:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcus-downing/c68ebe754e4ecf7bd14ddf5b94fd1e0a to your computer and use it in GitHub Desktop.
Save marcus-downing/c68ebe754e4ecf7bd14ddf5b94fd1e0a to your computer and use it in GitHub Desktop.
Scripts for making randomised prompts for a writing contest.
#!/usr/bin/env node
/*
Make prompts
Creates a set of randomised prompt cards with five tiles each.
Requires a `tiles.csv` file in the format: id,title,group,desc,crop,img
where group is one of: race, character, location, concept, combo.
Requires a `tiles` folder with images as created by `make-tiles.js`
Saves prompt cards into a `prompts` folder.
*/
// const parse = require('csv-parse');
const csvtojson = require('csvtojson');
const jimp = require('jimp');
Array.prototype.diff = function(a) {
return this.filter(function(i) {return a.indexOf(i) < 0;});
};
// constants
const dataFile = 'tiles.csv';
const tileset = "tiles";
let num_tiles = 122;
const num_prompts = 200;
const num_picks_per_prompt = 5;
const bgcolor = "#f8f8f8";
let groups = {
"character": {
min: 1,
max: 3
},
"location": {
min: 0,
max: 2
},
"race": {
min: 0,
max: 1
},
"creature": {
min: 0,
max: 1
},
"concept": {
min: 0,
max: 3
},
"combo": {
min: 0,
max: 1
}
}
// data
let loadingQueue = [];
let fonts = {};
function loadFont(path, name) {
try {
let promise = jimp.loadFont(path).then(font => {
console.log("Font loaded:", name);
fonts[name] = font;
}).catch(err => {
console.log("Error", err);
});
loadingQueue.push(promise);
} catch (err) {
console.log("Error", err);
}
}
loadFont("./Equestria32.fnt", "Equestria 32");
// load data
let tileData = {};
let groupsFound = {
character: 0,
location: 0,
race: 0,
creature: 0,
concept: 0,
combo: 0
};
const tiles = {};
let dataPromise = new Promise((resolve, reject) => {
csvtojson()
.fromFile(dataFile)
.then((jsonObj)=>{
// console.log("Data:", jsonObj);
num_tiles = jsonObj.length;
var next_id = 1;
jsonObj.forEach(t => {
var tile_id = next_id++;
let n = ("00"+tile_id).slice(-3);
t.n = n;
// console.log("Tile:", n, t);
tileData[n] = t;
groupsFound[t.group]++;
try {
let filename = tileset+'/'+n+'.png';
let promise = jimp.read(filename).then(img => {
tiles[n] = img;
console.log("Tile",n,"loaded");
}).catch(err => {
console.log("Error", err);
});
loadingQueue.push(promise);
} catch (err) {
console.log("Error", err);
}
});
console.log("Tiles by group:", groupsFound);
resolve();
});
});
loadingQueue.push(dataPromise);
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// How to (pseudo) random
// cf https://gist.github.com/blixt/f17b47c62508be59987b
function Random(seed) {
this._seed = seed % 2147483647;
if (this._seed <= 0) this._seed += 2147483646;
}
Random.prototype.next = function () {
return this._seed = this._seed * 16807 % 2147483647;
};
const random = new Random(3198146354);
console.log("Random", random.next());
// make the prompt images
try {
Promise.all(loadingQueue).then(() => {
Promise.all(loadingQueue).then(() => {
// select the prompts
let prompts = [];
for (let i = 0; i <= num_prompts; i++) {
// pick tiles
let picks = [];
let nGroups = {};
Object.keys(groups).forEach(group => {
nGroups[group] = 0;
});
let combo = false;
let charpicks = [];
for (let j = 0; j < 40; j++) {
let index = random.next() % num_tiles + 1;
let n = ("00"+index).slice(-3);
if (picks.includes(index))
continue;
if (!tiles.hasOwnProperty(n))
continue;
if (!tileData.hasOwnProperty(n))
continue;
// console.log("Pick:", tileData[n]);
let group = tileData[n].group;
if (nGroups[group] >= groups[group].max)
continue;
nGroups[group]++;
if (group == "combo") {
if (j < 5 && !combo) {
combo = index;
}
continue;
} else if (group == "character") {
charpicks.push(index);
}
picks.push(index);
}
let reservedpicks = [];
Object.keys(groups).forEach(group => {
if (groups[group].max > 0) {
let found = 0;
picks.forEach(index => {
if (found >= groups[group].max) {
return;
}
let n = ("00"+index).slice(-3);
if (tileData[n].group == group) {
reservedpicks.push(index);
found++;
}
});
}
});
picks = [...reservedpicks, ...picks.diff(reservedpicks)];
if (combo && charpicks.length >= 2) {
// console.log("Combo for prompt", i);
let combopicks = [];
combopicks.push(charpicks[0]);
combopicks.push(combo);
combopicks.push(charpicks[1]);
// console.log(" -> ", combopicks, picks);
picks = [...combopicks, ...shuffle(picks.diff(combopicks))];
// console.log(" => ", picks);
}
// console.log("Picks", picks);
prompts.push(picks);
}
// make the prompt images
// let font16 = fonts["Equestria 16"];
let font32 = fonts["Equestria 32"];
try {
console.log("Generating...");
for (let i = 0; i <= num_prompts; i++) {
((i) => {
let n = ("00"+i).slice(-3);
// console.log("Writing prompt", n);
// pick tiles
let picks = prompts[i];
let anon = random.next().toString(16).slice(-6);
// console.log("Picked", picks);
// generate the image
new jimp(810, 200, bgcolor, (err, canvas) => {
try {
canvas.print(font32, 10, 2, 'Season 10 Bingo Writing Contest');
canvas.print(font32, 660, 2, 'Prompt #'+n);
for (j = 0; j < num_picks_per_prompt; j++) {
((j) => {
let t = picks[j];
let u = ("00"+t).slice(-3);
// console.log("Using image",u);
let img = tiles[u];
canvas.composite(img, 10+(j*160), 40);
})(j);
}
canvas.write('prompts/prompt-'+n+'-'+anon+'.png');
console.log("Generated prompt",n);
} catch (err) {
console.log(err);
}
});
})(i);
}
} catch (err) {
console.log("Error", err);
}
});
});
} catch (err) {
console.log("Error", err);
}
#!/usr/bin/env node
/*
Make Tiles
Creates tiles for the bingo contest
Requires a `tiles.csv` file in the format: id,title,group,desc,crop,img
where group is one of: race, character, location, concept, combo.
Requires a `source-images` folder containing a PNG image for each tile
with a name matching the slug. Images will be resized and cropped.
Requires font files as created by "Bitmap font generator" with files
saved as XML + PNG.
Saves files into a `tiles` folder.
*/
const fs = require('fs');
const csvtojson = require('csvtojson');
const jimp = require('jimp');
Array.prototype.diff = function(a) {
return this.filter(function(i) {return a.indexOf(i) < 0;});
};
// constants
const dataFile = 'tiles.csv';
const tileset = "tiles";
let num_tiles = 0;
const bgcolor = "#f8f8f8";
// data
let loadingQueue = [];
let fonts = {};
// utils
function slugify_title(s) {
s = s.trim();
s = s.toLowerCase();
s = s.replace(/['’]/, '');
s = s.replace(/[^a-z0-9]+/g, '-');
s = s.replace(/^-/, '');
s = s.replace(/-$/, '');
return s;
}
function normalize_title(s) {
s = s.replace(/[\r\n]+/g, "\n");
s = s.replace(/^\n+/, '');
s = s.replace(/\n+$/, '');
s = s.trim();
s = s.toLowerCase();
[
("\u201C", "\""),
("\u201D", "\""),
// ("\u2018", "'"),
// ("\u2019", "'"),
("\u2014", "-"),
("\u2013", "-"),
("\u2026", "..."),
// ("\u00A0", " "),
].forEach((f, t) => {
s = s.replace(f, t);
});
return s;
}
function loadFont(path, name) {
try {
let promise = jimp.loadFont(path).then(font => {
console.log("Font loaded:", name);
fonts[name] = font;
}).catch(err => {
console.log("Error", err);
});
loadingQueue.push(promise);
} catch (err) {
console.log("Error", err);
}
}
loadFont("./Equestria20b.fnt", "Equestria 20 b");
loadFont("./Equestria24b.fnt", "Equestria 24 b");
loadFont("./Equestria32.fnt", "Equestria 32");
// load data
let tileData = {};
let groupsFound = {
character: 0,
location: 0,
race: 0,
creature: 0,
concept: 0,
combo: 0
};
let dataPromise = new Promise((resolve, reject) => {
csvtojson()
.fromFile(dataFile)
.then((jsonObj)=>{
// console.log("Data:", jsonObj);
num_tiles = jsonObj.length;
var next_id = 1;
jsonObj.forEach(t => {
var tile_id = next_id++;
let n = ("00"+tile_id).slice(-3);
t.n = n;
// console.log("Tile:", n, t);
tileData[n] = t;
groupsFound[t.group]++;
});
console.log("Tiles by group:", groupsFound);
resolve();
});
});
loadingQueue.push(dataPromise);
// load any images not already cached
function getImage(title) {
return new Promise((resolve, reject) => {
slug = slugify_title(title);
cachePNG = 'source-images/'+slug+'.png';
cacheJPEG = 'source-images/'+slug+'.jpg';
cacheGIF = 'source-images/'+slug+'.gif';
// console.log("Looking for cached image:", slug);
if (fs.existsSync(cachePNG)) {
jimp.read(cachePNG).then(image => {
resolve(image);
});
} else if (fs.existsSync(cacheJPEG)) {
jimp.read(cacheJPEG).then(image => {
resolve(image);
});
} else if (fs.existsSync(cacheGIF)) {
jimp.read(cacheGIF).then(image => {
resolve(image);
});
} else {
console.log("Image not found:", slug, title);
// reject();
}
});
}
function alignMode(cropdir) {
switch(cropdir) {
case 'top': return [jimp.HORIZONTAL_ALIGN_CENTER, jimp.VERTICAL_ALIGN_TOP];
case 'left': return [jimp.HORIZONTAL_ALIGN_LEFT, jimp.VERTICAL_ALIGN_MIDDLE];
case 'right': return [jimp.HORIZONTAL_ALIGN_RIGHT, jimp.VERTICAL_ALIGN_MIDDLE];
case 'bottom': return [jimp.HORIZONTAL_ALIGN_CENTER, jimp.VERTICAL_ALIGN_BOTTOM];
case 'tl': return [jimp.HORIZONTAL_ALIGN_LEFT, jimp.VERTICAL_ALIGN_TOP];
case 'br': return [jimp.HORIZONTAL_ALIGN_RIGHT, jimp.VERTICAL_ALIGN_BOTTOM];
case 'tr': return [jimp.HORIZONTAL_ALIGN_RIGHT, jimp.VERTICAL_ALIGN_TOP];
case 'bl': return [jimp.HORIZONTAL_ALIGN_LEFT, jimp.VERTICAL_ALIGN_BOTTOM];
default: return [jimp.HORIZONTAL_ALIGN_CENTER, jimp.VERTICAL_ALIGN_MIDDLE];
}
}
var black = new jimp(150, 40, '#000000');
// make tiles
function picture_tile(id, title, image, cropdir) {
let promise = new jimp(150, 150, bgcolor, (err, canvas) => {
try {
console.log("Writing tile", id, title);
[h_align, v_align] = alignMode(cropdir);
image.cover(150, 108, h_align, v_align);
canvas.composite(image, 0, 0);
title = normalize_title(title);
canvas.composite(black, 0, 110);
var font;
var vbase = 110;
if (title.length > 24) {
font = fonts["Equestria 20 b"];
font.common.lineHeight = 14; // force the line height
vbase = 106;
} else {
font = fonts["Equestria 24 b"];
font.common.lineHeight = 17; // force the line height
vbase = 106;
}
canvas.print(
font, 10, vbase,
{
text: title,
alignmentX: jimp.HORIZONTAL_ALIGN_CENTER,
alignmentY: jimp.VERTICAL_ALIGN_MIDDLE
},
130, 40
);
let n = ("00"+id).slice(-3);
// console.log("Generated tile", n);
canvas.write('tiles/'+n+'.png');
} catch (err) {
console.log(err);
}
});
loadingQueue.push(promise);
}
function combo_tile(id, title) {
let font32 = fonts["Equestria 32"];
let promise = new jimp(150, 150, bgcolor, (err, canvas) => {
// console.log("Writing combo tile", id, title);
canvas.print(
font32, 10, 10,
{
text: title,
alignmentX: jimp.HORIZONTAL_ALIGN_CENTER,
alignmentY: jimp.VERTICAL_ALIGN_MIDDLE
},
130, 130
);
let n = ("00"+id).slice(-3);
canvas.write('tiles/'+n+'.png');
// console.log("Generated prompt",id);
});
loadingQueue.push(promise);
}
Promise.all(loadingQueue).then(() => {
Object.keys(tileData).forEach(index => {
let tile = tileData[index];
if (tile.group == "combo") {
combo_tile(tile.n, tile.title);
} else {
getImage(tile.title).then(image => {
picture_tile(tile.n, tile.title, image, tile.crop);
});
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment