games.html
This is a browser-based viewer for exploring the contents of games.json
(generated by games.mjs
). It lets you group, filter, and browse JS13K game entries by year or author — all directly from a static webpage.
games.json
dynamically in the browserThe Group by Author query uses parent
as the author, if available, otherwise extracts the word after the last occurrence of by in the description
, otherwise categorized as unknown. The parent field is not reliable for extracting author information because participants might delete their repositories.
The Group by Year query includes the full description
.
All three queries need the year. It is the first year in the description
that is not found in the name, unless there are no other, otherwise it uses created_at
, which isn’t accurate because some old games got forked in the year of the cat.
games.mjs
⚠️ Warning: These are not usage instructions — this is just how I generate the JSON file…maybe once a day during the August/September competition.
This script fetches metadata for all public repositories in the js13kGames GitHub organization and saves a simplified log (games.json
) with just the essential fields.
@octokit/rest
p-limit
Install dependencies using NPM:
npm install @octokit/rest p-limit
Required libraries:
@octokit/rest
– GitHub API client libraryp-limit
– Limits the number of concurrent API calls to avoid throttlinggames.json
)Each repository is logged with fields like:
{
"name": "repo-name",
"full_name": "js13kGames/repo-name",
"owner": "js13kGames",
"description": "Game description",
"homepage": "https://game.example.com",
"created_at": "2023-09-13T12:34:56Z",
"size": 1234,
"html_url": "https://github.com/js13kGames/repo-name",
"parent": "original-author/repo-name",
"source": "original-author/repo-name",
"organization": "js13kGames"
}
export GITHUB_TOKEN=your_personal_access_token
node games.mjs
games.json
.Inside the script:
LIMIT = 0
: Fetches all repositories (recommended for full runs).LIMIT = 10
: Recommended for testing, to avoid hitting the GitHub rate limit.MAX_PARALLEL = 10
: Controls how many requests run concurrently.js13kGames
organization has over 2,000 repositories, and each one requires an individual API call.Thinky is a query tool for analyzing the source code of 188 games in the js13kgames 2024 competition
ever wonder who uses speechsynth?
https://bacionejs.github.io/stuff/thinky.html?word=speechSynthesis
or who doesn’t use a while/for loop?
https://bacionejs.github.io/stuff/thinky.html?word=-while,-for
audio?
https://bacionejs.github.io/stuff/thinky.html?word=AudioContext
zzfx?
https://bacionejs.github.io/stuff/thinky.html?word=zzfx
find your buddies and other strange oddities at Thinky
Brought to you by Stuff
Here are some scripts to analyze your own code.
Sort all words by length
cat minified.js|sed 's/base64[^=]*=//g'|tr -cs '[:alnum:]_' '\n'|awk '{print length,$0}'|sort -u|sort -n|cut -d' ' -f2
THESE ARE NOT INSTRUCTIONS
I posted these notes so other people can
These scripts were run on Android Termux
unroll.js
(un-roadroller) script is a homegrown hack. If you know the right way, like a tool, or better unroll logic, please let me know.extract.sh
script extracts the data to be used by thinky.js
Manually paste into page.txt
, the text of the https://js13kgames.com/2024/games website
Make a folder called games
Run ./git.sh
#!/bin/bash
#get game names
cat page.txt | sed '1,4d' | head -n -20 | awk 'NR % 3 == 1' | \
#fix game names
tr '[:upper:]' '[:lower:]' | \
sed "s/['.]//g" | \
sed 's/[^a-zA-Z0-9]/-/g' | sed 's/-\{2,\}/-/g' | sed 's/^-//;s/-$//' | \
#download submitted zips
while read folder; do
mkdir games/$folder
if curl -s -o /dev/null -w "%{http_code}" -L https://github.com/js13kGames/games/raw/main/games/$folder/.src/g.zip | grep "200"; then
curl -L -o $folder.zip https://github.com/js13kGames/games/raw/main/games/$folder/.src/g.zip
mv $folder.zip games/$folder/g.zip
else
echo "error: "$folder
fi
done
#unzip
find games -name "*.zip" -exec sh -c 'unzip -q "{}" -d "$(dirname "{}")" || echo "$PWD/{}"' +
#delete zip
find games -name "*.zip" -exec rm -f {} +
#fix folder spaces
find games -name '* *' -exec bash -c 'mv -v "$0" "`echo $0 | tr " " "-"`"' {} +
Run node unroll.js games
let fs = require('fs');
let path = require('path');
let vm = require('vm');
function extract(s) {
if (new RegExp(/Function\s*\(\s*"\[M/gs).test(s)){
let regex=/Function\s*\(\s*"\[M/gs;
let match=regex.exec(s);
let start=match.index;
let p=1;
let i=start+match[0].length;
while(i<s.length && p>0){if(s[i]=='(')p++;if(s[i]==')')p--;i++;}
while(i<s.length && s[i]!=='('){i++;}
let args=1;
i++;
while(i<s.length && args>0){if(s[i]=='(')args++;if(s[i]==')')args--;i++;}
s=s.substring(start,i);
s=s.replace(/document.*"/,'return String.fromCharCode(...c)"');
return s;
}else if(new RegExp(/<script>\s*M\s*=/g) .test(s)){
s=s.replace(new RegExp(/document.*</g),'String.fromCharCode(...n)<');
s=s.replace(new RegExp(/eval\(r\)/g), 'eval(JSON.stringify(r))');
return s.replace(/[\s\S]*<script\b[^>]*>([\s\S]*?)<\/script>[\s\S]*/,'$1');
}else{
return undefined;
}
}
function main(file) {
fs.readFile(file, 'utf8', (err, data) => {
if (err) { console.error("Error reading file:", err); return; }
let script,unrolled,s;
try {
s = extract(data);
if (s) {
script = new vm.Script(s);
unrolled = script.runInContext(vm.createContext({}));
overwrite(file,unrolled);
console.log(`Successfully updated: ${file}`);
}
} catch (error) {
console.error("Error:", `${file} ${error.message}`);
}
});
}
function overwrite(file,unrolled){
fs.writeFile(file, unrolled, (writeErr) => {
if (writeErr) {
console.error("Error writing file:", writeErr);
}
});
}
function processDirectory(dir) {
fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
if (err) { console.error("Error reading directory:", err); return; }
entries.forEach(entry => {
let fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { // Recursively process subdirectory
processDirectory(fullPath);
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.html'))) {
main(fullPath);
}
});
});
}
let startDir = path.resolve(process.argv[2]);
processDirectory(startDir);
Run ./extract.sh
#!/bin/bash
# Canonicalize zzfx
find games -type f \( -name '*.html' -o -name '*.js' \) -exec sed -i -r 's/module:\[\[\[/zzfx/gi;s/zzfx\w*/zzfx/gi' {} +
# Find all .js and .html files
# Remove base64
# Split the remaining content into tokens based on non-alphanumeric except _
find games -type f \( -name "*.js" -o -name "*.html" \) -exec \
awk --posix '{
gsub(/base64[^=]+=/, "");
n = split($0, tokens, /[^[:alnum:]_]/);
for (i=1; i<=n; i++) if (tokens[i] != "") print FILENAME, tokens[i];
}' {} + | \
# Extract the game name and token
awk -F'[/ ]' '{print $2, $NF}' | \
# Remove tokens starting with a digit
awk '$2 !~ /^[0-9]/' | \
# Remove tokens < 3 characters
awk 'length($2) > 2' | \
# Sort by token
sort -k2,2 | \
# Remove duplicates
uniq | \
# Remove tokens used by < 2 games
awk '{count[$2]++; games[$2] = (games[$2] ? games[$2] ORS : "") $1 " " $2} END {for (word in count) if (count[word] > 1) print games[word]}' | \
cat > data.txt
# Create foreignkeys for tokens
cat data.txt | awk '{if (!($2 in b)) { b[$2] = i++; } a[$1] = a[$1] ? a[$1] "," b[$2] : $1 "," b[$2]; } END { for (k in a) print a[k] }' > games.txt
# Create lookup table for tokens
cat data.txt | awk '{print $2}' | uniq | tr '\n' ',' | sed 's/,$//' > words.txt