image — Для начала, ты должен понять главное…
— Что главное?
— Нет никакой ложки!

"Матрица"
 


Как я уже неоднократно говорил ранее, некоторые вещи реализовать в Zillions of Games попросту невозможно. Впрочем, если нельзя, но очень хочется, то иногда бывает всё таки можно. Как далеко можно зайти по этому пути?

3. Всё обман


Мы уже сталкивались с этим. ZoG не позволяет определять ходы, ограничивающиеся удалением фигур с доски? Нет проблем — создадим ход, сбрасывающий фигуру на доску и немедленно удаляющий её (в результате чего, фигура располагавшаяся на целевом поле, будет удалена автоматически). Таким образом можно создавать «кнопки», расширяя интерфейсные возможности ZoG (хотя, на самом деле, никакие это не кнопки, а просто специальные фигуры на доске). ZoG ограничивает размерность доски пятью измерениями? Тоже не беда!

Создадим недостающие позиции и свяжем их вручную
(define 6D
    (image "images\6DChess\6DChess.bmp")
    (grid
       	(start-rectangle 34 7 76 53)
       	(dimensions 			; 2x2x2x2x2
                ("Zii/Zi" (0 148))	; 5D
                ("A/B" (162 0)) 	; 4D
                ("II/I" (0 68)) 	; 3D
                ("a/b" (66 0)) ; columns
                ("2/1" (31 19))) ; rows offset 31
        (directions (n   0  0  0  0 -1) (s  0  0  0  0  1) 
		    	(e   0  0  0  1  0) (w  0  0  0 -1  0) 
		        (u   0  0 -1  0  0) (d  0  0  1  0  0) 
		        (4e  0  1  0  0  0) (4w 0 -1  0  0  0)
		        (5u -1  0  0  0  0) (5d 1  0  0  0  0)))
	(positions	; 6D
                (YiAIa1   413 241 445 287) (YiAIa2   383 223 415 269)
                (YiAIb1   477 241 509 287) (YiAIb2   448 223 480 269)
                (YiAIIa1  413 175 445 221) (YiAIIa2  383 155 415 201)
                (YiAIIb1  477 175 509 221) (YiAIIb2  448 155 480 201)
                (YiBIa1   577 241 609 287) (YiBIa2   547 223 579 269)
                (YiBIb1   641 241 673 287) (YiBIb2   609 223 642 269)
                (YiBIIa1  577 175 609 221) (YiBIIa2  547 155 579 201)
                (YiBIIb1  641 175 673 221) (YiBIIb2  609 155 642 201)
                (YiiAIa1  413 95  445 141) (YiiAIa2  383  76 415 122)
                (YiiAIb1  477 95  509 141) (YiiAIb2  448  76 480 122)
                (YiiAIIa1 413 26  445  72) (YiiAIIa2 383   7 415  53)
                (YiiAIIb1 477 26  509  72) (YiiAIIb2 448   7 480  53)
                (YiiBIa1  577 95  609 141) (YiiBIa2  547  76 579 122)
                (YiiBIb1  641 95  673 141) (YiiBIb2  609  76 642 122)
                (YiiBIIa1 577 26  609  72) (YiiBIIa2 547   8 579  54)
                (YiiBIIb1 641 26  673  72) (YiiBIIb2 609   8 642  54)
	)
	(links n 
		(YiAIa1 YiAIa2)     (YiAIb1 YiAIb2)
		(YiAIIa1 YiAIIa2)   (YiAIIb1 YiAIIb2)
		(YiBIa1 YiBIa2)     (YiBIb1 YiBIb2)
		(YiBIIa1 YiBIIa2)   (YiBIIb1 YiBIIb2)
		(YiiAIa1 YiiAIa2)   (YiiAIb1 YiiAIb2)
		(YiiAIIa1 YiiAIIa2) (YiiAIIb1 YiiAIIb2)
		(YiiBIa1 YiiBIa2)   (YiiBIb1 YiiBIb2)
		(YiiBIIa1 YiiBIIa2) (YiiBIIb1 YiiBIIb2))
	(links s 
		(YiAIa2 YiAIa1)     (YiAIb2 YiAIb1)
		(YiAIIa2 YiAIIa1)   (YiAIIb2 YiAIIb1)
		(YiBIa2 YiBIa1)     (YiBIb2 YiBIb1)
		(YiBIIa2 YiBIIa1)   (YiBIIb2 YiBIIb1)
		(YiiAIa2 YiiAIa1)   (YiiAIb2 YiiAIb1)
		(YiiAIIa2 YiiAIIa1) (YiiAIIb2 YiiAIIb1)
		(YiiBIa2 YiiBIa1)   (YiiBIb2 YiiBIb1)
		(YiiBIIa2 YiiBIIa1) (YiiBIIb2 YiiBIIb1))
	(links e 
		(YiAIa1 YiAIb1)     (YiAIa2 YiAIb2)
		(YiAIIa1 YiAIIb1)   (YiAIIa2 YiAIIb2)
		(YiBIa1 YiBIb1)     (YiBIa2 YiBIb2)
		(YiBIIa1 YiBIIb1)   (YiBIIa2 YiBIIb2)
		(YiiAIa1 YiiAIb1)   (YiiAIa2 YiiAIb2)
		(YiiAIIa1 YiiAIIb1) (YiiAIIa2 YiiAIIb2)
		(YiiBIa1 YiiBIb1)   (YiiBIa2 YiiBIb2)
		(YiiBIIa1 YiiBIIb1) (YiiBIIa2 YiiBIIb2))
	(links w 
		(YiAIb1 YiAIa1)     (YiAIb2 YiAIa2)
		(YiAIIb1 YiAIIa1)   (YiAIIb2 YiAIIa2)
		(YiBIb1 YiBIa1)     (YiBIb2 YiBIa2)
		(YiBIIb1 YiBIIa1)   (YiBIIb2 YiBIIa2)
		(YiiAIb1 YiiAIa1)   (YiiAIb2 YiiAIa2)
		(YiiAIIb1 YiiAIIa1) (YiiAIIb2 YiiAIIa2)
		(YiiBIb1 YiiBIa1)   (YiiBIb2 YiiBIa2)
		(YiiBIIb1 YiiBIIa1) (YiiBIIb2 YiiBIIa2))
	(links u 
		(YiAIa1 YiAIIa1)   (YiAIa2 YiAIIa2)
		(YiAIb1 YiAIIb1)   (YiAIb2 YiAIIb2)
		(YiBIa1 YiBIIa1)   (YiBIa2 YiBIIa2)
		(YiBIb1 YiBIIb1)   (YiBIb2 YiBIIb2)
		(YiiAIa1 YiiAIIa1) (YiiAIa2 YiiAIIa2)
		(YiiAIb1 YiiAIIb1) (YiiAIb2 YiiAIIb2)
		(YiiBIa1 YiiBIIa1) (YiiBIa2 YiiBIIa2)
		(YiiBIb1 YiiBIIb1) (YiiBIb2 YiiBIIb2))
	(links d 
		(YiAIIa1 YiAIa1)   (YiAIIa2 YiAIa2)
		(YiAIIb1 YiAIb1)   (YiAIIb2 YiAIb2)
		(YiBIIa1 YiBIa1)   (YiBIIa2 YiBIa2)
		(YiBIIb1 YiBIb1)   (YiBIIb2 YiBIb2)
		(YiiAIIa1 YiiAIa1) (YiiAIIa2 YiiAIa2)
		(YiiAIIb1 YiiAIb1) (YiiAIIb2 YiiAIb2)
		(YiiBIIa1 YiiBIa1) (YiiBIIa2 YiiBIa2)
		(YiiBIIb1 YiiBIb1) (YiiBIIb2 YiiBIb2))
	(links 4e 
		(YiAIa1 YiBIa1)     (YiAIa2 YiBIa2)
		(YiAIb1 YiBIb1)     (YiAIb2 YiBIb2)
		(YiAIIa1 YiBIIa1)   (YiAIIa2 YiBIIa2)
		(YiAIIb1 YiBIIb1)   (YiAIIb2 YiBIIb2)
		(YiiAIa1 YiiBIa1)   (YiiAIa2 YiiBIa2)
		(YiiAIb1 YiiBIb1)   (YiiAIb2 YiiBIb2)
		(YiiAIIa1 YiiBIIa1) (YiiAIIa2 YiiBIIa2)
		(YiiAIIb1 YiiBIIb1) (YiiAIIb2 YiiBIIb2))
	(links 4w 
		(YiBIa1 YiAIa1)     (YiBIa2 YiAIa2)
		(YiBIb1 YiAIb1)     (YiBIb2 YiAIb2)
		(YiBIIa1 YiAIIa1)   (YiBIIa2 YiAIIa2)
		(YiBIIb1 YiAIIb1)   (YiBIIb2 YiAIIb2)
		(YiiBIa1 YiiAIa1)   (YiiBIa2 YiiAIa2)
		(YiiBIb1 YiiAIb1)   (YiiBIb2 YiiAIb2)
		(YiiBIIa1 YiiAIIa1) (YiiBIIa2 YiiAIIa2)
		(YiiBIIb1 YiiAIIb1) (YiiBIIb2 YiiAIIb2))
	(links 5u 
		(YiAIa1 YiiAIa1)   (YiAIa2 YiiAIa2)
		(YiAIb1 YiiAIb1)   (YiAIb2 YiiAIb2)
		(YiAIIa1 YiiAIIa1) (YiAIIa2 YiiAIIa2)
		(YiAIIb1 YiiAIIb1) (YiAIIb2 YiiAIIb2)
		(YiBIa1 YiiBIa1)   (YiBIa2 YiiBIa2)
		(YiBIb1 YiiBIb1)   (YiBIb2 YiiBIb2)
		(YiBIIa1 YiiBIIa1) (YiBIIa2 YiiBIIa2)
		(YiBIIb1 YiiBIIb1) (YiBIIb2 YiiBIIb2))
	(links 5d 
		(YiiAIa1 YiAIa1)   (YiiAIa2 YiAIa2)
		(YiiAIb1 YiAIb1)   (YiiAIb2 YiAIb2)
		(YiiAIIa1 YiAIIa1) (YiiAIIa2 YiAIIa2)
		(YiiAIIb1 YiAIIb1) (YiiAIIb2 YiAIIb2)
		(YiiBIa1 YiBIa1)   (YiiBIa2 YiBIa2)
		(YiiBIb1 YiBIb1)   (YiiBIb2 YiBIb2)
		(YiiBIIa1 YiBIIa1) (YiiBIIa2 YiBIIa2)
		(YiiBIIb1 YiBIIb1) (YiiBIIb2 YiBIIb2))
	(links 6e 
		(ZiAIa1 YiAIa1)     (ZiAIa2 YiAIa2)   
		(ZiAIb1 YiAIb1)     (ZiAIb2 YiAIb2)
		(ZiAIIa1 YiAIIa1)   (ZiAIIa2 YiAIIa2)  
		(ZiAIIb1 YiAIIb1)   (ZiAIIb2 YiAIIb2)
		(ZiBIa1 YiBIa1)     (ZiBIa2 YiBIa2)   
		(ZiBIb1 YiBIb1)     (ZiBIb2 YiBIb2)
		(ZiBIIa1 YiBIIa1)   (ZiBIIa2 YiBIIa2)  
		(ZiBIIb1 YiBIIb1)   (ZiBIIb2 YiBIIb2)
		(ZiiAIa1 YiiAIa1)   (ZiiAIa2 YiiAIa2)  
		(ZiiAIb1 YiiAIb1)   (ZiiAIb2 YiiAIb2)
		(ZiiAIIa1 YiiAIIa1) (ZiiAIIa2 YiiAIIa2) 
		(ZiiAIIb1 YiiAIIb1) (ZiiAIIb2 YiiAIIb2)
		(ZiiBIa1 YiiBIa1)   (ZiiBIa2 YiiBIa2)  
		(ZiiBIb1 YiiBIb1)   (ZiiBIb2 YiiBIb2)
		(ZiiBIIa1 YiiBIIa1) (ZiiBIIa2 YiiBIIa2) 
		(ZiiBIIb1 YiiBIIb1) (ZiiBIIb2 YiiBIIb2))
	(links 6w 
		(YiAIa1 ZiAIa1)     (YiAIa2 ZiAIa2)   
		(YiAIb1 ZiAIb1)     (YiAIb2 ZiAIb2)
		(YiAIIa1 ZiAIIa1)   (YiAIIa2 ZiAIIa2)  
		(YiAIIb1 ZiAIIb1)   (YiAIIb2 ZiAIIb2)
		(YiBIa1 ZiBIa1)     (YiBIa2 ZiBIa2)   
		(YiBIb1 ZiBIb1)     (YiBIb2 ZiBIb2)
		(YiBIIa1 ZiBIIa1)   (YiBIIa2 ZiBIIa2)  
		(YiBIIb1 ZiBIIb1)   (YiBIIb2 ZiBIIb2)
		(YiiAIa1 ZiiAIa1)   (YiiAIa2 ZiiAIa2)  
		(YiiAIb1 ZiiAIb1)   (YiiAIb2 ZiiAIb2)
		(YiiAIIa1 ZiiAIIa1) (YiiAIIa2 ZiiAIIa2) 
		(YiiAIIb1 ZiiAIIb1) (YiiAIIb2 ZiiAIIb2)
		(YiiBIa1 ZiiBIa1)   (YiiBIa2 ZiiBIa2)  
		(YiiBIb1 ZiiBIb1)   (YiiBIb2 ZiiBIb2)
		(YiiBIIa1 ZiiBIIa1) (YiiBIIa2 ZiiBIIa2) 
		(YiiBIIb1 ZiiBIIb1) (YiiBIIb2 ZiiBIIb2))
  	(symmetry Black (n s) (s n) (u d) (d u) 
		(4e 4w)   (4w 4e)   (5u 5d)   (5d 5u)
		(6e 6w)   (6w 6e))
  	(zone
     		(name promotion-zone)
     		(players White)
     		(positions YiiAIIa2 YiiAIIb2 YiiBIIa2 YiiBIIb2))
  	(zone
     		(name promotion-zone)
     		(players Black)
     		(positions ZiAIa1 ZiAIb1 ZiBIa1 ZiBIb1))
) 


Выглядит страшно? Зато работает!



Немного схитрив, можно определять гексагональные доски:



Это всё тот же двумерный grid, строки которого сдвинуты друг относительно друга
(define Board-Definitions
  (image "Images\6Chess.bmp")
  (grid
     (start-rectangle 21 -79 69 -31)
     (dimensions
	 ("a/b/c/d/e/f/g/h/i" (38 22)) ; files
	 ("10/9/8/7/6/5/4/3/2/1" (0 44)) ; ranks
     )
     (directions
       (z0 0 -1) (z1 1 -2) (z2 1 -1) (z3 2 -1) (z4 1 0) (z5 1 1)
       (z6 0 1) (z7 -1 2) (z8 -1 1) (z9 -2 1) (zx -1 0) (zy -1 -1)
     )
  )
  (kill-positions
     a7 a8 a9 a10 b8 b9 b10 c9 c10 d10
     f1 g1 g2 h1 h2 h3 i1 i2 i3 i4)
  (symmetry Black
    (z0 z6)(z6 z0) (z1 z7)(z7 z1) (z2 z8)(z8 z2)
    (z3 z9)(z9 z3) (zx z4)(z4 zx) (zy z5)(z5 zy)
  )
  (zone
     (name promotion-zone) (players White)
     (positions a6 b7 c8 d9 e10 f10 g10 h10 i10)
  )
  (zone
     (name promotion-zone) (players Black)
     (positions a1 b1 c1 d1 e1 f2 g3 h4 i5)
  )
  (zone
     (name fast-through) (players White)
     (positions b3 c3 d3 e3 f4 g5 h6)
  )
  (zone
     (name fast-through) (players Black)
     (positions b5 c6 d7 e8 f8 g8 h8)
  )
  (zone
     (name faster-through) (players White)
     (positions d4 e4 f5)
  )
  (zone
     (name faster-through) (players Black)
     (positions d6 e7 f7)
  )
)


Можно перемещать части доски, вместе с находящимися на них фигурами!



Это действительно просто.

На самом деле, здесь две доски
 (board
  (image "Images\PlatformChess\Void8x8.bmp")
  (grid
    (start-rectangle 10 10 48 48)
     (dimensions
         ("a/b/c/d/e/f/g/h" (49 0)) ; files
         ("8/7/6/5/4/3/2/1" (0 49)) ; ranks
     )
     (directions (n 0 -1) (e 1 0) (s 0 1) (w -1 0)
			     (ne 1 -1) (nw -1 -1) (se 1 1) (sw -1 1)
     )
  )
  (grid
    (start-rectangle 5 5 103 103)
    (dimensions
     ("A/B/C/D" (98 0)) ; grid files
     ("4/3/2/1" (0 98)) ;grid ranks
    )
    (directions (n 0 -1) (e 1 0) (w -1 0) (s 0 1)))
  (links up (A1 a2) (A2 a4) (A3 a6) (A4 a8)
            (B1 c2) (B2 c4) (B3 c6) (B4 c8)
            (C1 e2) (C2 e4) (C3 e6) (C4 e8)
            (D1 g2) (D2 g4) (D3 g6) (D4 g8))
  (links down (a1 A1) (a2 A1) (b1 A1) (b2 A1)
              (a3 A2) (a4 A2) (b3 A2) (b4 A2)
              (a5 A3) (a6 A3) (b5 A3) (b6 A3)
              (a7 A4) (a8 A4) (b7 A4) (b8 A4)

              (c1 B1) (c2 B1) (d1 B1) (d2 B1)
              (c3 B2) (c4 B2) (d3 B2) (d4 B2)
              (c5 B3) (c6 B3) (d5 B3) (d6 B3)
              (c7 B4) (c8 B4) (d7 B4) (d8 B4)

              (e1 C1) (e2 C1) (f1 C1) (f2 C1)
              (e3 C2) (e4 C2) (f3 C2) (f4 C2)
              (e5 C3) (e6 C3) (f5 C3) (f6 C3)
              (e7 C4) (e8 C4) (f7 C4) (f8 C4)

              (g1 D1) (g2 D1) (h1 D1) (h2 D1)
              (g3 D2) (g4 D2) (h3 D2) (h4 D2)
              (g5 D3) (g6 D3) (h5 D3) (h6 D3)
              (g7 D4) (g8 D4) (h7 D4) (h8 D4)
  )

  (symmetry Black (n s) (s n) (ne se) (se ne) (nw sw) (sw nw))

  (zone
     (name promotion-zone)
     (players White)
     (positions a8 b8 c8 d8 e8 f8 g8 h8)
  )
  (zone
     (name promotion-zone)
     (players Black)
     (positions a1 b1 c1 d1 e1 f1 g1 h1)
  )
  (zone
     (name enemy-promotion-zone)
     (players Black)
     (positions a8 b8 c8 d8 e8 f8 g8 h8)
  )
  (zone
     (name enemy-promotion-zone)
     (players White)
     (positions a1 b1 c1 d1 e1 f1 g1 h1)
  )
  (zone
     (name third-rank)
     (players White)
     (positions a3 b3 c3 d3 e3 f3 g3 h3
                a2 b2 c2 d2 e2 f2 g2 h2)
  )
  (zone
     (name third-rank)
     (players Black)
     (positions a6 b6 c6 d6 e6 f6 g6 h6
                a7 b7 c7 d7 e7 f7 g7 h7)
  )
 )


Традиционная доска 8x8 совмещена с доской 4x4, по которой перемещаются «части доски» — платформы. Дополнительные направления up и down помогают фигурам контролировать наличие «почвы под ногами». Сложнее всего обеспечить синхронное перемещение фигур вместе с платформами.

На помощь приходят операторы cascade и to
(define wplatform-move
 (mark $1 (verify empty?) to
    back up
    (if (and empty? (empty? e) (empty? s) (empty? se)) add
     else
       back up (if not-empty? cascade from $1 
                   (if (and (in-zone? promotion-zone $1)
                            (piece? Pawn (opposite $1))
                            (friend? (opposite $1))) $1 (change-type Queen) to
                    else $1 to))
       back up e (if not-empty? cascade from $1
                   (if (and (in-zone? promotion-zone $1)
                            (piece? Pawn (opposite $1))
                            (friend? (opposite $1))) $1 (change-type Queen) to
                    else $1 to))
       back up s (if not-empty? cascade from $1
                    (if (and (in-zone? enemy-promotion-zone $1)
                             (piece? Pawn (opposite $1))
                             (enemy? (opposite $1))) $1 (change-type Queen) to
                    else $1 to))
       back up se (if not-empty? cascade from $1
                    (if (and (in-zone? enemy-promotion-zone $1)
                             (piece? Pawn (opposite $1))
                             (enemy? (opposite $1))) $1 (change-type Queen) to
                    else $1 to))
       add
    )
 )
)


Можно подумать, что это предел возможностей ZoG, но посмотрите-ка вот на это:


Здесь явно что-то не так. Фигуры накладываются друг на друга! Разве ZoG так умеет? Конечно нет! Но кто сказал, что фигуры обязаны быть цельными? В этой игре, каждая «фигура» составлена из четырёх кусков, а доска, разумеется, трёхмерная (но видим мы лишь её «верхний план»). Реализация замечательная. Единственное, что может смутить — это то, что для выбора любой из фигур необходимо указывать её левый верхний угол (это можно исправить, при желании).

Можно пойти ещё дальше! С того самого момента, как я впервые увидел эту книгу, я загорелся желанием сделать доску для «Margo». Поскольку я реалист, я отдаю себе отчёт в том, что мне вряд ли удастся заставить AI ZoG правильно с ней работать. Даже для классической реализации Го на двумерной доске, ZoG понадобился engine, чтобы играть на уровне среднего новичка. В Margo, с её «зомби», «мостами» и «виртуальными группами», всё ещё сложнее. Полноценную игру можно реализовать в Axiom, но там разработка AI — отдельная история. Для начала, меня вполне устроит доска, контролирующая правила игры, поддерживающая игру двух человек и разбор этюдов. Начать следует с определения доски:

Доска ''Margo''
   (board
      (image "../images/margo/board.bmp")
      (grid
         (start-rectangle 30 30 59 59)
         (dimensions
             ("a/b/c/d/e/f/g/h/i/j/k/l/m/n" (30 0)) ; files
             ("14/13/12/11/10/9/8/7/6/5/4/3/2/1" (0 30)) ; ranks
             ("I/II/III/IV/V/VI/VII" (1600 0))  ;layers
         )
         (directions (n   0 -1 0) (e  1  0 0) (s  0  1 0) (w -1 0 0)
                     (nw -1 -1 0) (ne 1 -1 0) (sw -1 1 0) (se 1 1 0)
                     (u 0 0 -1) (d 0 0 1) 
         )
      )
      (zone (name im) (players R)
            (positions a1I)
      )
      (zone (name im) (players B)
            (positions a2I)
      )
      (zone (name empty-plane) (players R B)
            (positions a2I a4I a6I a8I a10I a12I a14I
                       c2I c4I c6I c8I c10I c12I c14I
                       e2I e4I e6I e8I e10I e12I e14I
                       g2I g4I g6I g8I g10I g12I g14I
                       i2I i4I i6I i8I i10I i12I i14I
                       k2I k4I k6I k8I k10I k12I k14I
                       m2I m4I m6I m8I m10I m12I m14I
            )
      )
      (zone (name plane) (players R B)
            (positions a1I a2I a3I a4I a5I a6I a7I a8I a9I a10I a11I a12I a13I a14I
                       b1I b2I b3I b4I b5I b6I b7I b8I b9I b10I b11I b12I b13I b14I
                       c1I c2I c3I c4I c5I c6I c7I c8I c9I c10I c11I c12I c13I c14I
                       d1I d2I d3I d4I d5I d6I d7I d8I d9I d10I d11I d12I d13I d14I
                       e1I e2I e3I e4I e5I e6I e7I e8I e9I e10I e11I e12I e13I e14I
                       f1I f2I f3I f4I f5I f6I f7I f8I f9I f10I f11I f12I f13I f14I
                       g1I g2I g3I g4I g5I g6I g7I g8I g9I g10I g11I g12I g13I g14I
                       h1I h2I h3I h4I h5I h6I h7I h8I h9I h10I h11I h12I h13I h14I
                       i1I i2I i3I i4I i5I i6I i7I i8I i9I i10I i11I i12I i13I i14I
                       j1I j2I j3I j4I j5I j6I j7I j8I j9I j10I j11I j12I j13I j14I
                       k1I k2I k3I k4I k5I k6I k7I k8I k9I k10I k11I k12I k13I k14I
                       l1I l2I l3I l4I l5I l6I l7I l8I l9I l10I l11I l12I l13I l14I
                       m1I m2I m3I m4I m5I m6I m7I m8I m9I m10I m11I m12I m13I m14I
                       n1I n2I n3I n4I n5I n6I n7I n8I n9I n10I n11I n12I n13I n14I
            )
      )
   )


Взяв за основу определение доски «Маджонга», я изменил графические ресурсы и размеры доски (для получения доски 7x7 необходимо определить grid 14x14 тайлов, с поддержкой 7 слоёв). Здесь стоит обратить внимание на определение игровых зон. Во первых, необходимо определить «верхний план» — зону plane, в которой будут расположены все видимые тайлы. Только в этой зоне возможно добавление новых фигур и с неё же должно начинаться их удаление. Далее, создание фигур на пустой доске возможно не в любом месте. Зона empty-plan определяет возможное расположение их левых верхних тайлов.

Что касается определения зоны "im" — это забавный трюк, который позволит в дальнейшем определять принадлежность фигур тому или иному игроку (а не просто их дружественность или враждебность). Этот макрос будет использоваться для получения полной информации о тайле, расположенном на текущей позиции (к сожалению, никаким более внятным способом реализовать аналогичный функционал в ZRF невозможно):

Получение информации о тайле
(define get-piece
   (set-flag is-piece? false)
   (set-flag is-black? false)
   (set-flag is-left?  false)
   (set-flag is-top?   false)
   (if not-empty?
       (set-flag is-piece? true)
       (if (and enemy? (in-zone? im a1I))
           (set-flag is-black? true)
       )
       (if (and friend? (in-zone? im a2I))
           (set-flag is-black? true)
       )
       (if (or (piece? tnw) (piece? tne))
           (set-flag is-top?   true)
       )
       (if (or (piece? tnw) (piece? tsw))
           (set-flag is-left?  true)
       )
   )
)


Задача размещения фигуры (четырёх тайлов) на пустую доску решается тривиально. Единственная техническая сложность заключается в том, что ход начинается сбросом другой специальной фигуры (не тайла), поскольку это единственная фигура, которую можно сбрасывать на любое поле. В точке сброса, её необходимо подменить на соответствующий тайл (это почти тоже самое, что делалось в «Huffing Checkers» в предыдущей статье). Остальные тайлы просто создаются на соседних полях (поскольку ход может начаться с любого тайла, необходимо определить 4 возможных хода):

Ход на пустую доску
(define pre-check-3
   (verify (on-board? $1))
   (verify (on-board? $2))
   (verify (in-zone? $3))
)

(define pre-check-4
   (verify (on-board? $1))
   (verify (on-board? $2))
   (verify (in-zone? $3 $4))
)

(define check-e
   (verify empty?)
   (verify (empty? $1))
   (verify (empty? $2))
   (verify (empty? $3))
)

(define post-action
   (create t-$1)
   (create t-$2 $2)
   (create t-$3 $4)
   (create t-$5 $6)
   add
)

(define de-nw (
   (pre-check-3 e s empty-plane)
   (check-e e s se)
   (post-action nw se ne e sw s)
))                          

(define de-ne (
   (pre-check-4 w s empty-plane w)
   (check-e w s sw)
   (post-action ne sw nw w se s)
))

(define de-se (
   (pre-check-4 w n empty-plane nw)
   (check-e n w nw)
   (post-action se nw ne n sw w)
))

(define de-sw (
   (pre-check-4 e n empty-plane n)
   (check-e e n ne)
   (post-action sw ne nw n se e)
))

(define tile
   (name  $1$2)
   (image G "../images/margo/w$1$2.bmp"
          R "../images/margo/b$1$2.bmp")
   (attribute is-ko? false)
)

(piece (tile t nw) )
(piece (tile t ne) )
(piece (tile t sw) )
(piece (tile t se) )

(piece
   (name  M)
   (image R "../images/margo/m.bmp"
          B "../images/margo/m.bmp")
   (drops (de-nw)
          (de-ne)
          (de-se)
          (de-sw)
   )
)


Ход, выстраивающий «пирамиду», более сложен. Необходимо убедиться, что все четыре тайла, на которых размещается новая фигура, имеют требуемый тип и глубина под ними одинакова. Далее, перед тем как размещать фигуру на доске в верхнем (видимом) слое, необходимо перенести ранее размещённые там тайлы в более глубокие слои. Первоначально, я выбрал не вполне удачное решение, используя слои со II по VII в качестве «стека», сверху-вниз:

Выстраивание пирамид
(define check-p
   (verify (piece? t-$1))
   (verify (piece? t-$2 $3))
   (verify (piece? t-$4 $5))
   (verify (piece? t-$6 $7))
)

(define check-d
   mark
   u
   (while not-empty?
       (verify (not-empty? $1))
       (verify (not-empty? $2))
       (verify (not-empty? $1$2))
       u
   )
   (verify (empty? $1))
   (verify (empty? $2))
   (verify (empty? $1$2))
   back
)

(define dp-nw (
   (pre-check-3 e s plane)
   (check-p se sw e nw se ne s)
   (check-d s e)
   (pre-action s e)
   (post-action nw se ne e sw s)
))

(define dp-ne (
   (pre-check-4 w s plane w)
   (check-p sw se w nw s ne sw)
   (check-d s w)
   (pre-action s w)
   (post-action ne sw nw w se s)
))

(define dp-se (
   (pre-check-4 w n plane nw)
   (check-p nw sw n ne w se nw)
   (check-d n w)
   (pre-action n w)
   (post-action se nw ne n sw w)
))

(define dp-sw (
   (pre-check-4 e n plane n)
   (check-p ne nw e se n sw ne)
   (check-d n e)
   (pre-action n e)
   (post-action sw ne nw n se e)
))

(piece
   (name  M)
   (image R "../images/margo/m.bmp"
          B "../images/margo/m.bmp")
   (drops (de-nw) (dp-nw)
          (de-ne) (dp-ne)
          (de-se) (dp-se)
          (de-sw) (dp-sw)
   )
)


Впоследствии, стало ясно, что гораздо удобнее держать все тайлы одной фигуры (кроме видимых) в одном слое. Стек пришлось "перевернуть" (для этого потребовалось создать дополнительное направление, соединяющее первый слой с седьмым). Следующим шагом стало удаление. Алгоритмы удаления фигур, в связи с их «смертью» в Го (и тем более в Margo) очень сложны. Я решил начать с простого — удаления любой фигуры простым щелчком мыши. Разумеется, чтобы вся конструкция не развалилась, тем же ходом необходимо удалить и все фигуры «опирающиеся» на удалённую (прямо или опосредованно). Само по себе удаление помеченных тайлов достаточно очевидно.

Групповое удаление тайлов
(define clean-piece
   (set-flag is-piece? false)
   d (while empty? d)
   (while (and not-empty? (position-flag? is-dead?))
        capture
        d
   )
   (if not-empty?
        (get-piece)
        capture
   )
   (while (not-in-zone? plane) u)
   (if (flag? is-piece?)
        (add-piece)
    else
        capture
   )
)

(define clean-pieces
   mark a0
   (while (on-board? next)
        next
        (if (and not-empty? (position-flag? is-dead?))
            (if (empty? b)
                 capture
             else
                 (clean-piece)
            )
        )
   )
   back
)


В главном цикле clean-pieces просматриваются все тайлы «верхнего плана». Если какой-то из них помечен для удаления, проверяются тайлы непосредственно под ним, в нижних слоях. Всё помеченное для удаления — удаляется, а первый не помеченный тайл перемещается наверх, в первый слой. Для пометки используются позиционные флаги, позволяющие связать с произвольной позицией булевское значение. Осталось придумать, как помечать тайлы для удаления.

Пометка тайлов для удаления
(define check-top
   (verify (on-board? $1))
   (if (and (not-empty? $1) (not-position-flag? is-dead? $1) (not-position-flag? is-current? $1))
        $1 
        (set-position-flag is-current? true)
        (if (not-piece? t$3)
            d (while (and empty? (on-board? d)) d)
            (verify not-empty?)
            (if (and not-empty? (not-position-flag? is-dead?) (not-position-flag? is-current?))
                (set-position-flag is-current? true)
            )
            (while (not-in-zone? plane) u)
        )
        $2
   )
)

(define check-deep
   (verify (on-board? $1))
   (if (and (not-empty? $1) (not-position-flag? is-dead? $1) (not-position-flag? is-current? $1))
       $1
       (set-position-flag is-current? true)
       (verify (on-board? u))
       (if (and (not-empty? u) (not-position-flag? is-dead? u) (not-position-flag? is-current? u))
           u (set-position-flag is-current? true) d
       )
       $2
   )
)

(define proceed-current
   (set-position-flag is-dead? true)
   (set-position-flag is-current? false)
   (get-piece)
   (if (in-zone? plane)
       (if (flag? is-left?)
           (if (flag? is-top?)
               (check-top e  w  ne)
               (check-top s  n  sw)
               (check-top se nw se)
            else
               (check-top e  w  se)
               (check-top n  s  nw)
               (check-top ne sw ne)
           )
        else
           (if (flag? is-top?)
               (check-top w  e  nw)
               (check-top s  n  se)
               (check-top sw ne sw)
            else
               (check-top w  e  sw)
               (check-top n  s  ne)
               (check-top nw se nw)
           )
       )
    else
       (if (flag? is-left?)
           (if (flag? is-top?)
               (check-deep e  w)
               (check-deep s  n)
               (check-deep se nw)
            else
               (check-deep e  w)
               (check-deep n  s)
               (check-deep ne sw)
           )
        else
           (if (flag? is-top?)
               (check-deep w  e)
               (check-deep s  n)
               (check-deep sw ne)
            else
               (check-deep w  e)
               (check-deep n  s)
               (check-deep nw se)
           )
       )
   )
)

(define proceed-all
   (set-flag is-done? false)
   (while (not-flag? is-done?)
        (set-flag is-done? true)
        mark a0
        (while (on-board? next-all)
             next-all
             (if (position-flag? is-current?)
                 (set-flag is-done? false)
                 (proceed-current)
             )
        )
        back
   )
)

(define dp-clear (
   (verify not-empty?)
   (set-position-flag is-current? true)
   (proceed-all)
   (clean-pieces)
   add
))
...
(piece
   (name  M)
   (image G "../images/margo/m.bmp"
          R "../images/margo/m.bmp")
   (drops (de-nw) (dp-nw)
          (de-ne) (dp-ne)
          (de-se) (dp-se)
          (de-sw) (dp-sw)

          (dp-clear)
   )
)


Здесь, важно понимать, что удаление всегда начинается с видимых тайлов, расположенных в первом слое. Установив начальную пометку в dp-clear, мы выполняем proceed-current до тех пор, пока встречаются новые, ещё не удалённые тайлы. Сама эта процедура выполняет «распространение» флагов на соседние тайлы и делает это по разному, в зависимости от того, происходит ли действие в верхнем слое или на глубине.

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



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



Разумеется, для того чтобы превратить эту заготовку в полноценную игру «Margo», мне придётся ещё очень много поработать. Скорее всего, мне всё-таки придётся переделать всё на Axiom, а это само по себе целая история. В Axiom нет атрибутов фигур, игровых зон и позиционных флагов, но, в плане разработки (в том числе AI) её возможности неизмеримо превосходят ZRF. Кроме того, код на ForthScript можно худо-бедно отлаживать. Когда (и если) я справлюсь со всеми трудностями, это будет заслуживать отдельной статьи, сегодня же я хотел показать лишь то, что при помощи ZRF можно делать действительно удивительные вещи. Достаточно проявить немного изобретательности и не бояться (очень много) поработать руками.

Комментарии (6)


  1. datacompboy
    08.06.2015 14:59

    Даже и не знаю. А это точно проще, чем расширить движок?


    1. GlukKazan Автор
      08.06.2015 15:00
      +3

      Одна задача другой стоит. Но я плавно подхожу к этой теме.
      Margo на Axiom думаю всё таки сделаю. Будет весело.


      1. morgreek
        09.06.2015 07:17

        Шахматы на платформе… тут должна быть фраза «теперь я видел всё», но, чую, это ещё далеко не так)

        Margo будет отдельной статьёй, не входящей в в цикл «пинков»?


        1. GlukKazan Автор
          09.06.2015 08:43

          Безусловно да. Возможно на geektimes (в этом случае, там будет больше про саму игру и меньше технических подробностей).
          Не уверен, впрочем, что это будет скоро. Там действительно нужно очень много сделать, а впереди отпуск.


  1. Vitter
    09.06.2015 02:30

    Код становится всё монструозней и монструозней.
    А нам надо всё проще и проще!


    1. GlukKazan Автор
      09.06.2015 08:45

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