Футбольний вечір із друзями завжди проходить за одним сценарієм. Спершу всі сперечаються, який буде рахунок. Потім дев'яносто хвилин, коли матч дивишся краєм ока, бо всі говорять. А тоді він закінчується — і розходитись ще нікому не хочеться.
Я зробив по грі на кожен із цих моментів. Три проєкти, три вихідні й одне правило, яке сам собі поставив: кожна гра має бути одним HTML-файлом, що відкривається подвійним кліком. Без npm install, без збірника, без сервера. Якщо файл не можна просто скинути другові — значить, не вийшло.
Ось що з цього вийшло і де я наламав дров.
1. Пресинг — квіз за форматом телешоу
Пресинг — футбольне шоу на Setanta Sports, де троє учасників змагаються протягом кількох раундів. Я переніс формат у браузер: ведучий керує всім з одного екрана, читає питання вголос і тисне кнопки, щоб нарахувати бали.
Чотири раунди:
- Турнірна таблиця. Чотири таблиці на вибір, гравці викреслюють три. Далі називають позиції в тій, що лишилась, — і балів дають рівно стільки, яке це місце. Назвати чемпіона коштує 1 бал, а того, хто фінішував восьмим, — 8. Очевидні відповіді тут найдешевші.
- Категорії по 10/20/30, із перехопленням за неправильну відповідь.
- Прихована тема. Шість питань, відповіді яких поєднує спільна таємниця. Єдина підказка — навмисно розмита назва раунду.
- Фінал із трьох загадок по п'ять підказок, із спадною ціною.
Найцікавіше тут — як звіряти відповіді. Ведучий не повинен п'ятдесят разів за вечір вирішувати, чи «достатньо близько» сказав гравець. А українське написання підкидає пасток: апостроф буває щонайменше в чотирьох варіантах Unicode, і половина компанії напише назву клубу російською.
Тому кожна відповідь спершу проходить нормалізацію:
function norm(s){
return s.toLowerCase()
.replace(/['’ʼ`´-]/g,"") // апострофи й дефіси, усі варіанти
.replace(/є/g,"е")
.replace(/і/g,"и")
.replace(/ї/g,"и") // згортаємо суто українські літери
.replace(/\s+/g," ")
.trim();
}
До того ж у кожного запису є список синонімів, а все, що довше за чотири символи, звіряється з відстанню Левенштейна 1. У підсумку «МЮ», «ман юнайтед» і «Манчестер Юнайтед» з одруківкою потрапляють в один рядок. Перш ніж цьому повірити, я прогнав вісімнадцять варіантів введення.
2. Бінго на матч — те, у що грають під час гри
Сітка 5×5 із подіями, які трапляються в трансляції. Пенальті. Червона картка. Коментатор, який знову згадав 2006-й. Сталося — тапнув. Зібрав лінію — кричиш.
Усі відкривають те саме посилання на своїх телефонах і тиснуть «нова картка»: у пулі 44 події, тож однакових карток не буває.
Ось тут правило «без збірки» стало цікавим. У таких іграшках зазвичай беруть localStorage, але я хотів, щоб картку можна було відкрити на іншому пристрої. Тому весь стан гри живе в адресі сторінки.
Картка виростає з чотирисимвольного коду через генератор випадкових чисел із сидом (mulberry32) — один і той самий код завжди дає ту саму картку:
function rng(seed){
return function(){
seed|=0; seed=seed+0x6D2B79F5|0;
var t=Math.imul(seed^seed>>>15,1|seed);
t=t+Math.imul(t^t>>>7,61|t)^t;
return ((t^t>>>14)>>>0)/4294967296;
};
}
function seedOf(code){
var s=0;
for(var i=0;i<code.length;i++) s=(s*31+code.charCodeAt(i))|0;
return s||1;
}
А 25 відмічених клітинок — це просто бітова маска, яку я кодую в base36 і кладу в хеш:
function writeHash(){
location.replace("#"+code+"-"+marks.toString(36));
}
function readHash(){
var m=/^#([A-Z0-9]{4})-([0-9a-z]+)$/.exec(location.hash||"");
if(!m) return false;
code=m[1];
marks=parseInt(m[2],36)|0;
return true;
}
Увесь стан гри — близько п'ятнадцяти символів. Його переживає і перезавантаження, і режим польоту, а посилання можна скинути собі й продовжити з ноутбука. location.replace замість присвоєння не дає історії браузера набити по запису на кожен тап.
3. Прогнози на матч — ритуал перед свистком
Перед стартовим свистком телефон іде по колу. Кожен вводить рахунок, автора першого гола, кількість жовтих карток, чи буде пенальті та червона. Між ходами стоїть екран «передай телефон», щоб ніхто не бачив чужих відповідей.
Після фінального свистка ведучий вводить, що сталося насправді, і застосунок будує таблицю.
Підрахунок — це, власне, і є весь застосунок:
function pointsFor(pr,R,fsOk){
var d={score:0,fs:0,yc:0,pen:0,red:0};
if(pr.ga===R.ga && pr.gb===R.gb) d.score=5; // точний рахунок
else if(sign(pr.ga-pr.gb)===sign(R.ga-R.gb)){
d.score=(pr.ga-pr.gb===R.ga-R.gb) ? 3 : 2; // різниця м'ячів або просто результат
}
if(fsOk) d.fs=3; // автор першого гола
var dy=Math.abs(pr.yc-R.yc);
d.yc = dy===0 ? 2 : (dy===1 ? 1 : 0); // картки: помилка на одну ще рахується
if(pr.pen===R.pen) d.pen=1;
if(pr.red===R.red) d.red=1;
d.total=d.score+d.fs+d.yc+d.pen+d.red;
return d;
}
Максимум — дванадцять балів. Бал за помилку на одну картку важить більше, ніж здається: без нього це поле майже завжди нуль, і люди просто перестають ним перейматись.
Автор першого гола — єдине поле, яке машина не розсудить сама. Хтось пише прізвище, хтось повне ім'я, хтось «не буде голів». Тому ведучий тисне кнопку, застосунок сам проставляє вердикти, звіряючи прізвища, а будь-який з них можна перевернути тапом. Нудні дев'яносто відсотків — машині, рішення — людині.
Тут теж уся сесія живе в адресі: дев'яносто хвилин — надто довго, щоб покладатися на вкладку браузера.
Три речі, на яких я обпікся
iOS сам збільшує шрифт. Сітка бінго ідеально виглядала в десктопному браузері, звуженому до ширини телефона, і розсипалась на справжньому айфоні: текст помітно більший за заданий, п'ятий стовпець поїхав за екран. Safari вмикає автозбільшення тексту на деяких розкладках. Лікується одним рядком:
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
1fr — це не «одна п'ята». У мене було repeat(5, 1fr), а елементи гріда за замовчуванням мають min-width: auto — тобто колонка не стискається вужче за своє найдовше нерозривне слово. Одне довге українське слово розпирало всю сітку:
.grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.cell {
min-width: 0;
overflow-wrap: anywhere;
font-size: clamp(7.5px, 2.25vw, 11px);
}
Мобільний GitHub не показує блок About. Я додав посилання на GitHub Pages у кожен репозиторій, переконався, що в десктопній версії воно на місці, а на телефоні не знайшов його ніде. Догортав до підвалу, перевірив усі меню — у мобільній верстці його просто немає. Тож якщо хочеш, щоб людина з телефона знайшла твоє демо, клади посилання в README: це єдиний блок, який мобільна версія показує завжди.
Чи зробив би я так само вдруге
Так, з однією поправкою. Обмеження «один файл» дало головне — ці проєкти вдалося довести до кінця. Жодного дня не пішло на налаштування інструментів, і кожна гра від ідеї доходила до того, у що ми справді грали, за одні вихідні. А тримати стан в адресі сторінки виявилося не милицею, а цілком робочим підходом для маленьких інструментів.
Поправка ось яка: усе їде на клієнт, тож відповіді квізу лежать просто в коді сторінки. Для гри, якою керує ведучий, це не біда — я запускаю її зі свого екрана й посилання нікому не даю. Але це і є стеля такого підходу: щойно потрібні секрети — потрібен сервер.
Усі три лежать тут — форкай і підставляй свої питання:
Квіз — неофіційний фанатський проєкт, не пов'язаний із Setanta Sports чи самим шоу.
Top comments (0)