Русский
Русский
English
Статистика
Реклама

Dagaz

DagazServer Как всё устроено

23.10.2020 10:09:29 | Автор: admin
По всей видимости, я не самый правильный фронтэдщик. Большую часть сознательной жизни я занимаюсь базами данных и немного бакэндом. Любовь к настольным играм вот что заставило меня связаться с web-разработкой. Новый, совершенно незнакомый мне язык программирования JavaScript, я изучал по ходу дела. Допускаю, что многое из того что я делал, способно ужаснуть опытных web-разработчиков, но я стараюсь стать лучше. Этот текст не для тех, кто привык во всём следовать раз и навсегда написанным инструкциям. Но если вы любите экспериментировать, докапываться до каждой мелочи, открывать новое для себя, добро пожаловать под сень моего леса.



С чего всё началось
Фаза активного увлечения настольными играми началась для меня со знакомства с Zillions of Games. То что на компьютере можно играть в игры новостью не было. Оригинальность этого проекта заключалась в использовании ZRF Lisp-оподобного языка, предназначенного для быстрого описания разнообразных игр. К сожалению, я быстро осознал, что хотя простые игры делаются на ZRF просто, игры чуть сложнее быстро выливаются в тысячи строк малопонятного кода. Для примера, в ZRF нет самой обычной арифметики. В результате, целочисленные значения приходится собирать из битовых флагов.

И выглядит это как-то так
(define not-0?  (or (flag? $1-08)      (flag? $1-04)      (flag? $1-02)      (flag? $1-01)  ))(define inc   (if (flag? $1-01)       (set-flag $1-01 false)       (if (flag? $1-02)           (set-flag $1-02 false)           (if (flag? $1-04)               (set-flag $1-04 false)               (if (flag? $1-08)                   (set-flag $1-08 false)                else                   (set-flag $1-08 true)               )            else               (set-flag $1-04 true)           )        else           (set-flag $1-02 true)       )    else       (set-flag $1-01 true)   ))(define dec   (if (not-flag? $1-01)       (set-flag $1-01 true)       (if (not-flag? $1-02)           (set-flag $1-02 true)           (if (not-flag? $1-04)               (set-flag $1-04 true)               (if (not-flag? $1-08)                   (set-flag $1-08 true)                else                   (set-flag $1-08 false)               )            else               (set-flag $1-04 false)           )        else           (set-flag $1-02 false)       )    else       (set-flag $1-01 false)   ))(define sum   (while (not-0? $2)       (inc $1)       (dec $2)   ))


В Axiom Development Kit библиотеке разработанной Грегом Шмидтом, складывать числа было можно, но в качестве метаязыка использовался диалект Forth-а, что не делало программы более понятными. Само решение представляло собой, в некотором роде, хак использующий API, предназначенное для подключения к Zillions ботов. Кроме того, Axiom никак не решала фатальный недостаток Zillions игры продолжали запускаться только под Windows и только на платной платформе с закрытым исходным кодом.

Знакомство с проектом Jocly подсказало мне возможное решение этой проблемы. Действительно, игра написанная на JavaScript могла запускаться в любом современном браузере, в том числе на мобильных платформах. С этого момента началась работа над Dagaz. Что-то я подсмотрел в Jocly, а что-то у Зейна Фишера. Также как Zillions, Dagaz использует ZRF (это позволяет разрабатывать прототипы новых игр очень быстро), но не напрямую (что оказалось невозможным из-за недостаточной производительности), а после компиляции специальной утилитой.

Вот это
(define checker-shift (   $1 (verify empty?)   (if (in-zone? promotion)      (add King)    else      add   )))(define checker-jump (   $1 (verify enemy?)    capture    $1 (verify empty?)   (if (in-zone? promotion)       (add-partial King continue-type)    else       (add-partial jump-type)   )))(define king-shift (   $1 (while empty?       add $1   )))(define king-jump (   $1 (while empty?  $1)   (verify enemy?)   $1 (while empty?       mark       (while empty?           (opposite $1)       )        capture       back       (add-partial continue-type) $1   )))(define king-continue (   $1 (while empty?        $1 (verify not-last-from?)   )   (verify enemy?)    $1 (while empty?       mark       (while empty?           (opposite $1)       )        capture       back       (add-partial continue-type) $1   )))(game  (title "Russian Checkers")  (players White Black)  (turn-order White Black)  (move-priorities jump-type normal-type)  (board     (image "images/8x8.bmp")     (grid         (start-rectangle 2 2 52 52)         (dimensions           ("a/b/c/d/e/f/g/h" (50 0)) ; files           ("8/7/6/5/4/3/2/1" (0 50)) ; ranks         )         (directions (ne 1 -1) (nw -1 -1) (se 1 1) (sw -1 1))     )     (symmetry Black (nw se) (se nw) (ne sw) (sw ne))     (zone (name promotion) (players White)           (positions b8 d8 f8 h8)     )     (zone (name promotion) (players Black)           (positions a1 c1 e1 g1)     )  )  (piece     (name Man)     (image White "images/wman.bmp"            Black "images/bman.bmp")     (moves         (move-type jump-type)         (checker-jump nw) (checker-jump ne) (checker-jump sw) (checker-jump se)         (move-type normal-type)         (checker-shift nw) (checker-shift ne)     )  )  (piece     (name King)     (image White "images/wdamone.bmp"            Black "images/bdamone.bmp")     (moves         (move-type jump-type)         (king-jump nw) (king-jump ne) (king-jump sw) (king-jump se)         (move-type continue-type)         (king-continue nw) (king-continue ne)          (king-continue sw) (king-continue se)         (move-type normal-type)         (king-shift nw) (king-shift ne) (king-shift sw) (king-shift se)     )  )  (board-setup    (White (Man a1 c1 e1 g1  b2 d2 f2 h2  a3 c3 e3 g3) )    (Black (Man b8 d8 f8 h8  a7 c7 e7 g7  b6 d6 f6 h6) )  ))

превращается в это
ZRF = {    JUMP:          0,    IF:            1,    FORK:          2,    FUNCTION:      3,    IN_ZONE:       4,    FLAG:          5,    SET_FLAG:      6,    POS_FLAG:      7,    SET_POS_FLAG:  8,    ATTR:          9,    SET_ATTR:      10,    PROMOTE:       11,    MODE:          12,    ON_BOARD_DIR:  13,    ON_BOARD_POS:  14,    PARAM:         15,    LITERAL:       16,    VERIFY:        20};Dagaz.Model.BuildDesign = function(design) {    design.checkVersion("z2j", "2");    design.checkVersion("animate-captures", "false");    design.checkVersion("smart-moves", "true");    design.checkVersion("show-hints", "false");    design.checkVersion("show-blink", "true");    design.checkVersion("deferred-captures", "true");    design.checkVersion("advisor-wait", "5");    design.addDirection("ne");    design.addDirection("se");    design.addDirection("sw");    design.addDirection("nw");    design.addPlayer("White", [2, 3, 0, 1]);    design.addPlayer("Black", [2, 3, 0, 1]);    design.addPosition("a8", [0, 9, 0, 0]);    design.addPosition("b8", [0, 9, 7, 0]);    design.addPosition("c8", [0, 9, 7, 0]);    design.addPosition("d8", [0, 9, 7, 0]);    design.addPosition("e8", [0, 9, 7, 0]);    design.addPosition("f8", [0, 9, 7, 0]);    design.addPosition("g8", [0, 9, 7, 0]);    design.addPosition("h8", [0, 0, 7, 0]);    design.addPosition("a7", [-7, 9, 0, 0]);    design.addPosition("b7", [-7, 9, 7, -9]);    design.addPosition("c7", [-7, 9, 7, -9]);    design.addPosition("d7", [-7, 9, 7, -9]);    design.addPosition("e7", [-7, 9, 7, -9]);    design.addPosition("f7", [-7, 9, 7, -9]);    design.addPosition("g7", [-7, 9, 7, -9]);    design.addPosition("h7", [0, 0, 7, -9]);    design.addPosition("a6", [-7, 9, 0, 0]);    design.addPosition("b6", [-7, 9, 7, -9]);    design.addPosition("c6", [-7, 9, 7, -9]);    design.addPosition("d6", [-7, 9, 7, -9]);    design.addPosition("e6", [-7, 9, 7, -9]);    design.addPosition("f6", [-7, 9, 7, -9]);    design.addPosition("g6", [-7, 9, 7, -9]);    design.addPosition("h6", [0, 0, 7, -9]);    design.addPosition("a5", [-7, 9, 0, 0]);    design.addPosition("b5", [-7, 9, 7, -9]);    design.addPosition("c5", [-7, 9, 7, -9]);    design.addPosition("d5", [-7, 9, 7, -9]);    design.addPosition("e5", [-7, 9, 7, -9]);    design.addPosition("f5", [-7, 9, 7, -9]);    design.addPosition("g5", [-7, 9, 7, -9]);    design.addPosition("h5", [0, 0, 7, -9]);    design.addPosition("a4", [-7, 9, 0, 0]);    design.addPosition("b4", [-7, 9, 7, -9]);    design.addPosition("c4", [-7, 9, 7, -9]);    design.addPosition("d4", [-7, 9, 7, -9]);    design.addPosition("e4", [-7, 9, 7, -9]);    design.addPosition("f4", [-7, 9, 7, -9]);    design.addPosition("g4", [-7, 9, 7, -9]);    design.addPosition("h4", [0, 0, 7, -9]);    design.addPosition("a3", [-7, 9, 0, 0]);    design.addPosition("b3", [-7, 9, 7, -9]);    design.addPosition("c3", [-7, 9, 7, -9]);    design.addPosition("d3", [-7, 9, 7, -9]);    design.addPosition("e3", [-7, 9, 7, -9]);    design.addPosition("f3", [-7, 9, 7, -9]);    design.addPosition("g3", [-7, 9, 7, -9]);    design.addPosition("h3", [0, 0, 7, -9]);    design.addPosition("a2", [-7, 9, 0, 0]);    design.addPosition("b2", [-7, 9, 7, -9]);    design.addPosition("c2", [-7, 9, 7, -9]);    design.addPosition("d2", [-7, 9, 7, -9]);    design.addPosition("e2", [-7, 9, 7, -9]);    design.addPosition("f2", [-7, 9, 7, -9]);    design.addPosition("g2", [-7, 9, 7, -9]);    design.addPosition("h2", [0, 0, 7, -9]);    design.addPosition("a1", [-7, 0, 0, 0]);    design.addPosition("b1", [-7, 0, 0, -9]);    design.addPosition("c1", [-7, 0, 0, -9]);    design.addPosition("d1", [-7, 0, 0, -9]);    design.addPosition("e1", [-7, 0, 0, -9]);    design.addPosition("f1", [-7, 0, 0, -9]);    design.addPosition("g1", [-7, 0, 0, -9]);    design.addPosition("h1", [0, 0, 0, -9]);    design.addZone("promotion", 1, [1, 3, 5, 7]);    design.addZone("promotion", 2, [56, 58, 60, 62]);    design.addZone("best", 1, [26, 21]);    design.addZone("best", 2, [37, 42]);    design.addCommand(0, ZRF.FUNCTION,24);// from    design.addCommand(0, ZRF.PARAM,0);// $1    design.addCommand(0, ZRF.FUNCTION,22);// navigate    design.addCommand(0, ZRF.FUNCTION,2);// enemy?    design.addCommand(0, ZRF.FUNCTION,20);// verify    design.addCommand(0, ZRF.FUNCTION,26);// capture    design.addCommand(0, ZRF.PARAM,1);// $2    design.addCommand(0, ZRF.FUNCTION,22);// navigate    design.addCommand(0, ZRF.FUNCTION,1);// empty?    design.addCommand(0, ZRF.FUNCTION,20);// verify    design.addCommand(0, ZRF.IN_ZONE,0);// promotion    design.addCommand(0, ZRF.FUNCTION,0);// not    design.addCommand(0, ZRF.IF,5);    design.addCommand(0, ZRF.PROMOTE,1);// King    design.addCommand(0, ZRF.MODE,2);// continue-type    design.addCommand(0, ZRF.FUNCTION,25);// to    design.addCommand(0, ZRF.JUMP,3);    design.addCommand(0, ZRF.MODE,0);// jump-type    design.addCommand(0, ZRF.FUNCTION,25);// to    design.addCommand(0, ZRF.FUNCTION,28);// end    design.addCommand(1, ZRF.FUNCTION,24);// from    design.addCommand(1, ZRF.PARAM,0);// $1    design.addCommand(1, ZRF.FUNCTION,22);// navigate    design.addCommand(1, ZRF.FUNCTION,1);// empty?    design.addCommand(1, ZRF.FUNCTION,20);// verify    design.addCommand(1, ZRF.IN_ZONE,0);// promotion    design.addCommand(1, ZRF.FUNCTION,0);// not    design.addCommand(1, ZRF.IF,4);    design.addCommand(1, ZRF.PROMOTE,1);// King    design.addCommand(1, ZRF.FUNCTION,25);// to    design.addCommand(1, ZRF.JUMP,2);    design.addCommand(1, ZRF.FUNCTION,25);// to    design.addCommand(1, ZRF.FUNCTION,28);// end    design.addCommand(2, ZRF.FUNCTION,24);// from    design.addCommand(2, ZRF.PARAM,0);// $1    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.FUNCTION,1);// empty?    design.addCommand(2, ZRF.FUNCTION,0);// not    design.addCommand(2, ZRF.IF,4);    design.addCommand(2, ZRF.PARAM,1);// $2    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.JUMP,-5);    design.addCommand(2, ZRF.FUNCTION,2);// enemy?    design.addCommand(2, ZRF.FUNCTION,20);// verify    design.addCommand(2, ZRF.PARAM,2);// $3    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.FUNCTION,1);// empty?    design.addCommand(2, ZRF.FUNCTION,0);// not    design.addCommand(2, ZRF.IF,18);    design.addCommand(2, ZRF.FUNCTION,6);// mark    design.addCommand(2, ZRF.FUNCTION,1);// empty?    design.addCommand(2, ZRF.FUNCTION,0);// not    design.addCommand(2, ZRF.IF,5);    design.addCommand(2, ZRF.PARAM,3);// $4    design.addCommand(2, ZRF.FUNCTION,23);// opposite    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.JUMP,-6);    design.addCommand(2, ZRF.FUNCTION,26);// capture    design.addCommand(2, ZRF.FUNCTION,7);// back    design.addCommand(2, ZRF.FORK,4);    design.addCommand(2, ZRF.MODE,2);// continue-type    design.addCommand(2, ZRF.FUNCTION,25);// to    design.addCommand(2, ZRF.FUNCTION,28);// end    design.addCommand(2, ZRF.PARAM,4);// $5    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.JUMP,-19);    design.addCommand(2, ZRF.FUNCTION,28);// end    design.addCommand(3, ZRF.FUNCTION,24);// from    design.addCommand(3, ZRF.PARAM,0);// $1    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.FUNCTION,1);// empty?    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.IF,7);    design.addCommand(3, ZRF.PARAM,1);// $2    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.FUNCTION,4);// last-from?    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.FUNCTION,20);// verify    design.addCommand(3, ZRF.JUMP,-8);    design.addCommand(3, ZRF.FUNCTION,2);// enemy?    design.addCommand(3, ZRF.FUNCTION,20);// verify    design.addCommand(3, ZRF.PARAM,2);// $3    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.FUNCTION,1);// empty?    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.IF,18);    design.addCommand(3, ZRF.FUNCTION,6);// mark    design.addCommand(3, ZRF.FUNCTION,1);// empty?    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.IF,5);    design.addCommand(3, ZRF.PARAM,3);// $4    design.addCommand(3, ZRF.FUNCTION,23);// opposite    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.JUMP,-6);    design.addCommand(3, ZRF.FUNCTION,26);// capture    design.addCommand(3, ZRF.FUNCTION,7);// back    design.addCommand(3, ZRF.FORK,4);    design.addCommand(3, ZRF.MODE,2);// continue-type    design.addCommand(3, ZRF.FUNCTION,25);// to    design.addCommand(3, ZRF.FUNCTION,28);// end    design.addCommand(3, ZRF.PARAM,4);// $5    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.JUMP,-19);    design.addCommand(3, ZRF.FUNCTION,28);// end    design.addCommand(4, ZRF.FUNCTION,24);// from    design.addCommand(4, ZRF.PARAM,0);// $1    design.addCommand(4, ZRF.FUNCTION,22);// navigate    design.addCommand(4, ZRF.FUNCTION,1);// empty?    design.addCommand(4, ZRF.FUNCTION,0);// not    design.addCommand(4, ZRF.IF,7);    design.addCommand(4, ZRF.FORK,3);    design.addCommand(4, ZRF.FUNCTION,25);// to    design.addCommand(4, ZRF.FUNCTION,28);// end    design.addCommand(4, ZRF.PARAM,1);// $2    design.addCommand(4, ZRF.FUNCTION,22);// navigate    design.addCommand(4, ZRF.JUMP,-8);    design.addCommand(4, ZRF.FUNCTION,28);// end    design.addPriority(0);// jump-type    design.addPriority(1);// normal-type    design.addPiece("Man", 0, 20);    design.addMove(0, 0, [3, 3], 0);    design.addMove(0, 0, [0, 0], 0);    design.addMove(0, 0, [2, 2], 0);    design.addMove(0, 0, [1, 1], 0);    design.addMove(0, 1, [3], 1);    design.addMove(0, 1, [0], 1);    design.addPiece("King", 1, 100);    design.addMove(1, 2, [3, 3, 3, 3, 3], 0, 10);    design.addMove(1, 2, [0, 0, 0, 0, 0], 0, 10);    design.addMove(1, 2, [2, 2, 2, 2, 2], 0, 10);    design.addMove(1, 2, [1, 1, 1, 1, 1], 0, 10);    design.addMove(1, 3, [3, 3, 3, 3, 3], 2, 10);    design.addMove(1, 3, [0, 0, 0, 0, 0], 2, 10);    design.addMove(1, 3, [2, 2, 2, 2, 2], 2, 10);    design.addMove(1, 3, [1, 1, 1, 1, 1], 2, 10);    design.addMove(1, 4, [3, 3], 1, 10);    design.addMove(1, 4, [0, 0], 1, 10);    design.addMove(1, 4, [2, 2], 1, 10);    design.addMove(1, 4, [1, 1], 1, 10);    design.setup("White", "Man", 56);    design.setup("White", "Man", 58);    design.setup("White", "Man", 60);    design.setup("White", "Man", 62);    design.setup("White", "Man", 49);    design.setup("White", "Man", 51);    design.setup("White", "Man", 53);    design.setup("White", "Man", 55);    design.setup("White", "Man", 40);    design.setup("White", "Man", 42);    design.setup("White", "Man", 44);    design.setup("White", "Man", 46);    design.setup("Black", "Man", 1);    design.setup("Black", "Man", 3);    design.setup("Black", "Man", 5);    design.setup("Black", "Man", 7);    design.setup("Black", "Man", 8);    design.setup("Black", "Man", 10);    design.setup("Black", "Man", 12);    design.setup("Black", "Man", 14);    design.setup("Black", "Man", 17);    design.setup("Black", "Man", 19);    design.setup("Black", "Man", 21);    design.setup("Black", "Man", 23);}Dagaz.View.configure = function(view) {    view.defBoard("Board");    view.defPiece("WhiteMan", "White Man");    view.defPiece("BlackMan", "Black Man");    view.defPiece("WhiteKing", "White King");    view.defPiece("BlackKing", "Black King");     view.defPosition("a8", 2, 2, 50, 50);    view.defPosition("b8", 52, 2, 50, 50);    view.defPosition("c8", 102, 2, 50, 50);    view.defPosition("d8", 152, 2, 50, 50);    view.defPosition("e8", 202, 2, 50, 50);    view.defPosition("f8", 252, 2, 50, 50);    view.defPosition("g8", 302, 2, 50, 50);    view.defPosition("h8", 352, 2, 50, 50);    view.defPosition("a7", 2, 52, 50, 50);    view.defPosition("b7", 52, 52, 50, 50);    view.defPosition("c7", 102, 52, 50, 50);    view.defPosition("d7", 152, 52, 50, 50);    view.defPosition("e7", 202, 52, 50, 50);    view.defPosition("f7", 252, 52, 50, 50);    view.defPosition("g7", 302, 52, 50, 50);    view.defPosition("h7", 352, 52, 50, 50);    view.defPosition("a6", 2, 102, 50, 50);    view.defPosition("b6", 52, 102, 50, 50);    view.defPosition("c6", 102, 102, 50, 50);    view.defPosition("d6", 152, 102, 50, 50);    view.defPosition("e6", 202, 102, 50, 50);    view.defPosition("f6", 252, 102, 50, 50);    view.defPosition("g6", 302, 102, 50, 50);    view.defPosition("h6", 352, 102, 50, 50);    view.defPosition("a5", 2, 152, 50, 50);    view.defPosition("b5", 52, 152, 50, 50);    view.defPosition("c5", 102, 152, 50, 50);    view.defPosition("d5", 152, 152, 50, 50);    view.defPosition("e5", 202, 152, 50, 50);    view.defPosition("f5", 252, 152, 50, 50);    view.defPosition("g5", 302, 152, 50, 50);    view.defPosition("h5", 352, 152, 50, 50);    view.defPosition("a4", 2, 202, 50, 50);    view.defPosition("b4", 52, 202, 50, 50);    view.defPosition("c4", 102, 202, 50, 50);    view.defPosition("d4", 152, 202, 50, 50);    view.defPosition("e4", 202, 202, 50, 50);    view.defPosition("f4", 252, 202, 50, 50);    view.defPosition("g4", 302, 202, 50, 50);    view.defPosition("h4", 352, 202, 50, 50);    view.defPosition("a3", 2, 252, 50, 50);    view.defPosition("b3", 52, 252, 50, 50);    view.defPosition("c3", 102, 252, 50, 50);    view.defPosition("d3", 152, 252, 50, 50);    view.defPosition("e3", 202, 252, 50, 50);    view.defPosition("f3", 252, 252, 50, 50);    view.defPosition("g3", 302, 252, 50, 50);    view.defPosition("h3", 352, 252, 50, 50);    view.defPosition("a2", 2, 302, 50, 50);    view.defPosition("b2", 52, 302, 50, 50);    view.defPosition("c2", 102, 302, 50, 50);    view.defPosition("d2", 152, 302, 50, 50);    view.defPosition("e2", 202, 302, 50, 50);    view.defPosition("f2", 252, 302, 50, 50);    view.defPosition("g2", 302, 302, 50, 50);    view.defPosition("h2", 352, 302, 50, 50);    view.defPosition("a1", 2, 352, 50, 50);    view.defPosition("b1", 52, 352, 50, 50);    view.defPosition("c1", 102, 352, 50, 50);    view.defPosition("d1", 152, 352, 50, 50);    view.defPosition("e1", 202, 352, 50, 50);    view.defPosition("f1", 252, 352, 50, 50);    view.defPosition("g1", 302, 352, 50, 50);    view.defPosition("h1", 352, 352, 50, 50);}


Да, читать это труднее, зато работает гораздо быстрее. Например, навигация перемещение по доске, сводится к обычному арифметическому сложению. Есть ещё один момент. Некоторые правила, относящиеся не к перемещению отдельных фигур, а к игре в целом, невероятно трудно выразить на языке ZRF. Я и не пытаюсь. Для этого в Dagaz есть расширения. Например, в "Русских шашках", как и в большинстве других шашечных игр с летающими дамками, действует правило "Турецкого удара" взятые фигуры убираются с доски только по завершении хода.

Вот как это делается при помощи расширения
(function() {Dagaz.Model.deferredStrike = true;var checkVersion = Dagaz.Model.checkVersion;Dagaz.Model.checkVersion = function(design, name, value) {  if (name != "deferred-captures") {     checkVersion(design, name, value);  }}var CheckInvariants = Dagaz.Model.CheckInvariants;Dagaz.Model.CheckInvariants = function(board) {  _.chain(board.moves)   .filter(function(move) {        return move.actions.length > 0;    })   .each(function(move) {        var mx = _.chain(move.actions)         .map(function(action) {              return action[3];          }).max().value();          var actions = [];          _.each(move.actions, function(action) {              var pn = action[3];              if ((action[0] !== null) && (action[1] === null)) {                  pn = mx;              }              actions.push([ action[0], action[1], action[2], pn ]);          });          move.actions = actions;    });  CheckInvariants(board);}})();


После того как список ходов сгенерирован базовой логикой, закодированной ZRF-описанием игры, он передаётся подключенным расширениям, способным выполнять сложные проверки, запрещать отдельные ходы, обогащать их дополнительными действиями или изменять порядок выполняемых действий, как в случае с deferred-captures, упомянутым выше. Такой гибридный подход серьёзным образом расширяет описательные возможности системы, без необходимости усложнения базового метаязыка ZRF.

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

Вот как выглядит описание игры в новом стиле
ZRF = {    JUMP:          0,    IF:            1,    FORK:          2,    FUNCTION:      3,    IN_ZONE:       4,    FLAG:          5,    SET_FLAG:      6,    POS_FLAG:      7,    SET_POS_FLAG:  8,    ATTR:          9,    SET_ATTR:      10,    PROMOTE:       11,    MODE:          12,    ON_BOARD_DIR:  13,    ON_BOARD_POS:  14,    PARAM:         15,    LITERAL:       16,    VERIFY:        20};Dagaz.Model.BuildDesign = function(design) {    design.checkVersion("z2j", "2");    design.checkVersion("smart-moves", "false");    design.checkVersion("show-blink", "false");    design.checkVersion("show-hints", "false");    design.checkVersion("show-captures", "false");    design.checkVersion("dtc-extension", "extended");    var g = design.addGrid();    g.addScale("A/B/C/D/E/F/G/H"); g.addScale("8/7/6/5/4/3/2/1");    g.addDirection("n",[ 0, -1]); g.addDirection("nw",[-1, -1]);    g.addDirection("e",[ 1,  0]); g.addDirection("ne",[ 1, -1]);    g.addDirection("w",[-1,  0]); g.addDirection("sw",[-1,  1]);    g.addDirection("s",[ 0,  1]); g.addDirection("se",[ 1,  1]);    design.addPlayer("White", [6, 7, 4, 5, 2, 3, 0, 1]);    design.addPlayer("Black", [6, 5, 2, 7, 4, 1, 0, 3]);    g.addPositions();    design.addPosition(["RWP", "RWN", "RWB", "RWR", "RWQ", "RWK", "RBP", "RBN", "RBB", "RBR", "RBQ", "RBK", "UP", "DN"]);    design.addZone("last-rank",  1, [0, 1, 2, 3, 4, 5, 6, 7]);    design.addZone("last-rank",  2, [56, 57, 58, 59, 60, 61, 62, 63]);    design.addZone("third-rank", 1, [40, 41, 42, 43, 44, 45, 46, 47]);    design.addZone("third-rank", 2, [16, 17, 18, 19, 20, 21, 22, 23]);    design.addCommand(0, ZRF.FUNCTION,24);// from    design.addCommand(0, ZRF.PARAM,0);// $1    design.addCommand(0, ZRF.FUNCTION,22);// navigate    design.addCommand(0, ZRF.IN_ZONE,1);// third-rank    design.addCommand(0, ZRF.FUNCTION,0);// not    design.addCommand(0, ZRF.IF,11);    design.addCommand(0, ZRF.FUNCTION,2);// enemy?    design.addCommand(0, ZRF.FUNCTION,0);// not    design.addCommand(0, ZRF.FUNCTION,20);// verify    design.addCommand(0, ZRF.FORK,3);    design.addCommand(0, ZRF.FUNCTION,25);// to    design.addCommand(0, ZRF.FUNCTION,28);// end    design.addCommand(0, ZRF.FUNCTION,1);// empty?    design.addCommand(0, ZRF.FUNCTION,20);// verify    design.addCommand(0, ZRF.PARAM,1);// $2    design.addCommand(0, ZRF.FUNCTION,22);// navigate    design.addCommand(0, ZRF.FUNCTION,2);// enemy?    design.addCommand(0, ZRF.FUNCTION,0);// not    design.addCommand(0, ZRF.FUNCTION,20);// verify    design.addCommand(0, ZRF.FUNCTION,25);// to    design.addCommand(0, ZRF.FUNCTION,28);// end    design.addCommand(1, ZRF.FUNCTION,24);// from    design.addCommand(1, ZRF.PARAM,0);// $1    design.addCommand(1, ZRF.FUNCTION,22);// navigate    design.addCommand(1, ZRF.FUNCTION,2);// enemy?    design.addCommand(1, ZRF.FUNCTION,20);// verify    design.addCommand(1, ZRF.FUNCTION,25);// to    design.addCommand(1, ZRF.FUNCTION,28);// end    design.addCommand(2, ZRF.FUNCTION,24);// from    design.addCommand(2, ZRF.PARAM,0);// $1    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.FUNCTION,2);// enemy?    design.addCommand(2, ZRF.FUNCTION,20);// verify    design.addCommand(2, ZRF.FUNCTION,5);// last-to?    design.addCommand(2, ZRF.FUNCTION,20);// verify    design.addCommand(2, ZRF.LITERAL,0);// Pawn    design.addCommand(2, ZRF.FUNCTION,10);// piece?    design.addCommand(2, ZRF.FUNCTION,20);// verify    design.addCommand(2, ZRF.FUNCTION,26);// capture    design.addCommand(2, ZRF.PARAM,1);// $2    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.FUNCTION,6);// mark    design.addCommand(2, ZRF.PARAM,2);// $3    design.addCommand(2, ZRF.FUNCTION,22);// navigate    design.addCommand(2, ZRF.FUNCTION,4);// last-from?    design.addCommand(2, ZRF.FUNCTION,20);// verify    design.addCommand(2, ZRF.FUNCTION,7);// back    design.addCommand(2, ZRF.FUNCTION,25);// to    design.addCommand(2, ZRF.FUNCTION,28);// end    design.addCommand(3, ZRF.FUNCTION,24);// from    design.addCommand(3, ZRF.IN_ZONE,0);// last-rank    design.addCommand(3, ZRF.FUNCTION,20);// verify    design.addCommand(3, ZRF.PARAM,0);// $1    design.addCommand(3, ZRF.FUNCTION,21);// position    design.addCommand(3, ZRF.ON_BOARD_DIR,7);// name    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.IF,9);    design.addCommand(3, ZRF.FUNCTION,1);// empty?    design.addCommand(3, ZRF.IF,4);    design.addCommand(3, ZRF.FORK,3);    design.addCommand(3, ZRF.FUNCTION,25);// to    design.addCommand(3, ZRF.FUNCTION,28);// end    design.addCommand(3, ZRF.PARAM,1);// $2    design.addCommand(3, ZRF.FUNCTION,22);// navigate    design.addCommand(3, ZRF.JUMP,-10);    design.addCommand(3, ZRF.FUNCTION,1);// empty?    design.addCommand(3, ZRF.FUNCTION,0);// not    design.addCommand(3, ZRF.FUNCTION,20);// verify    design.addCommand(3, ZRF.FUNCTION,25);// to    design.addCommand(3, ZRF.FUNCTION,28);// end    design.addCommand(4, ZRF.FUNCTION,24);// from    design.addCommand(4, ZRF.PARAM,0);// $1    design.addCommand(4, ZRF.FUNCTION,22);// navigate    design.addCommand(4, ZRF.PARAM,1);// $2    design.addCommand(4, ZRF.FUNCTION,22);// navigate    design.addCommand(4, ZRF.FUNCTION,25);// to    design.addCommand(4, ZRF.FUNCTION,28);// end    design.addCommand(5, ZRF.FUNCTION,24);// from    design.addCommand(5, ZRF.PARAM,0);// $1    design.addCommand(5, ZRF.FUNCTION,22);// navigate    design.addCommand(5, ZRF.FUNCTION,1);// empty?    design.addCommand(5, ZRF.FUNCTION,0);// not    design.addCommand(5, ZRF.IF,7);    design.addCommand(5, ZRF.FORK,3);    design.addCommand(5, ZRF.FUNCTION,25);// to    design.addCommand(5, ZRF.FUNCTION,28);// end    design.addCommand(5, ZRF.PARAM,1);// $2    design.addCommand(5, ZRF.FUNCTION,22);// navigate    design.addCommand(5, ZRF.JUMP,-8);    design.addCommand(5, ZRF.FUNCTION,25);// to    design.addCommand(5, ZRF.FUNCTION,28);// end    design.addCommand(6, ZRF.FUNCTION,24);// from    design.addCommand(6, ZRF.PARAM,0);// $1    design.addCommand(6, ZRF.FUNCTION,22);// navigate    design.addCommand(6, ZRF.FUNCTION,25);// to    design.addCommand(6, ZRF.FUNCTION,28);// end    design.addCommand(7, ZRF.FUNCTION,24);// from    design.addCommand(7, ZRF.PARAM,0);// $1    design.addCommand(7, ZRF.FUNCTION,22);// navigate    design.addCommand(7, ZRF.FUNCTION,1);// empty?    design.addCommand(7, ZRF.FUNCTION,20);// verify    design.addCommand(7, ZRF.PARAM,1);// $2    design.addCommand(7, ZRF.FUNCTION,22);// navigate    design.addCommand(7, ZRF.FUNCTION,1);// empty?    design.addCommand(7, ZRF.FUNCTION,20);// verify    design.addCommand(7, ZRF.FUNCTION,25);// to    design.addCommand(7, ZRF.PARAM,2);// $3    design.addCommand(7, ZRF.FUNCTION,22);// navigate    design.addCommand(7, ZRF.FUNCTION,3);// friend?    design.addCommand(7, ZRF.FUNCTION,20);// verify    design.addCommand(7, ZRF.LITERAL,3);// Rook    design.addCommand(7, ZRF.FUNCTION,10);// piece?    design.addCommand(7, ZRF.FUNCTION,20);// verify    design.addCommand(7, ZRF.FUNCTION,24);// from    design.addCommand(7, ZRF.PARAM,3);// $4    design.addCommand(7, ZRF.FUNCTION,22);// navigate    design.addCommand(7, ZRF.PARAM,4);// $5    design.addCommand(7, ZRF.FUNCTION,22);// navigate    design.addCommand(7, ZRF.FUNCTION,25);// to    design.addCommand(7, ZRF.FUNCTION,28);// end    design.addCommand(8, ZRF.FUNCTION,24);// from    design.addCommand(8, ZRF.PARAM,0);// $1    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.FUNCTION,1);// empty?    design.addCommand(8, ZRF.FUNCTION,20);// verify    design.addCommand(8, ZRF.PARAM,1);// $2    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.FUNCTION,1);// empty?    design.addCommand(8, ZRF.FUNCTION,20);// verify    design.addCommand(8, ZRF.FUNCTION,25);// to    design.addCommand(8, ZRF.PARAM,2);// $3    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.FUNCTION,1);// empty?    design.addCommand(8, ZRF.FUNCTION,20);// verify    design.addCommand(8, ZRF.PARAM,3);// $4    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.FUNCTION,3);// friend?    design.addCommand(8, ZRF.FUNCTION,20);// verify    design.addCommand(8, ZRF.LITERAL,3);// Rook    design.addCommand(8, ZRF.FUNCTION,10);// piece?    design.addCommand(8, ZRF.FUNCTION,20);// verify    design.addCommand(8, ZRF.FUNCTION,24);// from    design.addCommand(8, ZRF.PARAM,4);// $5    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.PARAM,5);// $6    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.PARAM,6);// $7    design.addCommand(8, ZRF.FUNCTION,22);// navigate    design.addCommand(8, ZRF.FUNCTION,25);// to    design.addCommand(8, ZRF.FUNCTION,28);// end    design.addPiece("Pawn", 0);    design.addMove(0, 0, [0, 0], 0);    design.addMove(0, 1, [1], 0);    design.addMove(0, 1, [3], 0);    design.addMove(0, 2, [2, 0, 0], 0);    design.addMove(0, 2, [4, 0, 0], 0);    design.addPiece("Knight", 1);    design.addMove(1, 4, [0, 1], 0);    design.addMove(1, 4, [0, 3], 0);    design.addMove(1, 4, [6, 5], 0);    design.addMove(1, 4, [6, 7], 0);    design.addMove(1, 4, [2, 3], 0);    design.addMove(1, 4, [2, 7], 0);    design.addMove(1, 4, [4, 1], 0);    design.addMove(1, 4, [4, 5], 0);    design.addPiece("Bishop", 2);    design.addMove(2, 5, [1, 1], 0);    design.addMove(2, 5, [3, 3], 0);    design.addMove(2, 5, [5, 5], 0);    design.addMove(2, 5, [7, 7], 0);    design.addPiece("Rook", 3);    design.addMove(3, 5, [0, 0], 0);    design.addMove(3, 5, [2, 2], 0);    design.addMove(3, 5, [4, 4], 0);    design.addMove(3, 5, [6, 6], 0);    design.addPiece("Queen", 4);    design.addMove(4, 5, [0, 0], 0);    design.addMove(4, 5, [1, 1], 0);    design.addMove(4, 5, [2, 2], 0);    design.addMove(4, 5, [3, 3], 0);    design.addMove(4, 5, [4, 4], 0);    design.addMove(4, 5, [5, 5], 0);    design.addMove(4, 5, [6, 6], 0);    design.addMove(4, 5, [7, 7], 0);    design.addPiece("King", 5);    design.addMove(5, 6, [0], 0);    design.addMove(5, 6, [1], 0);    design.addMove(5, 6, [2], 0);    design.addMove(5, 6, [3], 0);    design.addMove(5, 6, [4], 0);    design.addMove(5, 6, [5], 0);    design.addMove(5, 6, [6], 0);    design.addMove(5, 6, [7], 0);    design.addMove(5, 7, [2, 2, 2, 4, 4], 1);    design.addMove(5, 8, [4, 4, 4, 4, 2, 2, 2], 1);    design.addPiece("PawnR", 6);    design.addPiece("KnightR", 7);    design.addPiece("BishopR", 8);    design.addPiece("RookR", 9);    design.addPiece("QueenR", 10);    design.addPiece("KingR", 11);    design.setup("White", "Pawn", ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"]);    design.setup("White", "Knight", ["B1", "G1"]);    design.setup("White", "Bishop", ["C1", "F1"]);    design.setup("White", "Rook", ["A1", "H1"]);    design.setup("White", "Queen", ["D1"]);    design.setup("White", "King", ["E1"]);    design.setup("White", "PawnR", ["RWP"]);    design.setup("White", "KnightR", ["RWN"]);    design.setup("White", "BishopR", ["RWB"]);    design.setup("White", "RookR", ["RWR"]);    design.setup("White", "QueenR", ["RWQ"]);    design.setup("White", "KingR", ["RWK"]);    design.setup("Black", "Pawn", ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"]);    design.setup("Black", "Knight", ["B8", "G8"]);    design.setup("Black", "Bishop", ["C8", "F8"]);    design.setup("Black", "Rook", ["A8", "H8"]);    design.setup("Black", "Queen", ["D8"]);    design.setup("Black", "King", ["E8"]);    design.setup("Black", "PawnR", ["RBP"]);    design.setup("Black", "KnightR", ["RBN"]);    design.setup("Black", "BishopR", ["RBB"]);    design.setup("Black", "RookR", ["RBR"]);    design.setup("Black", "QueenR", ["RBQ"]);    design.setup("Black", "KingR", ["RBK"]);}Dagaz.View.configure = function(view) {    var b = view.root.addRegion(70, 0, 540, 540);    b.addBoard("WhiteBoard", [0]);    b.addBoard("BlackBoard", [1]);    var g = b.addGrid(31, 31, 89, 89);    g.addScale("A/B/C/D/E/F/G/H", 60, 0);    g.addScale("8/7/6/5/4/3/2/1", 0, 60);    g.addTurns(0, [0]);    g.addTurns(1, [1]);    var r = view.root.addRegion(630, 0, 120, 540);    r.addBoard("rpw", [0]);    r.addBoard("rpb", [1]);    r.addPosition("RWP",  1,  41, 58, 58, [0]);    r.addPosition("RWN",  1, 121, 58, 58, [0]);    r.addPosition("RWB",  1, 201, 58, 58, [0]);    r.addPosition("RWR",  1, 281, 58, 58, [0]);    r.addPosition("RWQ",  1, 361, 58, 58, [0]);    r.addPosition("RWK",  1, 441, 58, 58, [0]);    r.addPosition("RBP", 61,  41, 58, 58, [0]);    r.addPosition("RBN", 61, 121, 58, 58, [0]);    r.addPosition("RBB", 61, 201, 58, 58, [0]);    r.addPosition("RBR", 61, 281, 58, 58, [0]);    r.addPosition("RBQ", 61, 361, 58, 58, [0]);    r.addPosition("RBK", 61, 441, 58, 58, [0]);    r.addPosition("RBP",  1,  41, 58, 58, [1]);    r.addPosition("RBN",  1, 121, 58, 58, [1]);    r.addPosition("RBB",  1, 201, 58, 58, [1]);    r.addPosition("RBR",  1, 281, 58, 58, [1]);    r.addPosition("RBQ",  1, 361, 58, 58, [1]);    r.addPosition("RBK",  1, 441, 58, 58, [1]);    r.addPosition("RWP", 61,  41, 58, 58, [1]);    r.addPosition("RWN", 61, 121, 58, 58, [1]);    r.addPosition("RWB", 61, 201, 58, 58, [1]);    r.addPosition("RWR", 61, 281, 58, 58, [1]);    r.addPosition("RWQ", 61, 361, 58, 58, [1]);    r.addPosition("RWK", 61, 441, 58, 58, [1]);    var d = view.root.addRegion(770, 0, 120, 540, true, undefined, Dagaz.Model.drawDivision, Dagaz.Controller.eventDivision);    d.addBoard("div");    d.addPosition("UP", 1, 1, 120, 30);    d.addPosition("DN", 1, 510, 120, 30);    view.addPiece(["WhitePawn", "WhiteKnight", "WhiteBishop", "WhiteRook", "WhiteQueen", "WhiteKing"], Dagaz.View.drawPiece);    view.addPiece(["BlackPawn", "BlackKnight", "BlackBishop", "BlackRook", "BlackQueen", "BlackKing"], Dagaz.View.drawPiece);    view.addPiece(["WhitePawnR", "BlackPawnR", "WhiteKnightR", "BlackKnightR", "WhiteBishopR", "BlackBishopR", "WhiteRookR", "BlackRookR", "WhiteQueenR", "BlackQueenR", "WhiteKingR", "BlackKingR"], Dagaz.View.drawRes);    view.addPiece(["PawnWhite", "SmallPawnWhite", "KnightWhite", "SmallKnightWhite", "BishopWhite", "SmallBishopWhite", "RookWhite", "SmallRookWhite", "QueenWhite", "SmallQueenWhite", "KingWhite", "SmallKingWhite"]);    view.addPiece(["PawnBlack", "SmallPawnBlack", "KnightBlack", "SmallKnightBlack", "BishopBlack", "SmallBishopBlack", "RookBlack", "SmallRookBlack", "QueenBlack", "SmallQueenBlack", "KingBlack", "SmallKingBlack"]);    view.addPiece(["two", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "plus", "minus", "question"]);    view.addPiece(["db", "dw", "ub", "uw"]);}


Сама модель тоже меняется. Все эти длинные и непонятные столбцы команд стековой машины могут скоро уйти в прошлое.

Описание игры станет более читаемым
(function() {var step = function(ctx, params) {    if (ctx.go(params, 0) && !ctx.isFriend()) {        ctx.end();    }}var pawnShift = function(ctx, params) {    if (ctx.go(params, 0) && ctx.isEmpty()) {        if (ctx.inZone(0)) {            ctx.promote(4);        }            ctx.end();    }}var pawnLeap = function(ctx, params) {    if (ctx.go(params, 0) && ctx.isEnemy()) {        if (ctx.inZone(0)) {            ctx.promote(4);        }            ctx.end();    }}var pawnJump = function(ctx, params) {    if (ctx.go(params, 0) &&         ctx.isEmpty() &&         ctx.inZone(1) &&         ctx.go(params, 0) &&         ctx.isEmpty()) {        ctx.end();    }}var enPassant = function(ctx, params) {    if (ctx.go(params, 0) &&        ctx.isEnemy() &&        ctx.isPiece(0)) {        ctx.capture();        if (ctx.go(params, 1)) {            ctx.put();            if (ctx.go(params, 1) &&                ctx.isLastFrom()) {                ctx.end();            }        }    }}var jump = function(ctx, params) {    if (ctx.go(params, 0) &&         ctx.go(params, 1) &&        !ctx.isFriend()) {        ctx.end();    }}var slide = function(ctx, params) {    while (ctx.go(params, 0)) {        if (ctx.isFriend()) break;        ctx.end();        if (!ctx.isEmpty()) break;    }}var O_O = function(ctx, params) {    if (ctx.go(params, 0) &&        ctx.isEmpty() &&        ctx.go(params, 0) &&        ctx.isEmpty()) {        ctx.put();        if (ctx.go(params, 0) &&            ctx.isFriend() &&            ctx.isPiece(1)) {            ctx.take();            if (ctx.go(params, 1) &&                ctx.go(params, 1)) {                ctx.end();            }        }    }}var O_O_O = function(ctx, params) {    if (ctx.go(params, 0) &&        ctx.isEmpty() &&        ctx.go(params, 0) &&        ctx.isEmpty()) {        ctx.put();        if (ctx.go(params, 0) &&            ctx.isEmpty() &&            ctx.go(params, 0) &&            ctx.isFriend() &&            ctx.isPiece(1)) {            ctx.take();            if (ctx.go(params, 1) &&                ctx.go(params, 1) &&                ctx.go(params, 1)) {                ctx.end();            }        }    }}games.model.BuildDesign = function(design) {    design.checkVersion("smart-moves", "false");    design.addDirection("w");  // 0    design.addDirection("e");  // 1    design.addDirection("s");  // 2    design.addDirection("ne"); // 3    design.addDirection("n");  // 4    design.addDirection("se"); // 5    design.addDirection("sw"); // 6    design.addDirection("nw"); // 7    design.addPlayer("White", [1, 0, 4, 6, 2, 7, 3, 5]);    design.addPlayer("Black", [0, 1, 4, 5, 2, 3, 7, 6]);    design.addPosition("a8", [0, 1, 8, 0, 0, 9, 0, 0]);    design.addPosition("b8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("c8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("d8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("e8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("f8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("g8", [-1, 1, 8, 0, 0, 9, 7, 0]);    design.addPosition("h8", [-1, 0, 8, 0, 0, 0, 7, 0]);    design.addPosition("a7", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g7", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h7", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a6", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g6", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h6", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a5", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g5", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h5", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a4", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g4", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h4", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a3", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g3", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h3", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a2", [0, 1, 8, -7, -8, 9, 0, 0]);    design.addPosition("b2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("c2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("d2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("e2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("f2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("g2", [-1, 1, 8, -7, -8, 9, 7, -9]);    design.addPosition("h2", [-1, 0, 8, 0, -8, 0, 7, -9]);    design.addPosition("a1", [0, 1, 0, -7, -8, 0, 0, 0]);    design.addPosition("b1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("c1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("d1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("e1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("f1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("g1", [-1, 1, 0, -7, -8, 0, 0, -9]);    design.addPosition("h1", [-1, 0, 0, 0, -8, 0, 0, -9]);    design.addZone("last-rank", 1, ["a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8"]);    design.addZone("last-rank", 2, ["a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1"]);    design.addZone("third-rank", 1, ["a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3"]);    design.addZone("third-rank", 2, ["a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6"]);    design.addPiece("Pawn", 0, 2);    design.addMove(0, pawnShift, [4], 0);    design.addMove(0, pawnJump, [4], 0);    design.addMove(0, pawnLeap, [7], 0);    design.addMove(0, pawnLeap, [3], 0);    design.addMove(0, enPassant, [1, 4], 0);    design.addMove(0, enPassant, [0, 4], 0);    design.addPiece("Rook", 1, 10);    design.addMove(1, slide, [4], 0);    design.addMove(1, slide, [2], 0);    design.addMove(1, slide, [0], 0);    design.addMove(1, slide, [1], 0);    design.addPiece("Knight", 2, 6);    design.addMove(2, jump, [4, 7], 0);    design.addMove(2, jump, [4, 3], 0);    design.addMove(2, jump, [2, 6], 0);    design.addMove(2, jump, [2, 5], 0);    design.addMove(2, jump, [0, 7], 0);    design.addMove(2, jump, [0, 6], 0);    design.addMove(2, jump, [1, 3], 0);    design.addMove(2, jump, [1, 5], 0);    design.addPiece("Bishop", 3, 6);    design.addMove(3, slide, [7], 0);    design.addMove(3, slide, [6], 0);    design.addMove(3, slide, [3], 0);    design.addMove(3, slide, [5], 0);    design.addPiece("Queen", 4, 18);    design.addMove(4, slide, [4], 0);    design.addMove(4, slide, [2], 0);    design.addMove(4, slide, [0], 0);    design.addMove(4, slide, [1], 0);    design.addMove(4, slide, [7], 0);    design.addMove(4, slide, [6], 0);    design.addMove(4, slide, [3], 0);    design.addMove(4, slide, [5], 0);    design.addPiece("King", 5, 1000);    design.addMove(5, step, [4], 0);    design.addMove(5, step, [2], 0);    design.addMove(5, step, [0], 0);    design.addMove(5, step, [1], 0);    design.addMove(5, step, [7], 0);    design.addMove(5, step, [6], 0);    design.addMove(5, step, [3], 0);    design.addMove(5, step, [5], 0);    design.addMove(5, O_O, [1, 0], 1);    design.addMove(5, O_O_O, [0, 1], 1);    design.setup("White", "Pawn", ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]);    design.setup("White", "Rook", ["a1", "h1"]);    design.setup("White", "Knight", ["b1", "g1"]);    design.setup("White", "Bishop", ["c1", "f1"]);    design.setup("White", "Queen", ["d1"]);    design.setup("White", "King", ["e1"]);    design.setup("Black", "Pawn", ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]);    design.setup("Black", "Rook", ["a8", "h8"]);    design.setup("Black", "Knight", ["b8", "g8"]);    design.setup("Black", "Bishop", ["c8", "f8"]);    design.setup("Black", "Queen", ["d8"]);    design.setup("Black", "King", ["e8"]);}})();


Более того, такой подход позволит полностью отказаться как от Z2J (утилиты преобразования ZRF в JavaScript), так и от самого ZRF. Описание игры можно будет писать руками, сразу на JavaScript. Всё это позволит разрабатывать более сложные игры, ещё быстрее чем раньше.

У меня были игры. Много игр, но настольные игры, за очень редким исключением это то, во что интересно играть с кем-то. В этом и заключалась проблема. Для некоторых игр мне удалось сделать ботов, но играли они слабо, а в том, что касается Шахмат или Го, я не питал никаких иллюзий по поводу того, что мне удастся разработать бота, играть с которым будет интересно. Отчасти, это связано с низкой производительностью разработанной мной универсальной модели, но по большей части, я просто не очень силён в разработке ботов для настольных игр.

Радикальным решением стала бы разработка сетевой версии Dagaz, при помощи которой пользователи смогли бы играть друг с другом через Internet, но это подразумевало платный хостинг с полноценным бакэндом и базой данных. Поскольку речь шла о хобби-проекте, я был не готов вкладывать в это деньги.

К счастью, появилась другая возможность
Дело в том, что моё начальство уже давно в курсе моего увлечения настольными играми. Иногда я провожу внутренние митапы на эту и другие темы, а недавно мы организовали небольшой, но самый настоящий турнир, по разработке ботов для игры в "Atari Go" среди сотрудников компании. И вот, буквально на днях, отдел маркетинга выступил с предложением о разработке платформы для повышения лояльности среди клиентов компании. Для меня это выглядело как публикация в Интернете нескольких, отобранных отделом маркетинга, игр и генерация бонусов, для получения скидок, при победах игроков. Разумеется, меня такое предложение очень порадовало.

Так, волей-неволей, мне пришлось взяться за бакэнд. Я выбрал Nest. Во первых, давно хотел попробовать TypeScript. Кроме того, все эти аннотации, максимально похожи на тот кровавый энтерпрайз, к которому я привык. Под капотом у Nest-а всё тот же Express, но код записывается лаконичнее это удобно. Swagger прикручивается к проекту при помощи нескольких строк кода, а также всё новых и новых аннотаций. После этого, с REST API можно ознакамливаться так или даже так.

Отдельно стоит рассказать о базе данных. Вообще, я из тех людей, кому гораздо проще сразу писать SQL запрос, чем разбираться в премудростях какой-нибудь ORM. Но для этого проекта я сделал исключение. Просто потому что TypeORM создаёт все задекларированные таблички автоматически, а мне остаётся просто создать на сервере пустую базу данных. Разумеется, я выбрал PostgreSQL, но думаю, что переключиться на MySQL, при необходимости, будет не слишком сложно.

Подробнее про базу данных
Для меня, игровой сервер это прежде всего база данных. В ней хранятся учётные данные пользователей, описания игр, данные по игровым сессиям, все выполненные игроками ходы, в общем, всё что нужно для организации игры по сети.


Параметры подключения к серверу конфигурируются в файле ormconfig.json в корне проекта. Помимо понятных вещей, типа логина и пароля, там определяется путь к каталогу, в котором описываются сущности базы данных (entities) и это именно то, за что мне нравится TypeORM. Дело в том, что мне нет необходимости писать SQL-скрипты для создания таблиц в базе данных. Достаточно создать по одному TypeScript-описанию на каждую таблицу.

Например такому
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn, Check, Unique } from "typeorm";import { users } from "./users";import { game_sessions } from "./game_sessions";import { game_results } from "./game_results";@Entity()@Unique(["session_id", "player_num"])@Check(`"is_ai" in (0, 1)`)export class user_games {    @PrimaryGeneratedColumn()    id: number;    @Index()    @Column({ nullable: false })    user_id: number;    @ManyToOne(type => users)    @JoinColumn({ name: "user_id" })    user: users;    @Index()    @Column({ nullable: false })    session_id: number;    @ManyToOne(type => game_sessions)    @JoinColumn({ name: "session_id" })    session: game_sessions;    @Index()    @Column({ nullable: true })    result_id: number;    @ManyToOne(type => game_results)    @JoinColumn({ name: "result_id" })    result: game_results;    @Column({ nullable: true })    score: number;    @Column()    player_num: number;    @Column({ default: 0 })    is_ai: number;    @Column({ nullable: true })    time_limit: number;}


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

Но структура таблиц это только половина дела. Надо как-то заполнить справочники начальными значениями. Для этого, в TypeORM используются миграции. Миграция это просто пара SQL-скриптов, используемых для наката и отката изменений в базе данных.

Что-то в таком роде
import {MigrationInterface, QueryRunner} from "typeorm";export class init1592210976213 implements MigrationInterface {    public async up(queryRunner: QueryRunner): Promise<any> {        await queryRunner.query(`insert into contact_types(id, name) values(1, 'EMail')`);        await queryRunner.query(`insert into token_types(id, name) values(1, 'Access')`);        await queryRunner.query(`insert into token_types(id, name) values(2, 'Refresh')`);        await queryRunner.query(`insert into game_results(id, name) values(1, 'Won')`);        await queryRunner.query(`insert into game_results(id, name) values(2, 'Lose')`);        await queryRunner.query(`insert into game_results(id, name) values(3, 'Draw')`);        await queryRunner.query(`insert into users(is_admin, login, pass) values(1, 'root', 'root')`);    }    public async down(queryRunner: QueryRunner): Promise<any> {        await queryRunner.query(`delete from users`);        await queryRunner.query(`delete from game_statuses`);        await queryRunner.query(`delete from game_results`);        await queryRunner.query(`delete from token_types`);        await queryRunner.query(`delete from contact_types`);        await queryRunner.query(`delete from realms`);    }}


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

npm install -g ts-nodets-node ./node_modules/typeorm/cli.js migration:run

Все выполненные миграции фиксируются в специальной табличке в базе данных (по умолчанию, она называется migrations) и при необходимости могут быть отменены командой 'typeorm migration:revert'.

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

  @UseGuards(LocalAuthGuard)  @Post('api/auth/login')  async login(@Request() req) {    const device: string = req.headers['x-forwarded-for'] ||                            req.connection.remoteAddress;    const r = await this.authService.login(req.user, device);    return r;  }

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

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

Проще всего, это делается в режиме отладки. Создаём файл с настройками, запускаем фронтэнд командой 'ng serve --proxy-config proxy.conf.json' и отлаживаемся на здоровье (при условии, что бакэнд тоже запущен, конечно). Все запросы, начинающиеся с префикса '/api' будут проксироваться на бакэнд. Но держать два открытых TCP-порта в релизной сборке немного неудобно. Вот как это делается в Nest-е:

main.ts
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { NestExpressApplication } from "@nestjs/platform-express";import { join } from 'path';import { NotFoundExceptionFilter } from './frontend.catch';async function bootstrap() {  const app = await NestFactory.create<NestExpressApplication>(    AppModule,  );  app.useStaticAssets(join(__dirname, '/../public'), {prefix: '/'});  app.setBaseViewsDir(join(__dirname, '/../public'));  app.useGlobalFilters(new NotFoundExceptionFilter());  app.enableCors();  await app.listen(3000);}bootstrap();

frontend.catch.ts
import { NotFoundException, Catch, ExceptionFilter, ArgumentsHost, HttpException } from "@nestjs/common";import { resolve } from 'path';@Catch(NotFoundException)export class NotFoundExceptionFilter implements ExceptionFilter {  catch(exception: HttpException, host: ArgumentsHost) {    const ctx = host.switchToHttp();    const response = ctx.getResponse();    response.sendFile(resolve('public/index.html'));  }}


Для начала, учим бакэнд отдавать статический контент. Корень сайта отображается на каталог public, то есть, файлы Nest будет искать именно там. Но что произойдёт, если файл найти не удастся? На этот случай, в проект добавлен перехватчик frontend.catch.ts, пересылающий все непонятные запросы в 'public/index.html', то есть собранному фронтэнду с сохранением URL. Это немножко трудно уложить в голове, но именно благодаря этому работает SPA Angular-а.

Теперь, когда всё заработало, осталось разместить это на какой-то живой машине, доступной из Internet. Но перед этим, стоит озаботиться переходом от HTTP-протокола к HTTPS (очевидно, что передача логинов и паролей в открытом виде, при выполнении базовой аутентификации это не самая удачная мысль). В принципе, Nest это тоже умеет, но гораздо проще, в смысле администрирования, установить на тот же сервер proxy Nginx. Конфигурация выглядит примерно вот так:

server {listen 80;listen 443 ssl;        ssl_certificate /etc/nginx/ssl/certificate.crt;        ssl_certificate_key /etc/nginx/ssl/private.key;        ssl_verify_client off;server_name games.dtco.ru;location / {index index.html;proxy_pass http://localhost:3000/;}}

Здесь, правда, не обошлось без ложки дёгтя
Для того чтобы сервер работал сам по себе, в Linux системах его желательно оформлять как сервис. В моём случае, это вылилось вот в такой вот dagaz.service файл:

[Unit]Description=Dagaz ServerAfter=network-online.target[Service]Restart=on-failureType=simpleUser=dagazWorkingDirectory=/home/dagaz/Downloads/DagazServer-masterExecStart=/usr/bin/npm start[Install]WantedBy=multi-user.target

Замечаете неладное? Да-да, я запускаю его командой 'npm start', то есть, при каждом запуске собираю заново из TypeScript-а. Что меня заставило пойти на это? Начну издалека. В идеальном мире, мне следовало бы собрать сервер один раз и после этого запускать уже собранный. Примерно так:

nest buildcd buildnode main.js

Но я использую TypeORM, чтобы работать с базой данных. Для начала, мне не удалось прикрутить ormconfig.json, из которого берутся настройки подключения к собранному серверу. Само по себе, это не беда. Есть другой способ для передачи настроек.

database.provider.ts
import { createConnection } from 'typeorm';import { dbOptions } from '../app.config';export const databaseProviders = [  {    provide: 'DATABASE_CONNECTION',    useFactory: async () => await createConnection(      dbOptions    ),  },];

app.config.ts
import "reflect-metadata";import { ConnectionOptions } from "typeorm";export let dbOptions: ConnectionOptions = {    type: "postgres",    host: "127.0.0.1",    port: 5433,    username: "dagaz",    password: "dagaz",    database: "dagaz",    migrationsTableName: "migrations",    synchronize: false,    logging: false,    entities: [        "src/entity/**/*.{ts,js}",        "./entity/**/*.{ts,js}"    ]}


Здесь есть нюанс. В entities я передаю две строки (это пути, по которым TypeORM ищет описания сущностей базы данных). Первая используется в режиме отладки, а по второй поиск идёт уже в собранной версии. В принципе, в каждой из них можно было оставить по одному расширению, просто я хотел показать и эту возможность. Файл ormconfig.json, после выполненных изменений, конечно убираем, он больше не нужен.

Ещё один добрый совет. Устанавливайте synchronize в false всегда, если только не собираетесь изменять структуру таблиц в базе данных. По причинам, о которых скажу чуть позже, со сборкой я возился на немного устаревшей версии проекта. Этого вполне хватило, чтобы снести в базе данных ненужные с точки зрения TypeORM поля. После чего, всё естественно сломалось. Не повторяйте моих ошибок.

Собранная версия запускается заметно быстрее (поскольку не надо каждый раз пересобирать TypeScript), но увы, воспользоваться этим я не смог.

Возникла ещё одна проблема
C:\Users\User\dagaz-server>npm run build> dagaz-server@0.0.1 prebuild C:\Users\User\dagaz-server> rimraf dist> dagaz-server@0.0.1 build C:\Users\User\dagaz-server> tsc -p tsconfig.build.jsonnode_modules/@nestjs/swagger/dist/type-helpers/omit-type.helper.d.ts:2:90 - error TS1005: ',' expected.2 export declare function OmitType<T, K extends keyof T>(classRef: Type<T>, keys: readonly K[]): Type<Omit<T, typeof keys[number]>>;                                                                                           ~node_modules/@nestjs/swagger/dist/type-helpers/omit-type.helper.d.ts:2:91 - error TS1005: ',' expected.2 export declare function OmitType<T, K extends keyof T>(classRef: Type<T>, keys: readonly K[]): Type<Omit<T, typeof keys[number]>>;                                                                                            ~node_modules/@nestjs/swagger/dist/type-helpers/pick-type.helper.d.ts:2:90 - error TS1005: ',' expected.2 export declare function PickType<T, K extends keyof T>(classRef: Type<T>, keys: readonly K[]): Type<Pick<T, typeof keys[number]>>;                                                                                           ~node_modules/@nestjs/swagger/dist/type-helpers/pick-type.helper.d.ts:2:91 - error TS1005: ',' expected.2 export declare function PickType<T, K extends keyof T>(classRef: Type<T>, keys: readonly K[]): Type<Pick<T, typeof keys[number]>>;Found 4 errors.npm ERR! code ELIFECYCLEnpm ERR! errno 2npm ERR! dagaz-server@0.0.1 build: `tsc -p tsconfig.build.json`npm ERR! Exit status 2npm ERR!npm ERR! Failed at the dagaz-server@0.0.1 build script.npm ERR! This is probably not a problem with npm. There is likely additional logging output above.npm ERR! A complete log of this run can be found in:npm ERR!     C:\Users\User\AppData\Roaming\npm-cache\_logs\2020-10-21T09_33_58_253Z-debug.log


При попытке сборки, ошибки компиляции летят откуда-то из недр @nestjs/swagger. Обновление пакета и TypeScript-а до последних версий не помогло. Если кто-то знает, как с этим бороться, скажите мне, я заинтригован. А сборка, о которой я писал выше, отлаживалась на копии проекта с полностью вырезанным Swagger-ом (и немного устаревшей, да). В результате, я не захотел отказываться от Swagger-а и запускаю сервис командой 'npm start'.

А что с играми? Если вы ещё не забыли, наша цель связать имеющиеся игры с сервером (в рамках текущего проекта, чтобы получать от сервера сгенерированный бонус при выигрыше). В этом вполне может помочь jQuery (кстати, это одна из всего лишь трёх используемых мной сторонних библиотек, кроме неё я использую Underscore и в некоторых случаях Seedrandom). Фактически, вся интеграция свелась к переписыванию всего одного модуля.

var SERVICE = "/api/";var inProgress = false;var auth = null;...var authorize = function() {  if (auth !== null) return;  inProgress = true;  $.ajax({     url: SERVICE + "auth/anonymous",     type: "GET",     dataType: "json",     success: function(data) {         auth = data.access_token;         console.log('Auth: Succeed ' + auth);         inProgress = false;     },     error: function() {         Dagaz.Controller.app.state = STATE.STOP;         alert('Auth: Error!');     },     statusCode: {        401: function() {             Dagaz.Controller.app.state = STATE.STOP;             alert('Auth: Bad User!');        },        500: function() {             Dagaz.Controller.app.state = STATE.STOP;             alert('Auth: Internal Error!');        }     }  });}...App.prototype.exec = function() {  this.view.configure();  ...  this.view.draw(this.canvas);  if (inProgress) return;  authorize();  ...}

В этом месте произошёл второй непонятный мне момент
Сразу после запуска, каждая игра выполняет несколько REST-запросов с JWT-аутентификацией к серверу. Проблема выглядела как плавающий сбой одного из таких запросов (в результате которого игра аварийно завершалась). Чтобы понять что происходит, пришлось собрать пакеты, летающие между Nginx-ом и Nest-ом.

tcpdump -vv -i any -s 4906 /var/log/dagaz.pcap

Вот что получилось в итоге:


Запросы передаваемые через IPv4 обрабатывались нормально, а передаваемые через IPv6 отбрасывались с ошибкой авторизации (403). Я облазил исходники Nest-а, но так и не понял, почему это происходит (если у кого-то есть мысли на этот счёт буду рад выслушать). В результате, IPv6 на Loopback-е пришлось отключить:

sysctl.conf
net.ipv6.conf.all.disable_ipv6=1net.ipv6.conf.default.disable_ipv6=1net.ipv6.conf.lo.disable_ipv6=1


и всё заработало.

Конечно, только этим дело не ограничивалось. Одним из требований отдела маркетинга была возможность запуска игр на мобильных устройствах. В этом, мне помог jonic (за что я очень ему благодарен). Было совершенно необходимо уметь масштабировать canvas, на котором происходит вся игра, по размерам экрана мобильного телефона или планшета. При этом, соотношение сторон должно было сохраняться, а в том случае если ширина canvas-а значительно превосходит высоту было крайне желательно попросить пользователя повернуть устройство должным образом.


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


Вот так, при помощи Nest-а, jQuery, Nginx-а и небольшой доли везения нам удалось развернуть в Internet-е игровой сервер. Кому стало интересно, заходите ещё.
Подробнее..

DagazServer Встречайте Garbo Chess

21.01.2021 18:22:37 | Автор: admin
Кто мне сказал, не получится?
Если мне хочется, сбудется!

Земфира

Плюнь тому в глаза, кто скажет,
что можно объять необъятное!

Козьма Прутков "Плоды раздумья"


Новогодние праздники вновь навалились внезапно. Такое обилие свободного времени было просто необходимо разбавить какой-то осмысленной деятельностью и я решил приделать к своему серверу бота для игры в Шахматы. Готовых шахматных движков существует множество. Я решил остановиться на Garbochess-JS простой и понятной реализации, на языке JavaScript, названной в честь знаменитой актрисы Греты Гарбо (вы можете видеть её на фотографии).

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


Игра, выбранная на карте, запоминается. Поэтому, после ввода логина и пароля, вы попадаете на вкладку запуска этой игры. Вы можете запустить игру (при помощи кнопки Launch) или просмотреть сессии, ожидающие подключения второго игрока (Join) и ранее сыгранные игры (View), при их наличии. Если вы запустите игру, создастся новая сессия, ожидающая подключения второго игрока. Вы попадёте в игру, сможете сделать ход (если выбрали игру первым игроком), но ожидание ответного хода может занять некоторое время (на самом деле, никто не гарантирует, что кто-нибудь вообще подключится к этой сессии). По этой причине, я рекомендую просматривать список сессий, ожидающих подключения, по интересующей вас игре, перед созданием новой сессии при помощи Launch. Вы избавите себя от лишнего ожидания и, может быть, доставите удовольствие другому человеку.

Другая возможность реализована не для всех игр, но если на форме имеется чекбокс Play against AI, вы можете ничего не ждать вовсе. Эта опция запускает режим игры с ботом, отвечающим на сделанные ходы практически немедленно. Как правило, речь идёт об очень простых ботах, без каких-то сложных вычислений (хотя победить некоторых из них, всё равно, может быть тяжело). Для Шахмат такое решение не подходит вовсе, но возможность поиграть с ботом всё-таки имеется.

Чего хотелось


Обычный встраиваемый в игру бот не является для Dagaz чем-то новым. Технически, такая игра представляет собой html-сборку, включающую в себя модуль выбора оптимального для этой игры хода, в зависимости от текущей позиции. К имени html-файла добавляется суффикс "-ai", позволяющий серверу загрузить правильную сборку, при выборе режима игры с ботом. Думаю понятно, что разработать бота, подходящего абсолютно для всех существующих на свете игр невозможно, но можно использовать одни и те же боты для различных, но сходных между собой игр.

Например, этот бот используется очень часто
(function() {function RandomAi(params) {  this.params = params;  if (_.isUndefined(this.params.rand)) {      this.params.rand = _.random;  }}var findBot = Dagaz.AI.findBot;Dagaz.AI.findBot = function(type, params, parent) {  if ((type == "random") || (type == "solver")) {      return new RandomAi(params);  } else {      return findBot(type, params, parent);  }}RandomAi.prototype.setContext = function(ctx, board) {  ctx.board  = board;}RandomAi.prototype.getMove = function(ctx) {  var moves = Dagaz.AI.generate(ctx, ctx.board);  if (moves.length == 0) {            return { done: true, ai: "nothing" };  }  if (moves.length == 1) {      return { done: true, move: moves[0], ai: "once" };  }  var ix = this.params.rand(0, moves.length - 1);  return {      done: true,      move: moves[ix],      ai:   "random"  };}})();

Это не шутка. Дело в том, что в Dagaz боты выстроены в цепочку и бот рандомного выбора хода используется в тех случаях, когда боты, стоящие выше по иерархии, дать ответ отказались (такое возможно в тех случаях, когда любой ход ведёт к поражению). Разумеется, есть и более сложные боты, но, в целом, все они не очень сильные. Прежде всего, это связано с использованием универсальной (а значит медленной) модели, пригодной для абсолютно любых игр, реализуемых Dagaz.

Второе ограничение связано со временем расчёта хода. Для встроенного бота оно не может превышать 2-3 секунд, поскольку все вычисления производятся непосредственно в браузере пользователя и, в силу однопоточности JavaScript, все другие действия, на это время блокируются. Если задержаться слишком долго, браузер выбросит предупреждение о возможном зацикливании на странице или вообще молча остановит все выполняющиеся скрипты (такое случалось в Safari).

Как бороться с этими двумя бедами понятно. Поскольку у нас есть сервер, надо разработать клиента, соединяющегося с ним по REST и отвечающего на ходы других пользователей. Иными словами, надо разработать внешнего бота. В качестве языка разработки можно использовать всё тот же JavaScript и запускать бота в Node.js, на одном хосте с сервером (или где-то ещё, это не принципиально).

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


После старта, бот выполняет инициализацию, авторизуясь на сервере, после чего переходит к циклу опроса: ищет на сервере сессии, в которых он должен выполнить очередной ход (TURN), загружает текущую позицию (RECO), передаёт её описание в Garbo Chess, а полученный ответный ход отправляет на сервер (MOVE). Если сессий ожидающих хода нет, бот переходит к проверке наличия ожидающих сессий, созданных ботом (CHCK), если таковых нет и общее количество сессий, в которых участвует бот, меньше заданного, создаёт новую (SESS).

При возникновении ошибок (например, в случае устаревания JWT-токена), бот возвращается к фазе инициализации (INIT) для выполнения повторной авторизации, а если ошибка возникла уже там (при недоступности сервера или чём-то подобном) переходит к фазе STOP и останавливается. Вот так всё это выглядит.

Бот может играть одновременно с несколькими игроками, но в каждый момент времени имеет дело не более чем с одной игрой. Таким образом, если игра ведётся с несколькими людьми, каждому придётся ждать чуть дольше. Кроме того, это не самый эффективный способ использования Garbo Chess. Движок поддерживает режим, при котором анализ игры ведётся непрерывно (запускается в отдельном потоке Web Worker-а), но в этом случае, играть можно только с одним противником.

SAN-ы, FEN-ы, PGN-ы


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

С этим была небольшая засада
Разумеется, в Dagaz я тоже описываю позиции. Это функциональность, обойтись без которой очень трудно. Представьте себе, что доиграв до середины партии мы вышли из игры, а потом зашли в неё заново (или просто перегрузили страницу). Было бы глупо прокручивать все ходы с самого начала. DagazServer использует описание позиции, чтобы загрузить правильную расстановку фигур сразу, без утомительной перемотки.

Вернее, он действует ещё хитрее: в случае если ожидается ход противника, загружается последняя расстановка фигур, как и было написано, но если ходить должен игрок, DagazServer загружает предыдущую позицию, возникшую в игре на момент начала последнего хода противника. Следующий ход игра получает обычным образом, уже после загрузки html. Благодаря этому, игрок, загружая игру, всегда видит анимацию последнего хода (или нескольких ходов) противника. А если он не успел рассмотреть этот ход во всех подробностях, всегда можно просто перезагрузить страницу.

Таким образом, модули описания позиции, вроде этого важная часть проекта. У меня их несколько и я использую тот или иной, в зависимости от особенностей игры. Почему я не использовал FEN? Просто потому, что Dagaz это не только Шахматы. Приведу простой пример: возможность рокировки, в FEN кодируется как KQkq, но такое описание слишком завязано на правила традиционных шахмат! Даже в слегка изменённых шахматных вариантах нотацию приходится расширять. В любом случае, мне понадобился FEN и я его сделал.

В ответ на полученное описание, Garbo Chess, проработав некоторое время, возвращает лучший (по его мнению) ход (просто пара позиций вроде e2e4 и тип фигуры, при наличии превращения). К сожалению, на момент разработки бота, на сервере было сыграно уже довольно много партий в Шахматы и я хранил описание хода немного в другом виде. Я не хотел, чтобы эти партии сломались и добавил дефис, разделяющий позиции (тот факт, что старые партии не использовали FEN-нотацию роли не играл, при проигрывании уже завершённых партий, описания позиций не используются).

С нотациями пришлось ещё повозиться
Одной из особенностей минимаксных алгоритмов является их детерминированность. При одинаковых условиях, будет формироваться один и тот же ход. Это может привести к однообразной игре бота, что не очень приятно. Я вижу два способа борьбы с этим:

  • Случайное ограничение времени на расчёт хода
  • Книга дебютов

С первым пунктом всё понятно вместо того чтобы жёстко ограничивать время на расчёт хода, например одной секундой, можно задать вилку из минимального и максимального значений и каждый раз брать случайное число в этих пределах. Что касается книги дебютов, то она полезна сама по себе. Шахматные движки придуманы для миттельшпиля. В дебюте и эндшпиле они не столь эффективны. Для эндшпилей существуют таблицы Налимова, а дебюты обрабатываются специальными справочниками, гораздо более компактными.

В силу своей иерархической организации, дебютные таблицы предоставляют некоторую вариативность. При наличии для позиции нескольких лучших ходов, мы можем использовать случайный выбор. Я уже использовал дебютные таблицы в Dagaz, в формате SGF, но для сервера потребовалось реорганизовать их иначе. Вместо последовательностей ходов от начала партии, мне были нужны FEN-описания позиций со списками соответствующих им лучших ходов (это, кстати, позволяет описывать дебютные ловушки, путём сохранения лучших ходов только одной стороны). Здесь очень помогла утилита pgn2fen, обнаруженная в блоге Николая Кисленко, работающая как с SAN так и с Long algebraic notation.

Далее, я создал пользователя, для того чтобы бот мог заходить на сервер и прописал его id в качестве дежурного бота шахматным играм, для того чтобы frontend автоматически включал бота в созданную игру, при выборе опции Play against AI. В целом, новая схема данных выглядела так:


Здесь есть ещё один момент, о котором стоит сказать. Я добавил табличку ai_settings, для хранения настроек бота по отношению к игроку в конкретной игре. Это, своего рода, рейтинг. Если игрок выигрывает, дополнительное время, выделяемое боту на раздумье, увеличивается. При поражениях оно уменьшается. Таким образом, бот может подстраиваться к силе игрока, с которым он играет.

Что получилось


Это одна из игр бота на сайте. И это не дебютная ловушка (можете проверить это самостоятельно)! При ограничении времени на расчёт хода в 1-2 секунды, бот вполне разумно играет против человека и умеет ставить красивые маты. Возможности бота не ограничиваются классическими шахматами. Подойдёт любая игра, в которой правила перемещения фигур не изменены, например "Шахматы Фишера" или "Шахматы Будённого".

Правда, здесь есть небольшая загвоздка
Шахматный движок должен знать, с какой позиции началась игра. Если первым ходит человек, никаких проблем нет после выполнения хода, FEN-описание позиции передаётся на сервер, но если бот ходит первым, описать позицию некому, поскольку сервер ничего не знает о конкретике игры. Мне пришлось применить небольшой трюк загружать FEN-описание начальной позиции даже в том случае, если игра после загрузки ожидает ход бота.

Может возникнуть, своего рода ситуация гонок, когда бот получает описание сессии раньше, чем интерфейс игры успел загрузиться и сохранить FEN начальной позиции. Понятно, что ни к чему хорошему это привести не может, поэтому я добавил атрибут setup_required, требующий наличия FEN на момент загрузки сессии. Если FEN ещё не сформирован, сервер возвращает ошибку, получив которую, бот просто переходит к повторной авторизации, после чего продолжает выполнение обычным образом.

Также, без каких либо изменений, можно играть в "Шахматы втёмную". Если проявить немного фантазии, можно пойти ещё дальше. Помните, как построены миссии кампании в знаменитой "Battle vs Chess"?

Здесь, в качестве бота, используется обычный шахматный движок Fritz. Если подумать, это становится понятно. Подрыв на мине просто потеря фигуры. Бот не будет ходить на мины в том случае если будет их учитывать это просто невыгодно, а суть миссии как раз в том, чтобы заманивать в ловушку его фигуры. Решение простое информация о минах просто не должна передаваться боту.


Вот что получается: я добавляю в FEN описание мин, но перед передачей в Garbo Chess, просто убираю мины из описания, а после получения ответа возвращаю в позицию те мины, которые не взорвались в результате хода бота. Теперь вы можете заманить бота в ловушку уже на DagazServer.

Что дальше


Есть много шахматных игр: Шатрандж, Макрук бот может играть в них, надо только добавить в Garbo Chess новые фигуры. Есть игры на малых досках. Наконец, можно переосмыслить Garbo Chess, создав на его основе универсальный движок, подходящий для более широкого класса игр. Возможностей для развития много, было бы желание.
Подробнее..

Сплетая дизайн

20.07.2020 20:10:45 | Автор: admin
Три кольца премудрым эльфам
для добра их гордого.
Семь колец пещерным гномам
для труда их горного.
Девять людям Средиземья

Джон Рональд Руэл Толкин


Работая над играми, я уделяю большое внимание дизайну. Речь идёт не о красивой картинке, хотя и она конечно имеет значение. Прежде всего, для меня важны удобные игровые механики. Игра должна приносить удовольствие. Когда в Сапёре, при клике на пустое поле, автоматически открываются все поля не соседствующие с минами это удобно. Известный всем пасьянс, также можно значительно улучшить, просто избавив пользователя от необходимости выполнения лишних действий. Но сегодня я хочу рассказать о другой игре. Той самой, которая изображена на КДПВ (как обычно, все картинки в статье кликабельные).

Оригинальная игра Filler была разработана компанией Gamos в далёком 1990 году и запускалась под управлением MS DOS. С тех пор, было выпущено немало её клонов, работающих на всех мыслимых и немыслимых платформах. В какой-то момент, я тоже решил быть в тренде и разработать свою версию этой игры. Ладно-ладно, я просто хотел проверить, как Dagaz справится с таким количеством изменяемых фигур на доске.


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

На картинке можно заметить два важных элемента пользовательского интерфейса. Двухцветная полоска сверху призвана избавить игроков от утомительных расчётов в уме того, чья территория больше. Можно было бы отобразить точные значения захваченной территории, но я стараюсь не применять числовые обозначения там, где без них можно обойтись. Что касается панели внизу, то именно с её помощью игрок выполняет свой ход. Помимо текущего, на ней отображаются лишь те цвета, которые разрешается выбрать по правилам игры. Ввод с клавиатуры также поддерживается. Можно не утруждать себя тыканьем мышью по экрану, а ограничиться нажатием всего одной клавиши с выбранной цифрой.

И то и другое - примеры повторного использования кода в Dagaz
Управление игрой с клавиатуры мне понадобилось когда я разрабатывал Sokoban (тоже клон, да). Клавиатурные стрелки, в этой игре, обеспечивают более естественное управление чем мышь. Надо сказать, что это был первый момент, когда пришлось писать разный код для различных браузеров. Пока всё ограничивалось тыканьем мыши по экрану и рисованием на канвасе, код оставался вполне себе браузерно-независимый.


Что касается панели с отображением распределения очков это довольно востребованный элемент. Впервые, он понадобился когда я работал над чудесной игрой из Брунея под названием "Пасанг".


Я пошёл навстречу пожеланием играющих и сделал полоску с отображением очков потолще.

Разумеется, первое о чём меня попросили благодарные пользователи это уменьшение размеров игрового поля (игра на доске 80x40 довольно занимательна, но крайне продолжительна по времени). Я же, в свою очередь, решил добавить в игру тактического разнообразия. Базовая идея игры Filler гениальна, но нет ничего такого, что нельзя было бы сделать ещё лучше. В первую очередь, я подумал о стенах. С их помощью можно строить лабиринты!


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


Далее, мне не нравилось то, что территорию, захваченную одним из игроков нельзя вернуть в игру, чтобы впоследствии перераспределить иным образом. Я добавил в игру тьму, распространяющуюся по доске при каждом ходе. Стены ограничивают её движение, но никакие цветные области препятствием для неё не являются. Разумеется, без каких либо сдерживающих факторов, тьма просто распространилась бы по доске, раздавив обоих игроков. Требовались какие-то предохранители.


Специальные поля с жёлтыми искрами позволяют подорвать соприкасающуюся с ними тьму, заполнив захваченную ей область случайно распределёнными цветами. Граничащие с этой областью клетки также подрывается, что позволяет разрушать стены. Голубые искры позволяют остановить тьму заморозив фронт её распространения. Это, своего рода, универсальный предохранитель. Для заморозки ему не обязательно граничить с тьмой. Оба типа детонаторов срабатывают как при достижении их игроками, так и в тех случаях, когда тьма до них добирается первой.

Кстати, о взрывах
Тема изменяемой геометрии доски очень привлекательна в компьютерных реализациях настольных игр. Одновременное перемещение нескольких фигур способно внести радикальные изменения в расстановку фигур на доске. Но платформа, на которой перемещаются фигуры, также является фигурой. Само собой, в голову приходят различные идеи для выведения из игры фигур целыми платформами. Именно таким образом в "Платформенных шахматах", придуманных Робертом Прайсом в 2001 году, появились бомбы.


По сути, это те же самые пешки, у которых я отобрал возможность атакующего хода. Дойдя до последней линии, они также ни во что не превращаются. Для чего они нужны? Такая фигура способна уничтожить всю платформу целиком, вместе с находящимися на ней фигурами. Также, я добавил предохранительный механизм, запрещающий взрыв при наличии на платформе других дружественных фигур. Автор игры всецело одобрил моё нововведение.

Детонаторы можно использовать и сами по себе, динамически строя лабиринт в зависимости от действий игроков. Дело в том, что заморозка, на самом деле, действует не на фронт распространение тьмы, как я сказал раньше, а на все клетки, изменяющие своё состояние при выполнении хода. То есть, если игрок выбрал цвет граничащий с заморозкой, все присоединённые им на этом ходу квадраты превратятся в стены. Это очень коварное препятствие, способное запереть игрока полностью. Жёлтые искры куда менее опасны.


Ну что же, я очевидно не первый, из тех кто решил разработать ещё один клон замечательной игры Filler. Будем надеяться, что и не последний. Больше игр, товарищи! Хороших и разных.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru