#!/usr/bin/env bash # # The daily challenge's schedule: check it, and add a day to it. # # scripts/daily.sh check is the schedule stocked and published? # scripts/daily.sh add [date] # # Meant to run on a schedule. There is nothing to "roll over" at midnight — # which fight is today's is computed from the dates in the schedule, by the # site and by the API alike, so no job has to be up for the day to change. # What a job is for is the thing that *does* need a human: noticing that the # schedule is running out before it does, and saying so while there is still # time to write another one. # # `check` fails when the schedule has fewer than DAYS_AHEAD days left, or when # the published copies have drifted from the scenarios. Both are conditions # somebody has to act on, which is what a non-zero exit is for. # # `add` is the mechanical half of putting a new challenge in: it validates the # file, copies it in and appends it to the schedule. It deliberately does not # write the scenario — that is a design job, for a person or for whatever # generates one — and it cannot fill in the battle values, which come from # loading the fight in real MegaMek: # # arena: java -cp ... arena.ScenarioReport > report.json # here: cargo run -p headquarters-mms --features cli --bin mms -- index ... # set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" DAILY_DIR="services/api/src/matches/daily" SCHEDULE="$DAILY_DIR/schedule.json" INDEX="web/src/content/daily.json" # How much runway the schedule should keep. Three days is enough that a check # failing on a Friday can be dealt with on Monday. DAYS_AHEAD="${DAYS_AHEAD:-3}" die() { echo "daily: $*" >&2; exit 1; } today() { date -u +%F; } # The scheduled days, one per line, in file order. days() { python3 -c ' import json, sys for slot in json.load(open(sys.argv[1])): print(slot["date"], slot["slug"]) ' "$SCHEDULE" } check() { local now last remaining stale=0 now="$(today)" [ -f "$SCHEDULE" ] || die "no schedule at $SCHEDULE" # Every scheduled slug has a scenario, and every scenario is scheduled. local scheduled scheduled="$(days | awk '{print $2}' | sort)" local present present="$(find "$DAILY_DIR" -name '*.mms' -printf '%f\n' | sed 's/\.mms$//' | sort)" if [ "$scheduled" != "$present" ]; then echo "daily: the schedule and the scenario files disagree:" >&2 diff <(echo "$scheduled") <(echo "$present") >&2 || true stale=1 fi # Every scenario parses. A challenge that does not is one nobody can play. # This is not the whole check — a file can parse and still name a board no # image has — which is arena's tests/scenario-load.sh. cargo run -q -p headquarters-mms --features cli --bin mms -- \ check "$DAILY_DIR"/*.mms >/dev/null || die "a scenario does not parse" # The site's copy of the index is current. cargo run -q -p headquarters-mms --features cli --bin mms -- index \ --scenarios "$DAILY_DIR" \ --schedule "$SCHEDULE" \ --report "$DAILY_DIR/report.json" \ --out "$INDEX" --check >/dev/null || { echo "daily: $INDEX is stale; re-run mms index" >&2 stale=1 } # And the downloadable copies are the files the API launches. npm --prefix web run --silent daily-public -- --check >/dev/null || { echo "daily: web/public/daily/ is out of step; run npm --prefix web run daily-public" >&2 stale=1 } last="$(days | awk '{print $1}' | sort | tail -1)" remaining="$(( ( $(date -u -d "$last" +%s) - $(date -u -d "$now" +%s) ) / 86400 ))" echo "daily: today is $now; the schedule runs to $last ($remaining day(s) left)" [ "$stale" -eq 0 ] || die "the published files are out of step with the scenarios" if [ "$remaining" -lt "$DAYS_AHEAD" ]; then die "fewer than $DAYS_AHEAD days of challenges left; write another one" fi } add() { local file="${1:-}" date="${2:-}" [ -n "$file" ] || die "usage: daily.sh add [YYYY-MM-DD]" [ -f "$file" ] || die "no such file: $file" local slug slug="$(basename "$file" .mms)" [ -f "$DAILY_DIR/$slug.mms" ] && die "$slug is already a challenge" # The day after the last scheduled one, unless told otherwise. A gap in the # schedule is allowed — for_day falls back to the most recent past challenge # — but it is never what somebody meant. if [ -z "$date" ]; then local last last="$(days | awk '{print $1}' | sort | tail -1)" date="$(date -u -d "$last + 1 day" +%F)" fi cargo run -q -p headquarters-mms --features cli --bin mms -- check "$file" \ || die "$file does not parse" cp "$file" "$DAILY_DIR/$slug.mms" python3 - "$SCHEDULE" "$slug" "$date" <<'PY' import json, sys path, slug, date = sys.argv[1], sys.argv[2], sys.argv[3] schedule = json.load(open(path)) if any(s["date"] == date for s in schedule): sys.exit(f"daily: {date} already has a challenge") schedule.append({"slug": slug, "date": date}) schedule.sort(key=lambda s: s["date"]) # One entry per line, which is how this file is kept. json.dump(indent=2) # would spread every entry over four lines and turn adding one challenge # into a fifty-line diff over eleven that did not change. with open(path, "w") as fh: rows = [f' {{ "slug": "{s["slug"]}", "date": "{s["date"]}" }}' for s in schedule] fh.write("[\n" + ",\n".join(rows) + "\n]\n") PY echo "daily: $slug is scheduled for $date" echo echo "Still to do, because neither can be done from the .mms alone:" echo " 1. add it to CHALLENGES in $DAILY_DIR/../daily.rs" echo " 2. load it in arena's ScenarioReport, merge the result into" echo " $DAILY_DIR/report.json, then re-run mms index and daily-public" } case "${1:-check}" in check) check ;; add) shift; add "$@" ;; *) die "usage: daily.sh " ;; esac