Через несколько дней начнется чемпионат Европы по футболу - второй по значимости турнир среди сборных. Как и в любом другом турнире в нем будут как интересные, так и достаточно проходные для нейтрального болельщика матчи вроде “Дания - Финляндия” или “Польша - Словакия”. Смотреть такие матчи не всегда интересно, поэтому мы придумали в офисе игру, благодаря которой всегда есть за кого болеть, что значительно добавляет интерес просмотру. Мы играли в нее во время каждого крупного футбольного турнира, и правила игры претерпели ряд изменений по сравнению с первоначальными, чтобы сделать ее более интересной.

Игра представляет собой тотализатор с элементами покера, в которую могут играть любое количество участников. Лучше всего в нее играть с коллегами, чтобы была возможность каждый день обсудить перепетии всех событий вчерашних матчей. В этой статье я опишу механику игры, и как быстро и легко организовать ее у себя в офисе среди коллег.

Механика

Суть игры заключается в том, что участники должны делать ставки на каждый матч, по результатам которого между участниками делятся очки пропорционально баллам. За угаданный счет дается 3 балла. За правильную разницу голов - 2 балла, а за угаданный результат - 1 балл.

Пример

За матч дается 100 очков, и он завершился победой хозяев со счетом 3:1. Один участник поставил на счет 3:1, еще 2 участника на счет 2:0 и 4:2, и еще 3 участника на счет 1:0, 4:1 и 5:1. В сумме они набрали 10 баллов (1 * 3 + 2 * 2 + 3 * 1). Таким образом первый участник получит 30 очков (100 / 10 * 3), а другие по 20 и 10 очков, соответственно.

Такая механика игры мотивируют участников не только угадывать результат игры, исходя из сил футбольных команд, а также угадывать на какой результат сделают ставки другие участники. Например, если в матче Венгрия - Германия 9 человек поставят на гостей, а только один на хозяев, то в случае победы Венгрии он сорвет джек-пот и получит все 100 очков, резко вырвавшись вперед. Правда на практике это иногда приводило к тому, что большинство людей ставили именно на аутсайдера, рассчитывая, что все сделают ставку на фаворита. Особенно сильно это проявлялось в конце турнира, когда участникам с небольшим количеством очков хотелось в конце вырваться вперед за счет “джек-потов”.

Также мы решили сделать, чтобы количество очков за матч возрастало к концу турнира, чтобы у аутсайдеров не терялся интерес. Так за групповой раунд мы начисляли 100 очков, за ? финала - 200, четвертьфинал - 300, полуфинал - 400, а финал стоил 500 очков. Благодаря такой схеме, самая “жара” начиналась именно к концу турнира, и иногда, даже с последнего места, за несколько последних матчей кто-то вырывался на первые места.

Одним из важных моментов для увлекательности игры является то, что участники видят ставки других участников сразу после начала матча (естественно, менять после начала матча свою ставку уже нельзя). В результате, в зависимости от текущего счета, выгодный для участника результат может меняться на совершенно другой в зависимости от текущей турнирной ситуации. Например, если кто-то поставил на победу крупную победу фаворита, и все остальные тоже, то в случае ничьи к концу игры выгоднее, чтобы игра завершилась победой аутсайдера.

Чтобы сделать игру более азартной можно организовать внутренний банк, куда все участники скинут какую-то сумму, а затем он будет поделен между победителями всего турнира. Можно, например, чтобы победитель забирал 50% всего банка, второе место - 30%, а третье - 20%. Количество призовых мест можно увеличить, если участников много, чтобы в конце турнира был интерес для тех, кто находится внизу таблицы.

Принципиальное отличие этой игры от классического тотализатора в том, что в современных букмекерских конторах игрок соревнуются против людей во всем мире, так как компании ставят друг у друга. Кроме того, статистически можно выиграть только, если человек лучше разбирается в футболе, чем средний игрок. В этой же игре люди играют против своих же коллег, и победа больше зависит не столько от знания футбола, сколько от понимания психологии своих противников. Плюс нет комиссии организатору :)

Организация турнира

Итак, что нужно, чтобы организовать у себя в офисе турнир ? Прежде всего нужна программа с веб-интерфейсом. Ее мы быстро сделали на открытой и бесплатной платформе lsFusion. Нельзя сказать, что там просто идеальный пользовательский интерфейс, но для организации игры нам было достаточно. Программу можно установить как на своем сервере (но нужно, чтобы она была доступна извне, так как матчи обычно проходят в нерабочее время), так и воспользоваться уже установленной программой на нашем выделенном сервере. Второй вариант не требует никаких дополнительных действий, поэтому я сначала расскажу именно про него.

Точка доступа в программу находится по адресу : https://euro.lsfusion.org. В ней можно одновременно проводить сколько угодно турниров. Заходить в систему можно как с настольного компьютера, так и с мобильного или планшета.

Для организации своего турнира нужен организатор. Он будет подключать к турниру других участников, а также, при необходимости, собирать и хранить банк. Организатор должен сначала зарегистрироваться по вышеуказанному адресу, нажав кнопку Зарегистрироваться :

Все поля обязательные для заполнения, но в них можно заполнять что угодно. Проверки email не проводится, но если его указать неправильно, то невозможно будет восстановить пароль. Логин должен быть глобально уникальный. В самом турнире будут показываться только логин, имя и фамилия. Поэтому желательно их заполнять таким образом, чтобы остальные участники могли определить кто есть кто (но не обязательно вводить реальные личные данные).

Таким же образом должны зарегистрироваться все остальные участники. После регистрации участника организатор может добавить его в свой турнир нажав кнопку Добавить игрока на форме Таблица :

На открывшейся форме нужно ввести логин участника, которого необходимо добавить в турнир.

В остальном ответственность организатора ничем не отличается от участников. При входе в программу открываются две закладки :

Форма "Ставки"
Форма "Ставки"

В этой форме пользователь должен в последних двух колонках (зеленых) верхней таблицы сделать ставку на счет для каждого матча. В нижней части можно посмотреть кто уже сделал ставку, а после начала матча - какую именно.

Форма "Таблица"
Форма "Таблица"

Здесь участник может посмотреть итоговую таблицу по набранным очкам в режиме реального времени.

Кроме того каждому участнику доступна форма Глобальная таблица, где рассчитывается итоговая таблица для всех участников, как будто они участвуют в одном турнире. В ней не показываются имена игроков, а только логины.

Опыт показывает, что лучше каждому участнику сделать ставки сразу на все матчи, так как их можно поменять в любой момент до начала матчи. Иначе, в случае форс-мажора, участник может забыть сделать хоть какую-то ставку, и не будет участвовать в розыгрыше очков по начавшемуся матчу.

Если по какой-то причине хочется провести турнир на своем сервере, то это можно сделать следующим образом :

Установка на собственный сервер

Для установки потребуется виртуальный сервер под Linux с 2ГБ памяти.

Сначала нужно установить платформу lsFusion, как указано в этой инструкции.

Затем создать в папке /var/lib/lsfusion файл с именем Euro2020.lsf и поместить туда следующий код :

MODULE Euro2020;

REQUIRE Authentication, Time;

CLASS Team 'Команда';
name 'Название' = DATA ISTRING[50] (Team) IN id;

FORM team 'Команда'
    OBJECTS t = Team PANEL
    PROPERTIES(t) name
    
    EDIT Team OBJECT t
;

FORM teams 'Команды'
    OBJECTS t = Team
    PROPERTIES(t) READONLY name
    PROPERTIES(t) NEWSESSION NEW, EDIT, DELETE 
    
    LIST Team OBJECT t
;

CLASS Match 'Матч';
dateTime 'Начало матча' = DATA DATETIME (Match) NONULL IN id;
started (Match m) = dateTime(m) < currentDateTime();

CONSTRAINT CHANGED(dateTime(Match m)) AND PREV(dateTime(m)) AND NOT DROPPED(m IS Match) MESSAGE 'Запрещено изменять время начала матча';

homeTeam 'Хозяева' = DATA Team (Match) NONULL;
nameHomeTeam 'Хозяева' (Match m) = name(homeTeam(m)) IN id; 

awayTeam 'Гости' = DATA Team (Match) NONULL;
nameAwayTeam 'Гости' (Match m) = name(awayTeam(m)) IN id; 

homeScore 'Результат (хозяева)' = DATA INTEGER (Match);
awayScore 'Результат (гости)' = DATA INTEGER (Match);

homeScore 'Ставка (хозяева)' = DATA INTEGER (Match, CustomUser);
awayScore 'Ставка (гости)' = DATA INTEGER (Match, CustomUser);

home 'Победа (хозяева)' (Match m) = (GROUP SUM 1 IF homeScore(m, CustomUser c) > awayScore(m, c)) IF started(m);   
tie 'Ничья' (Match m) = (GROUP SUM 1 IF homeScore(m, CustomUser c) = awayScore(m, c)) IF started(m);
away 'Победа (гости)' (Match m) = (GROUP SUM 1 IF homeScore(m, CustomUser c) < awayScore(m, c)) IF started(m);

startedHomeScore 'Ставка (хозяева)' (Match m, CustomUser c) = homeScore(m, c) IF started(m);  
startedAwayScore 'Ставка (гости)' (Match m, CustomUser c) = awayScore(m, c) IF started(m);  

hasBet 'Сделана ставка' (Match m, CustomUser c) = homeScore(m, c) AND awayScore(m, c);
countBets 'Кол-во ставок' (Match m) = GROUP SUM 1 IF hasBet(m, CustomUser c); 

CONSTRAINT DROPPED(Match m IS Match) AND PREV(countBets(m)) MESSAGE 'Запрещено удалять матч, по которому сделаны ставки';

CONSTRAINT CHANGED(homeScore(Match m, CustomUser c)) AND started(m) MESSAGE 'Запрещено изменять результат начавшегося матча';
CONSTRAINT CHANGED(awayScore(Match m, CustomUser c)) AND started(m) MESSAGE 'Запрещено изменять результат начавшегося матча';

CONSTRAINT CHANGED(homeScore(Match m)) AND NOT started(m) MESSAGE 'Запрещено изменять счет матча до начала матча';
CONSTRAINT CHANGED(awayScore(Match m)) AND NOT started(m) MESSAGE 'Запрещено изменять счет матча до начала матча';

currentHomeScore 'Ставка (хозяева)' (Match m) = homeScore(m, currentUser()); 
currentAwayScore 'Ставка (гости)' (Match m) = awayScore(m, currentUser());

changeCurrentHomeScore (Match m)  { 
    FOR CustomUser u = currentUser() DO
        INPUT =homeScore(m, u) CHANGE;
}

changeCurrentAwayScore (Match m)  { 
    FOR CustomUser u = currentUser() DO
        INPUT =awayScore(m, u) CHANGE;
}

// Расчет очков

cost 'Стоимость' = DATA INTEGER (Match);

correctScore (Match m, CustomUser c) = homeScore(m, c) == homeScore(m) AND awayScore(m, c) == awayScore(m); 
correctScore 'Правильный счет' (CustomUser c) = GROUP SUM 1 IF correctScore (Match m, c);
 
correctDelta (Match m, CustomUser c) = homeScore(m, c) - awayScore(m, c) == homeScore(m) -  awayScore(m); 
correctDelta 'Правильная разница' (CustomUser c) = GROUP SUM 1 IF correctDelta (Match m, c);

correctResult (Match m, CustomUser c) = (homeScore(m, c) - awayScore(m, c)) * (homeScore(m) -  awayScore(m)) > 0 OR 
                                      (homeScore(m, c) == awayScore(m, c) AND homeScore(m) == awayScore(m)); 
correctResult 'Правильный результат' (CustomUser c) = GROUP SUM 1 IF correctResult (Match m, c);

points 'Кол-во баллов' = CASE
                                            WHEN correctScore(Match m, CustomUser c) THEN 3
                                            WHEN correctDelta(m, c) THEN 2
                                            WHEN correctResult(m, c) THEN 1;

points 'Кол-во баллов' (CustomUser c) = GROUP SUM points(Match m, c); 
points 'Кол-во баллов' (Match m) = GROUP SUM points(m, CustomUser c);
pointsStarted 'Кол-во баллов' (Match m) = points(m) IF started(m); 

score 'Кол-во очков' (Match m, CustomUser c) = NUMERIC[16,4](cost(m) * points(m, c) / points(m));
  
score 'Кол-во очков' (CustomUser c) = GROUP SUM score(Match m, c); 


FORM match 'Матч'
    OBJECTS m = Match PANEL
    PROPERTIES(m) cost, dateTime, nameHomeTeam, nameAwayTeam, homeScore, awayScore
    EDIT Match OBJECT m
;

DESIGN match {
    GROUP(,m) {
        columns = 1;
    }
}

backgroundScore = RGB(255, 224, 224);
backgroundBet = RGB(224, 224, 255);
backgroundOwn = RGB(224, 255, 224);
FORM matches 'Ставки'
    OBJECTS m = Match
    PROPERTIES(m) READONLY cost, dateTime, nameHomeTeam, nameAwayTeam
    PROPERTIES(m) BACKGROUND backgroundScore() READONLY homeScore, awayScore, pointsStarted
    PROPERTIES(m) BACKGROUND backgroundBet() countBets, home, tie, away
    PROPERTIES BACKGROUND backgroundOwn() currentHomeScore(m) ON CHANGE changeCurrentHomeScore(m), 
                                          currentAwayScore(m) ON CHANGE changeCurrentAwayScore(m)
    PROPERTIES(m) NEWSESSION NEW, EDIT, DELETE 
    
    OBJECTS c = CustomUser GRID
    PROPERTIES(c) READONLY name[Contact]
    PROPERTIES(m, c) READONLY hasBet
    PROPERTIES(m, c) SHOWIF started(m) READONLY BACKGROUND backgroundScore() points, score 
    PROPERTIES(m, c) SHOWIF started(m) READONLY BACKGROUND backgroundBet() homeScore, awayScore
;


FORM standings 'Таблица'
    OBJECTS c = CustomUser GRID
    PROPERTIES(c) READONLY name[Contact], correctScore, correctDelta, correctResult, points, score
    PROPERTIES(c) NEWSESSION NEW, EDIT, DELETE 
    
    OBJECTS m = Match GRID
    PROPERTIES(m) READONLY cost, dateTime, nameHomeTeam, nameAwayTeam
    PROPERTIES(m) BACKGROUND backgroundScore() READONLY homeScore, awayScore, pointsStarted

    PROPERTIES(m, c) READONLY BACKGROUND backgroundScore() points, score 
    PROPERTIES(m, c) READONLY BACKGROUND backgroundBet() startedHomeScore, startedAwayScore

    ORDERS score(c) DESC 
;

NAVIGATOR {
    NEW matches;
    NEW standings;
    NEW teams;
} 

Перезапустить серверную службу при помощи команд :

systemctl stop lsfusion4-server
systemctl start lsfusion4-server

Приложение будет доступно по адресу http://<ip>:8080

Впервые мы провели игру еще во время чемпионата Европы 2012 года. Многие из наших сотрудников в итоге болели за кого-то в абсолютно всех матчах турнира, хотя в обычной ситуации не стали бы смотреть и половины. В этот раз мы решили поделиться нашим опытом с другими людьми. Если кому-то будет интересно, то тоже включайтесь в игру. В случае возникновения каких-либо проблем или вопросов всегда можно зайти в slack канал, где мы поможем по любым вопросам, или написать на почту info [собака] lsfusion.org.