BoilWatch: today’s in-world weather

The dial: turn it to see each level’s weather

Today

The city’s alert system, reporting the weather like feelings. Here it’s one reading a day, worked out from the date in your browser. Same for everyone. Collects nothing.

BOILWATCH STATUS

NO SIGNAL

RECOMMENDATION: READ ANYWAY

Back issues

The official placard

BOILWATCH

The city’s alert system.

Accurate. Trusted. Always on.

Every morning at seven, one reading for the whole city: how wet the air is, how it feels, and what to do about it. Issued without fail since the first morning it was issued.

PRESENTED BY THE CITY OF MACHATTAN · FOR YOUR SAFETY AND CONVENIENCE

Readers who finish a book sometimes find a sticker here. The city does not put them there.

  • Opens when you mark Book One finished. Book One’s sticker

    CORRECTION: THE STREET SAID NINETY.

  • Opens when you mark Book Three finished. Book Three’s sticker

    CORRECTION: THE NUMBERS ARE WRONG.

  • Opens when you mark Book Four finished. Book Four’s sticker

    CORRECTION: THE SPIKE WAS RIGHT. TO THE MINUTE.

  • Opens when you mark Book Seven finished. Book Seven’s sticker

    CORRECTION: IT HASN’T ALWAYS BEEN HERE.

  • Opens when you mark Book Nine finished. Book Nine’s sticker

    CORRECTION: IT TOLD THEM TO STAY PUT.

  • Opens when you mark Book Ten finished. Book Ten’s sticker

    CORRECTION: IT SAID THANK YOU.

The archive

Every alert of the season, as issued. Each book’s first alert is on the wall for everyone. Anything later waits under its own book.

  1. No. 01 · Welcome to the Boil

    BOILWATCH ALERT

    HUMIDITY: 87%

    RISK LEVEL: EMOTIONALLY UNSAFE

    RECOMMENDATION: DO NOT PROCESS FEELINGS OUTSIDE

  2. No. 02 · Saucegate

    BOILWATCH ALERT

    OVERNIGHT HUMIDITY: 84%

    CONFLICT LEVELS: RISING

    RECOMMENDATION: LOCK UP WHAT YOU LOVE

    SPOILERS UNDER PROTECTION PLUS Opens when you mark Book Two finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).

    Tag

    BOILWATCH ALERT

    HUMIDITY RISING

    CONFLICT LEVELS: ELEVATED

    RECOMMENDATION: LOWER EXPECTATIONS

  3. No. 03 · The Clout Pot

    BOILWATCH ALERT

    HUMIDITY: 71%

    RISK LEVEL: MANAGEABLE

    RECOMMENDATION: TRUST THE NUMBERS

  4. No. 04 · Uncooked and Unbothered

    BOILWATCH STATUS

    HUMIDITY: 58%

    RISK LEVEL: LOW

    RECOMMENDATION: STAY FIRM

    SPOILERS UNDER PROTECTION PLUS Opens when you mark Book Four finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).

    Act Three

    BOILWATCH EMERGENCY

    HUMIDITY SPIKE INCOMING

    97% HUMIDITY IN 10 MINUTES

    RECOMMENDATION: SEEK SHELTER IMMEDIATELY

  5. No. 05 · The Pasta Purity Filter

    BOILWATCH ALERT

    HUMIDITY: 86%

    RISK LEVEL: EMOTIONALLY UNSAFE

    RECOMMENDATION: DO NOT FORM NEW EMOTIONAL ATTACHMENTS

  6. No. 06 · Alley Auditions

    BOILWATCH ALERT

    HUMIDITY: 83%

    CONFLICT LEVELS: ELEVATED

    RECOMMENDATION: DO NOT MAKE LIFE DECISIONS TODAY

  7. No. 07 · Doors Are Closing

    BOILWATCH ALERT

    OVERNIGHT HUMIDITY: 88%

    RISK LEVEL: EMOTIONALLY UNSAFE

    RECOMMENDATION: GET HOME SAFE. YOU LOOK TIRED.

  8. No. 08 · Kindness War

    BOILWATCH ALERT

    HUMIDITY EQUALIZATION IN PROGRESS

    CONFLICT LEVELS: CRITICAL

    RECOMMENDATION: SAY SOMETHING NICE

  9. No. 09 · Heat Wave

    BOILWATCH EMERGENCY

    HEAT WAVE INCOMING

    RISK LEVEL: CRITICAL

    EVERYONE WILL GET EMOTIONALLY STICKY

    RECOMMENDATION: ACCEPT YOUR SOFTNESS

    SPOILERS UNDER PROTECTION PLUS Opens when you mark Book Nine finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).

    Day two

    BOILWATCH EMERGENCY

    HUMIDITY: 98%

    RISK LEVEL: CRITICAL

    RECOMMENDATION: STAY INSIDE. STAY PUT.

    Tag

    BOILWATCH ALERT

    HUMIDITY STABLE

    CONFLICT LEVELS: RETURNING TO NORMAL

    RECOMMENDATION: ENJOY THE PEACE WHILE IT LASTS

  10. No. 10 · The Boil Over

    The needle touches ninety-nine, the last number on its dial. Read off a brass dial, and told in words.

    SPOILERS UNDER PROTECTION PLUS Opens when you mark Book Ten finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).

    After the rumble

    BOILWATCH ALERT

    HUMIDITY STABILIZING

    RESUME NORMAL OPERATIONS

    THANK YOU FOR YOUR COOPERATION

The forecast

Forecasts are issued at 7:00 a.m. daily. They have never once been wrong.

On this day

Any morning from the day the wall went up to today.

BOILWATCH STATUS

NO SIGNAL

RECOMMENDATION: READ ANYWAY

From the files

A photocopied memoSPOILERS UNDER PROTECTION PLUS Opens when you mark Book Three finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).

ENVIRONMENTAL SERVICES DIVISION · INTERNAL · HUMIDITY REPORTING FORMULA

Photocopied. Some lines are blacked out. None of them are the numbers.

HUMIDITY TARGETS The share of days given to each level, period by period, as the file sets it.
HUMIDITY TARGETS
PeriodL0L1L2L3
June10%45%30%15%
July5%35%35%25%
August5%30%35%30%
September 1–1510%40%30%20%
Feast days 4–70%40%40%20%
Shoulder35%45%17%3%
Cold60%35%5%0%

Somebody measured. Somebody else decided what got printed.

This is the whole thing. It runs in your browser and sends nothing anywhere.

The formula, as shipped
// BOILWATCH · HUMIDITY REPORTING FORMULA.
//
// One reading a day, the same for everyone on Earth, worked out from the date
// in the reader's browser. Nothing is measured, sent, received or scheduled.
// Integer-only. Never level 5. The recommendation banks and event advisories
// are written lines that callers pass in, the same for every reader; this
// file only decides which line a day gets.

import { fnv1a32, mulberry32, permutation } from './prng.js';
import { issueDay, daysFromCivil, civilFromDays, weekday, iso } from './mdate.js';

// Level weights [L0, L1, L2, L3] by period; the level is the first whose
// running total exceeds u0 % 100.
const SUMMER = { 6: [10, 45, 30, 15], 7: [5, 35, 35, 25], 8: [5, 30, 35, 30] };
const EARLY_SEPTEMBER = [10, 40, 30, 20];
const FEAST = [0, 40, 40, 20];
const SHOULDER = [35, 45, 17, 3];
const COLD = [60, 35, 5, 0];

export function weightsFor(m, d, feastDay) {
  if (feastDay > 3) return FEAST;                          // Feast days 4–7 override any period
  if (SUMMER[m]) return SUMMER[m];
  if (m === 9 && d <= 15) return EARLY_SEPTEMBER;
  if (m === 5 || m === 9 || (m === 10 && d <= 15)) return SHOULDER;
  return COLD;
}

// Feast week: seven days from the second Wednesday of September (1–7, else 0).
export function feastDayOf(n, y) {
  const n9 = daysFromCivil(y, 9, 1);
  const f = n - (n9 + (3 - weekday(n9) + 7) % 7 + 7) + 1;
  return f >= 1 && f <= 7 ? f : 0;
}

// The summer heat run: three days starting between 8 July and 20 August (1–3, else 0).
export function runDayOf(n, y) {
  const r = n - (daysFromCivil(y, 7, 8) + fnv1a32('heat/v1/' + y) % 44) + 1;
  return r >= 1 && r <= 3 ? r : 0;
}

// The level for issue day n, with the five draws every day takes, in order.
export function levelFor(n) {
  const { y, m, d } = civilFromDays(n), s = iso(n);
  const next = mulberry32(fnv1a32('boilwatch/v1/' + s));
  const u = [next(), next(), next(), next(), next()];
  const feastDay = feastDayOf(n, y), runDay = runDayOf(n, y);
  const heatDay = feastDay && feastDay <= 3 ? feastDay : runDay;
  let level = 4;                                            // heat days: six a year
  if (!heatDay) {
    const w = weightsFor(m, d, feastDay);
    let r = u[0] % 100;
    level = 0;
    while (r >= w[level]) r -= w[level++];
  }
  return { n, y, m, d, s, u, level, heatDay, feastDay };
}

// The printed humidity (none on heat day 1) and the material humidity.
export function humidityFor(lv) {
  const L = lv.level, u1 = lv.u[1];
  const hum = L === 4 ? (lv.heatDay === 1 ? null : lv.heatDay === 2 ? 98 : 95)
    : [52 + u1 % 18, 70 + u1 % 15, 85 + u1 % 5, 90 + u1 % 8][L];
  return { hum, humM: hum === null ? 96 : hum };
}

// Book Three's ON THE STREET number: the official reading runs two to four points low.
export const streetNumber = (humM, u4) => Math.min(98, humM + 2 + u4 % 3);

// The notice's fixed parts: headers, risk lines and the footer, exactly as printed.
export const HEADERS = ['BOILWATCH STATUS', 'BOILWATCH ALERT', 'BOILWATCH ALERT',
  'BOILWATCH EMERGENCY', 'BOILWATCH EMERGENCY'];         // L3 and L4 carry the warning sign
export const RISK = [
  ['RISK LEVEL: LOW', 'CONFLICT LEVELS: RETURNING TO NORMAL'],
  ['RISK LEVEL: MANAGEABLE', 'CONFLICT LEVELS: RISING', 'CONFLICT LEVELS: ELEVATED'],
  ['RISK LEVEL: EMOTIONALLY UNSAFE', 'CONFLICT LEVELS: CRITICAL'],
  ['RISK LEVEL: EMOTIONALLY UNSAFE', 'CONFLICT LEVELS: CRITICAL'],
  ['RISK LEVEL: CRITICAL'],
];
export const FOOTER = 'ISSUED 7:00 A.M. · MACHATTAN TIME';
export const BANK_LENGTHS = [20, 25, 25, 20, 10];          // lines in each level's bank, L0–L4
export const WARNING_TEXT = '⚠︎';                 // the sign in copied plain text

// Which recommendation a day gets: no line repeats until its level's bank is used up.
export function recommendationFor(lv, len = BANK_LENGTHS[lv.level]) {
  let ordinal = 0;
  for (let k = daysFromCivil(lv.y, 1, 1); k < lv.n; k++) if (levelFor(k).level === lv.level) ordinal++;
  const cycle = Math.floor(ordinal / len);
  const index = permutation(len, 'perm/v1/' + lv.y + '/' + lv.level + '/' + cycle)[ordinal % len];
  return { ordinal, cycle, index };
}

// The day's event, if any: a public dated premiere ({id, date: 'YYYY-MM-DD'}) outranks
// the season opening on 1 June. Feast week adds no line (its first three days are heat days).
export function eventFor(lv, dated = []) {
  for (const e of dated) if (e && e.date === lv.s) return e.id;
  return lv.m === 6 && lv.d === 1 ? 'season-open' : null;
}

// BoilWatch Daily No. N: day 1 is the wall's founding date; earlier days do not exist.
export function dailyNumber(n, foundedIso) {
  const [y, m, d] = String(foundedIso).split('-').map(Number);
  const k = n - daysFromCivil(y, m, d) + 1;
  return k >= 1 ? k : null;
}

// The wall's day at an instant: the issue day n and its date, BoilWatch Daily's
// number, and k = n − founded, the day index every daily pool draws with
// (poolPick(zone, k, m, len)); the sign war's parity is number % 2. Before the founding
// morning the wall has no day yet: number and k are null. foundedIso is the wall's
// founding date as the page carries it (wall_founded).
export function wallDay(ms, foundedIso) {
  const n = issueDay(ms), number = dailyNumber(n, foundedIso);
  return { n, iso: iso(n), number, k: number === null ? null : number - 1 };
}

// The whole notice for an instant. banks: {0: [...], 1: [...], ...} (items or strings);
// events: {'season-open': {line, advisory, ...}, dated: [{id, date, line, advisory}]}.
export function readingFor(ms, banks = {}, events = {}) {
  const lv = levelFor(issueDay(ms)), L = lv.level, u = lv.u;
  const { hum, humM } = humidityFor(lv);
  const bank = banks[L], rec = recommendationFor(lv, bank && bank.length ? bank.length : BANK_LENGTHS[L]);
  const dated = events.dated || [], eventId = eventFor(lv, dated);
  const event = eventId ? (dated.find(e => e.id === eventId) || events[eventId] || { id: eventId }) : null;
  return {
    n: lv.n, iso: lv.s, level: L, hum, humM, heatDay: lv.heatDay, feastDay: lv.feastDay,
    header: HEADERS[L], warning: L >= 3,
    metric: lv.heatDay === 1 ? 'HEAT WAVE INCOMING'
      : ((L === 1 || L === 2) && u[2] % 5 === 0 ? 'OVERNIGHT ' : '') + 'HUMIDITY: ' + hum + '%',
    risk: RISK[L][u[3] % RISK[L].length],
    street: streetNumber(humM, u[4]),
    recommendation: { ...rec, item: bank && bank.length ? bank[rec.index] : null },
    event,
    footer: FOOTER,
  };
}

// The notice as plain text, one line per line (Take a copy; the warning sign as text).
export function noticeLines(r, recommendationText) {
  const head = r.warning ? WARNING_TEXT + ' ' + r.header + ' ' + WARNING_TEXT : r.header;
  const lines = [head, r.metric, r.risk];
  if (r.event && r.event.line) lines.push(r.event.line);
  const rec = r.event && r.event.advisory ? r.event.advisory : recommendationText;
  if (rec) lines.push(rec);
  return lines;
}

// Material level M0–M4 (data-wet): decoration only, never layout. Season heat: each
// finished book except the finale adds 1.5 points; after the finale the wall settles
// ten points drier for good. Stay firm pins M0.
export function materialFor(reading, progress, prefs, finaleId) {
  if (prefs && prefs.firm) return 0;
  const done = (progress && progress.finished) || [];
  const finale = done.indexOf(finaleId) >= 0;
  let others = 0;
  for (const id of done) if (id !== finaleId && String(id).slice(0, 5) === 'book:') others++;
  const eff = finale ? reading.humM - 10 : reading.humM + Math.floor(3 * others / 2);
  return (reading.level === 4 && !finale) || eff >= 94 ? 4 : eff >= 88 ? 3 : eff >= 80 ? 2 : eff >= 70 ? 1 : 0;
}

// Time of day (data-tod) from the visitor's own clock, or the Shade setting.
// Time changes flavour, never access.
export function todFor(date, shade) {
  if (shade === 'day' || shade === 'night') return { tod: shade, sweep: false };
  if (shade === '7am') return { tod: 'dawn', sweep: true };
  if (shade === '2am') return { tod: 'late', sweep: false };
  const h = date.getHours();
  return { tod: h < 5 ? 'late' : h < 7 ? 'dawn' : h < 16 ? 'day' : h < 19 ? 'golden' : 'night',
    sweep: h === 7 && date.getMinutes() < 15 };
}

// A daily pool: the m item indexes for wall day k from a bank of len everyday lines.
export function poolPick(zone, k, m, len) {
  if (len % m) throw new RangeError('poolPick: the bank length must be a multiple of the daily draw');
  const out = [];
  for (let i = 0; i < m; i++) {
    const p = k * m + i;
    out.push(permutation(len, 'pool/v1/' + zone + '/' + Math.floor(p / len))[p % len]);
  }
  return out;
}

Back to the wall

The brass dial

Preview a level

The ladder

BoilWatch’s five levels, as issued. The street has its own names for them.

LevelThe headerWhat the street calls itReadingColor
L0BOILWATCH STATUScold potunder 70% none
L1BOILWATCH ALERTsimmer70–84% yellow
L2BOILWATCH ALERT · EMOTIONALLY UNSAFEsticky85–89% orange
L3BOILWATCH EMERGENCYrolling boil90% and over red
L4BOILWATCH EMERGENCY · CRITICALthe meltheat wave pulsing red

On the wall the levels run L0 to L3 most days. L4 is kept for heat waves: six days a year, and nobody is told which in advance.

SPOILERS UNDER PROTECTION PLUS Opens when you mark Book Ten finished. Coverage: Basic (free). Premium (also free). Platinum Elite (free, but Louie calls you).
Back to the billboard: the camera pulls back out