feat: add 10 new puzzles with automated image processing pipeline

- Added process_puzzles.sh script for end-to-end puzzle image management:
  - Renames prep/ files to lowercase with underscores
  - Moves validated images to puzzles/ directory
  - Generates standardized thumbnails (576×324) using ImageMagick
  - Syncs puzzles.json and thumbnails.json manifests

- Added 10 new puzzle images (and corresponding thumbnails):
  cownappers, forest_conjurer, germanic_castle, hong_kong_phonecall,
  lonely_robot_2, mushroom_world, octomech_2, old_polish_town,
  red_eyes, the_mall_interior

- Updated asset manifests with new puzzle entries and thumbnails
This commit is contained in:
Brian Fertig 2026-04-06 20:01:25 -06:00
parent 02e0e70b99
commit 23b43a6f2a
23 changed files with 267 additions and 0 deletions

167
assets/images/process_puzzles.sh Executable file
View File

@ -0,0 +1,167 @@
#!/bin/bash
#
# Process new puzzle images end-to-end:
# 1. Rename files in prep/ to lowercase with underscores
# 2. Move them to puzzles/
# 3. Generate thumbnails for any images missing one
# 4. Sync puzzles.json and thumbnails.json
#
# Run from: assets/images/
# Requires: ImageMagick (convert command)
set -euo pipefail
cd "$(dirname "$0")"
PREP_DIR="prep"
PUZZLE_DIR="puzzles"
REPO_ROOT=$(git rev-parse --show-toplevel)
PUZZLES_JSON="$REPO_ROOT/assets/puzzles.json"
THUMBS_JSON="$REPO_ROOT/assets/thumbnails.json"
THUMB_WIDTH=576
THUMB_HEIGHT=324
# ─── 1. Rename files in prep/ ──────────────────────────────────────────
echo "=== Step 1: Renaming files in $PREP_DIR/ ==="
renamed=0
for f in "$PREP_DIR"/*.png "$PREP_DIR"/*.jpg "$PREP_DIR"/*.jpeg; do
[ -f "$f" ] || continue
dir=$(dirname "$f")
base=$(basename "$f")
newname=$(echo "$base" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
if [ "$base" != "$newname" ]; then
mv "$f" "$dir/$newname"
echo " Renamed: $base -> $newname"
renamed=$((renamed + 1))
fi
done
echo " Renamed $renamed file(s)"
# ─── 2. Move files from prep/ to puzzles/ ──────────────────────────────
echo ""
echo "=== Step 2: Moving images from $PREP_DIR/ to $PUZZLE_DIR/ ==="
moved=0
for f in "$PREP_DIR"/*.png "$PREP_DIR"/*.jpg "$PREP_DIR"/*.jpeg; do
[ -f "$f" ] || continue
base=$(basename "$f")
# Skip shell scripts
case "$base" in *.sh) continue ;; esac
if [ -f "$PUZZLE_DIR/$base" ]; then
echo " SKIP $base (already exists in $PUZZLE_DIR/)"
else
mv "$f" "$PUZZLE_DIR/$base"
echo " Moved: $base"
moved=$((moved + 1))
fi
done
echo " Moved $moved file(s)"
# ─── 3. Generate thumbnails ────────────────────────────────────────────
echo ""
echo "=== Step 3: Generating thumbnails ==="
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick is not installed."
echo " Ubuntu/Debian: sudo apt install imagemagick"
echo " macOS: brew install imagemagick"
exit 1
fi
created=0
skipped=0
for file in "$PUZZLE_DIR"/*; do
[ -f "$file" ] || continue
base=$(basename "$file")
# Skip thumbnails, scripts
case "$base" in
thumb_*|*.sh) continue ;;
esac
# Check it's actually an image
mime=$(file --mime-type -b "$file")
case "$mime" in
image/*) ;;
*) continue ;;
esac
thumb="$PUZZLE_DIR/thumb_${base}"
if [ -f "$thumb" ]; then
skipped=$((skipped + 1))
continue
fi
convert "$file" -resize "${THUMB_WIDTH}x${THUMB_HEIGHT}!" -strip "$thumb"
echo " Created: thumb_${base}"
created=$((created + 1))
done
echo " Created $created thumbnail(s), skipped $skipped existing"
# ─── 4. Sync JSON manifests ────────────────────────────────────────────
echo ""
echo "=== Step 4: Syncing JSON manifests ==="
puzzles="["
thumbs="["
first=true
for img in "$PUZZLE_DIR"/*.png "$PUZZLE_DIR"/*.jpg "$PUZZLE_DIR"/*.jpeg; do
[ -f "$img" ] || continue
base=$(basename "$img")
ext="${base##*.}"
name="${base%.*}"
# Skip thumbnails
[[ "$name" == thumb_* ]] && continue
# Derive keys (replace hyphens with underscores)
safe="${name//-/_}"
puzzle_key="puzzle_${safe}"
thumb_key="thumb_${safe}"
thumb_file="$PUZZLE_DIR/thumb_${name}.${ext}"
# Skip if no matching thumbnail
if [ ! -f "$thumb_file" ]; then
echo " WARNING: No thumbnail for $name — skipping"
continue
fi
# Build label: replace underscores/hyphens with spaces, then title-case
label=$(echo "$name" | sed 's/[_-]/ /g' | sed 's/\b\(.\)/\u\1/g')
# Paths relative to repo root
rel_img="assets/images/puzzles/${base}"
rel_thumb="assets/images/puzzles/thumb_${name}.${ext}"
sep=""
$first || sep=","
first=false
puzzles+="${sep}
{
\"key\": \"${puzzle_key}\",
\"path\": \"${rel_img}\",
\"label\": \"${label}\"
}"
thumbs+="${sep}
{
\"key\": \"${thumb_key}\",
\"path\": \"${rel_thumb}\",
\"puzzleKey\": \"${puzzle_key}\"
}"
done
puzzles+="
]"
thumbs+="
]"
echo "$puzzles" > "$PUZZLES_JSON"
echo "$thumbs" > "$THUMBS_JSON"
echo " Updated puzzles.json ($(grep -c '"key"' "$PUZZLES_JSON") entries)"
echo " Updated thumbnails.json ($(grep -c '"key"' "$THUMBS_JSON") entries)"
echo ""
echo "=== Done! ==="

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View File

@ -44,6 +44,11 @@
"path": "assets/images/puzzles/cottages_on_the_street.png",
"label": "Cottages On The Street"
},
{
"key": "puzzle_cownappers",
"path": "assets/images/puzzles/cownappers.png",
"label": "Cownappers"
},
{
"key": "puzzle_dwarven_blacksmith",
"path": "assets/images/puzzles/dwarven_blacksmith.png",
@ -69,6 +74,11 @@
"path": "assets/images/puzzles/floating_wizards_village.png",
"label": "Floating Wizards Village"
},
{
"key": "puzzle_forest_conjurer",
"path": "assets/images/puzzles/forest_conjurer.png",
"label": "Forest Conjurer"
},
{
"key": "puzzle_four_seasons_2",
"path": "assets/images/puzzles/four_seasons_2.png",
@ -84,6 +94,11 @@
"path": "assets/images/puzzles/fuzzy_halloween.png",
"label": "Fuzzy Halloween"
},
{
"key": "puzzle_germanic_castle",
"path": "assets/images/puzzles/germanic_castle.png",
"label": "Germanic Castle"
},
{
"key": "puzzle_golden_in_the_mountains",
"path": "assets/images/puzzles/golden_in_the_mountains.png",
@ -114,6 +129,11 @@
"path": "assets/images/puzzles/home_sweet_home.png",
"label": "Home Sweet Home"
},
{
"key": "puzzle_hong_kong_phonecall",
"path": "assets/images/puzzles/hong_kong_phonecall.png",
"label": "Hong Kong Phonecall"
},
{
"key": "puzzle_jeweled_frogs",
"path": "assets/images/puzzles/jeweled_frogs.png",
@ -129,6 +149,11 @@
"path": "assets/images/puzzles/lake_treehouse.png",
"label": "Lake Treehouse"
},
{
"key": "puzzle_lonely_robot_2",
"path": "assets/images/puzzles/lonely_robot_2.png",
"label": "Lonely Robot 2"
},
{
"key": "puzzle_lone_ship_in_the_seas",
"path": "assets/images/puzzles/lone_ship_in_the_seas.png",
@ -159,6 +184,21 @@
"path": "assets/images/puzzles/mountain_blanket.png",
"label": "Mountain Blanket"
},
{
"key": "puzzle_mushroom_world",
"path": "assets/images/puzzles/mushroom_world.png",
"label": "Mushroom World"
},
{
"key": "puzzle_octomech_2",
"path": "assets/images/puzzles/octomech_2.png",
"label": "Octomech 2"
},
{
"key": "puzzle_old_polish_town",
"path": "assets/images/puzzles/old_polish_town.png",
"label": "Old Polish Town"
},
{
"key": "puzzle_opulant_flowers",
"path": "assets/images/puzzles/opulant_flowers.png",
@ -169,6 +209,11 @@
"path": "assets/images/puzzles/orc_and_the_undead.png",
"label": "Orc And The Undead"
},
{
"key": "puzzle_red_eyes",
"path": "assets/images/puzzles/red_eyes.png",
"label": "Red Eyes"
},
{
"key": "puzzle_red_panda_in_the_bamboo_forest",
"path": "assets/images/puzzles/red_panda_in_the_bamboo_forest.png",
@ -204,6 +249,11 @@
"path": "assets/images/puzzles/the_disassembled_android.png",
"label": "The Disassembled Android"
},
{
"key": "puzzle_the_mall_interior",
"path": "assets/images/puzzles/the_mall_interior.png",
"label": "The Mall Interior"
},
{
"key": "puzzle_the_old_w0rm",
"path": "assets/images/puzzles/the_old_w0rm.png",

View File

@ -44,6 +44,11 @@
"path": "assets/images/puzzles/thumb_cottages_on_the_street.png",
"puzzleKey": "puzzle_cottages_on_the_street"
},
{
"key": "thumb_cownappers",
"path": "assets/images/puzzles/thumb_cownappers.png",
"puzzleKey": "puzzle_cownappers"
},
{
"key": "thumb_dwarven_blacksmith",
"path": "assets/images/puzzles/thumb_dwarven_blacksmith.png",
@ -69,6 +74,11 @@
"path": "assets/images/puzzles/thumb_floating_wizards_village.png",
"puzzleKey": "puzzle_floating_wizards_village"
},
{
"key": "thumb_forest_conjurer",
"path": "assets/images/puzzles/thumb_forest_conjurer.png",
"puzzleKey": "puzzle_forest_conjurer"
},
{
"key": "thumb_four_seasons_2",
"path": "assets/images/puzzles/thumb_four_seasons_2.png",
@ -84,6 +94,11 @@
"path": "assets/images/puzzles/thumb_fuzzy_halloween.png",
"puzzleKey": "puzzle_fuzzy_halloween"
},
{
"key": "thumb_germanic_castle",
"path": "assets/images/puzzles/thumb_germanic_castle.png",
"puzzleKey": "puzzle_germanic_castle"
},
{
"key": "thumb_golden_in_the_mountains",
"path": "assets/images/puzzles/thumb_golden_in_the_mountains.png",
@ -114,6 +129,11 @@
"path": "assets/images/puzzles/thumb_home_sweet_home.png",
"puzzleKey": "puzzle_home_sweet_home"
},
{
"key": "thumb_hong_kong_phonecall",
"path": "assets/images/puzzles/thumb_hong_kong_phonecall.png",
"puzzleKey": "puzzle_hong_kong_phonecall"
},
{
"key": "thumb_jeweled_frogs",
"path": "assets/images/puzzles/thumb_jeweled_frogs.png",
@ -129,6 +149,11 @@
"path": "assets/images/puzzles/thumb_lake_treehouse.png",
"puzzleKey": "puzzle_lake_treehouse"
},
{
"key": "thumb_lonely_robot_2",
"path": "assets/images/puzzles/thumb_lonely_robot_2.png",
"puzzleKey": "puzzle_lonely_robot_2"
},
{
"key": "thumb_lone_ship_in_the_seas",
"path": "assets/images/puzzles/thumb_lone_ship_in_the_seas.png",
@ -159,6 +184,21 @@
"path": "assets/images/puzzles/thumb_mountain_blanket.png",
"puzzleKey": "puzzle_mountain_blanket"
},
{
"key": "thumb_mushroom_world",
"path": "assets/images/puzzles/thumb_mushroom_world.png",
"puzzleKey": "puzzle_mushroom_world"
},
{
"key": "thumb_octomech_2",
"path": "assets/images/puzzles/thumb_octomech_2.png",
"puzzleKey": "puzzle_octomech_2"
},
{
"key": "thumb_old_polish_town",
"path": "assets/images/puzzles/thumb_old_polish_town.png",
"puzzleKey": "puzzle_old_polish_town"
},
{
"key": "thumb_opulant_flowers",
"path": "assets/images/puzzles/thumb_opulant_flowers.png",
@ -169,6 +209,11 @@
"path": "assets/images/puzzles/thumb_orc_and_the_undead.png",
"puzzleKey": "puzzle_orc_and_the_undead"
},
{
"key": "thumb_red_eyes",
"path": "assets/images/puzzles/thumb_red_eyes.png",
"puzzleKey": "puzzle_red_eyes"
},
{
"key": "thumb_red_panda_in_the_bamboo_forest",
"path": "assets/images/puzzles/thumb_red_panda_in_the_bamboo_forest.png",
@ -204,6 +249,11 @@
"path": "assets/images/puzzles/thumb_the_disassembled_android.png",
"puzzleKey": "puzzle_the_disassembled_android"
},
{
"key": "thumb_the_mall_interior",
"path": "assets/images/puzzles/thumb_the_mall_interior.png",
"puzzleKey": "puzzle_the_mall_interior"
},
{
"key": "thumb_the_old_w0rm",
"path": "assets/images/puzzles/thumb_the_old_w0rm.png",