BOILWATCH STATUS
NO SIGNAL
RECOMMENDATION: READ ANYWAY
ISSUED 7:00 A.M. · MACHATTAN TIME
Every switch applies at once and is remembered in this browser only.
These switches need JavaScript. Your device’s own motion and contrast settings still apply to every page.
Steam, neon, a train somewhere underneath. Press again to make it stop existing.
No sag, no drip, no ink run. The wall stays dry for you.
Screen readers hear the street only if you ask.
Your progress code:
No account. Type it into another browser to carry your season over.
This clears your stamps, notes, scores and settings in this browser. Nothing else knows them.
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
ISSUED 7:00 A.M. · MACHATTAN TIME
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.
CORRECTION: THE STREET SAID NINETY.
CORRECTION: THE NUMBERS ARE WRONG.
CORRECTION: THE SPIKE WAS RIGHT. TO THE MINUTE.
CORRECTION: IT HASN’T ALWAYS BEEN HERE.
CORRECTION: IT TOLD THEM TO STAY PUT.
CORRECTION: IT SAID THANK YOU.
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.
BOILWATCH ALERT
HUMIDITY: 87%
RISK LEVEL: EMOTIONALLY UNSAFE
RECOMMENDATION: DO NOT PROCESS FEELINGS OUTSIDE
BOILWATCH ALERT
OVERNIGHT HUMIDITY: 84%
CONFLICT LEVELS: RISING
RECOMMENDATION: LOCK UP WHAT YOU LOVE
Tag
BOILWATCH ALERT
HUMIDITY RISING
CONFLICT LEVELS: ELEVATED
RECOMMENDATION: LOWER EXPECTATIONS
BOILWATCH ALERT
HUMIDITY: 71%
RISK LEVEL: MANAGEABLE
RECOMMENDATION: TRUST THE NUMBERS
BOILWATCH STATUS
HUMIDITY: 58%
RISK LEVEL: LOW
RECOMMENDATION: STAY FIRM
Act Three
BOILWATCH EMERGENCY
HUMIDITY SPIKE INCOMING
97% HUMIDITY IN 10 MINUTES
RECOMMENDATION: SEEK SHELTER IMMEDIATELY
BOILWATCH ALERT
HUMIDITY: 86%
RISK LEVEL: EMOTIONALLY UNSAFE
RECOMMENDATION: DO NOT FORM NEW EMOTIONAL ATTACHMENTS
BOILWATCH ALERT
HUMIDITY: 83%
CONFLICT LEVELS: ELEVATED
RECOMMENDATION: DO NOT MAKE LIFE DECISIONS TODAY
BOILWATCH ALERT
OVERNIGHT HUMIDITY: 88%
RISK LEVEL: EMOTIONALLY UNSAFE
RECOMMENDATION: GET HOME SAFE. YOU LOOK TIRED.
BOILWATCH ALERT
HUMIDITY EQUALIZATION IN PROGRESS
CONFLICT LEVELS: CRITICAL
RECOMMENDATION: SAY SOMETHING NICE
BOILWATCH EMERGENCY
HEAT WAVE INCOMING
RISK LEVEL: CRITICAL
EVERYONE WILL GET EMOTIONALLY STICKY
RECOMMENDATION: ACCEPT YOUR SOFTNESS
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
The needle touches ninety-nine, the last number on its dial. Read off a brass dial, and told in words.
After the rumble
BOILWATCH ALERT
HUMIDITY STABILIZING
RESUME NORMAL OPERATIONS
THANK YOU FOR YOUR COOPERATION
Forecasts are issued at 7:00 a.m. daily. They have never once been wrong.
Any morning from the day the wall went up to today.
BOILWATCH STATUS
NO SIGNAL
RECOMMENDATION: READ ANYWAY
ISSUED 7:00 A.M. · MACHATTAN TIME
ENVIRONMENTAL SERVICES DIVISION · INTERNAL · HUMIDITY REPORTING FORMULA
Photocopied. Some lines are blacked out. None of them are the numbers.
| Period | L0 | L1 | L2 | L3 |
|---|---|---|---|---|
| June | 10% | 45% | 30% | 15% |
| July | 5% | 35% | 35% | 25% |
| August | 5% | 30% | 35% | 30% |
| September 1–15 | 10% | 40% | 30% | 20% |
| Feast days 4–7 | 0% | 40% | 40% | 20% |
| Shoulder | 35% | 45% | 17% | 3% |
| Cold | 60% | 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.
// 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; }
BoilWatch’s five levels, as issued. The street has its own names for them.
| Level | The header | What the street calls it | Reading | Color |
|---|---|---|---|---|
| L0 | BOILWATCH STATUS | cold pot | under 70% | none |
| L1 | BOILWATCH ALERT | simmer | 70–84% | yellow |
| L2 | BOILWATCH ALERT · EMOTIONALLY UNSAFE | sticky | 85–89% | orange |
| L3 | BOILWATCH EMERGENCY | rolling boil | 90% and over | red |
| L4 | BOILWATCH EMERGENCY · CRITICAL | the melt | heat 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.