diff --git a/tools/deploy_hardyards.sh b/tools/deploy_hardyards.sh
new file mode 100755
index 0000000..dda100a
--- /dev/null
+++ b/tools/deploy_hardyards.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Deploy HARD YARDS to partly.party/hardyards (games VPS, forum-nginx).
+# Pattern per ~/.claude/skills/deploy-map: bundle -> /tmp staging -> docker cp
+# -> curl verify. The bundle is web/ ONLY — never docs/, THREADS.md, or tools/
+# (they carry fleet detail that has no business on a public host).
+set -eu
+cd "$(dirname "$0")/.."
+
+VPS=humanjing@100.71.119.27
+ROOT=/usr/share/nginx/html/hardyards
+
+# Redirect index so partly.party/hardyards/ lands on the game.
+STAGE=$(mktemp -d)
+cat > "$STAGE/index.html" << 'HTML'
+
+
+
HARD YARDS
+
+HARD YARDS — rig it, then survive the night
+HTML
+
+rsync -a --delete web/ "$VPS":/tmp/hardyards-stage/
+rsync -a "$STAGE/index.html" "$VPS":/tmp/hardyards-stage/index.html
+rm -rf "$STAGE"
+ssh "$VPS" "docker exec forum-nginx rm -rf $ROOT \
+ && docker cp /tmp/hardyards-stage forum-nginx:$ROOT"
+
+# Verify with our own eyes (cache-busted; Cloudflare fronts partly.party).
+CB=$(date +%s)
+for p in "?cb=$CB" "world/index.html?cb=$CB" \
+ "world/data/sites/backyard_01.json?cb=$CB" \
+ "world/vendor/three.module.js?cb=$CB"; do
+ code=$(curl -s -o /dev/null -w '%{http_code}' "https://partly.party/hardyards/$p")
+ echo "$code /hardyards/$p"
+ [ "$code" = 200 ] || { echo "NOT DEPLOYED: $p returned $code" >&2; exit 1; }
+done
+echo "deployed: https://partly.party/hardyards/"