Last active
February 10, 2020 22:01
-
-
Save marcus-downing/c68ebe754e4ecf7bd14ddf5b94fd1e0a to your computer and use it in GitHub Desktop.
Scripts for making randomised prompts for a writing contest.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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