Anatomy of melancholy

  • Tutorial
Knowledge is power.

       Francis Bacon.

... in much wisdom there is much sadness;
    and whoever multiplies knowledge, multiplies sorrow.

       Book of Ecclesiastes.

Games live their own lives. They arise from nowhere, develop, give rise to new games, are forgotten by everyone and, sometimes, come back from oblivion. There are many examples of games that have been defeated in this process of natural selection. These are the various variations of Shogi that have survived only thanks to the reverent attitude of the people of Japan to their cultural heritage. A game party like Taikyoku shogi could drag on for months (if not years). But these Heian- era chess dinosaursare not the most prominent representatives of the "fossil" world of board games.

I want to talk about a truly amazing game. The threats in it are not obvious, and the goals are not trivial. You can win in many ways, but playing is not at all easy. It cannot be attributed either to the family of Checkers or to the chess family. Its origin is foggy. For the average layman, this game is about as exciting as composing magic squares . It fully justifies one of its names - only philosophers can play it .

For the first time, Rithmomachia , also known as the “Battle of the Numbers” or “The Game of the Philosophers”, was described around 1030 by a monk named Asilo. Authorship, apparently unfounded, is attributedПлатону, а правила игры основаны на арифметической теории Боэция. Впоследствии, правила игры были незначительно изменены другим монахом, по имени Hermannus Contractus, добавившим примечания, посвященные теории музыки. Продолжительное время Ритмомахия использовалась в качестве учебного пособия, при обучении студентов математике. Интеллектуалы того времени играли в Ритмомахию для удовольствия (одно время она была даже более популярна чем Шахматы), Роберт Бёртон упоминал её в "Анатомии меланхолии", а Томас Мор считал эту игру хорошим досугом для обитателей своей "Утопии". Then, suddenly, it was over. The interests of mathematics and Rhythmachia diverged and the game was forgotten. Of course, this does not mean that we cannot remember it.

Rules of the game


Two players are playing Rhythm Machia on a rectangular 8x16 square board. The moves are made in turn. Each move moves one piece. As a result of the move, one or more pieces of the opponent can be taken (removed from the board). The following types of shapes are defined:

  • A circle
  • Triangle
  • Square
  • Pyramid



Ignoring the numerical marks on the figures (I will talk about them below), you can see that two types of moves are allowed. In the first case, the figure moves in a straight line and can be stopped by any other figure (its own or the enemy) that is in the way. Thus, the Circle moves (one cell diagonally), as well as the Triangle (strictly two cells along the orthogonal) and the Square (strictly three cells along the orthogonal). Another possibility is to “jump” the pieces onto the target square, similar to the move of the Knight in Chess. So the Triangle or the Square can walk. Any move should be performed only on an empty cell.

A unique figure is the Pyramid. Actually this is not a figure, but a set of figures. Each piece in the set (Circle, Triangle or Square) allows the Pyramid to perform the corresponding type of move. The figure above shows the possible moves of the Pyramid in the "maximum configuration". As you might guess, by the end of the game, “completeness” can be severely violated (by the way, the Pyramid from Rhythmachia is the only figure I know that can be “killed in parts”).

I will dwell on the methods of "killing". In Rhythmachia there are four of them:

  • Capture by siege
  • Capture by equality
  • Capture by ambush
  • Capture by eruption



The most radical is the siege of the figure. The figure will be removed if after the next move it is blocked in all diagonal or orthogonal directions. If the figure is located at the edge of the board or in the corner, you will have to block fewer directions. This is the simplest (but not the only) way to take the Pyramid all at once - as a whole. The siege is the only way that does not use the numerical values ​​of the figures.

Another way is to take a figure with a matching numerical value. If, after completing the move, on the field where the piece could move the next move (if this field was empty, of course), the opponent’s piece is located, with the same numerical value, it will be taken. It should be noted that this method of combat may be asymmetric. So (in the figure above), after White’s move, the Circle, with a numerical value of 16, can take a black Triangle with the same value, but the Triangle, in its turn, cannot do this, since it goes to other cells. At the same time, white and black triangles with numerical values ​​of 25 beat each other symmetrically (then which figure will be shot is determined by the order of the move).

An improved version of the previous is the following method. This time, "out of ambush", two figures are attacking. If the sum, difference, product or quotient of their numerical values ​​coincides with the numerical value on the opponent’s figure, the figure is removed. This is the most frequently used feature of the battle of figures in the game, but to see all such threats, in a more or less difficult position, can be quite difficult.

The latter method allows you to beat figures at a long distance. If the result of the work or the private numerical value of the figure and the distance to the opponent’s figure (orthogonal) coincides with the numerical value of the opponent’s figure, the figure is removed. Other figures located in the direction of the battle do not impede the threat. When calculating the distance, the start and end positions are taken into account. This is a symmetrical method of combat (since it does not depend on the rules for moving figures). The sequence of the move determines which of the figures will be taken.

It remains to talk about how these rules apply to the Pyramids. With this, everything is simple - in any calculation for taking figures, the Pyramid can act either as an independent figure (its numerical value coincides with the sum of the numerical values ​​of all its components), or as any of its parts. Similarly, the Pyramid can be taken as a whole (by its total value), or in parts (each of its components individually is vulnerable). Given the rules for moving the Pyramid, which I mentioned above, this makes the Pyramid the strongest (and most vulnerable) figure.



The initial arrangement of figures is associated with another important feature of the game. Although the set of figures is the same, the numerical values ​​vary. Also, the "complete set" of the pyramids is different (shown on the side of the board, for each of the players). This makes the game asymmetric. Tactics and strategies for the black player are not suitable for white (and vice versa). This makes the game more interesting.

Glorious victories


The rules described above are enough to start playing Rhythmachy, but if the goal of the game was to simply capture all the opponent’s pieces, the game would not be so interesting. Yes, you can win in this way, but this is not the only (and not the best) way to win! I really like games, the purpose of which is not a straightforward "killing" of enemy figures. So in Chess, to win, it is not at all necessary to “eat” all the pieces, it is enough to check the King! In some versions of Hasami Shogi , the goal of the game is even more unexpected (to win, you need to build a line of 5 of your chips “in a row”). Rhythmachy does not disappoint in this regard either. Here is a list of ways to win this game (in increasing order of their “fame”):

  • De Corpore ("by body"): Get a given (or more) number of enemy pieces (usually 15)
  • De Bonis (“by goods”): Take the opponent’s pieces with a given (or large) total value (usually 1315 for white and 984 for black)
  • De Lite (“by lawsuit”): Take enemy figures with a given (or large) total value, provided that the number of digits on the captured figures does not exceed a given
  • De Honore ("by honor"): Take enemy pieces with a given (or large) total value, provided that the number of captured pieces does not exceed a given
  • De Honore Liteque (“by honor and lawsuit”): Take enemy figures with a given (or large) total value, provided that the number of captured figures does not exceed the specified and the number of digits on the captured figures does not exceed the specified
  • Victoria Magna ("great victory"): Arrange three pieces (on enemy territory) in arithmetic progression
  • Victoria Major (“greater victory”): Position four figures (on the territory of the enemy) so that they have two (but no more) groups of three figures placed in different types of progression ( arithmetic , geometric or harmonic )
  • Victoria Excellentissima ("most excellent victory"): Position (on the territory of the enemy) four pieces so that there are all three types of progression

Here you can distinguish two fundamentally different types of goals of the game. Common Victories are associated with the capture of enemy figures (with the possible limitation of the number of captured figures, in order to protect against aggressive play). Proper Victories are considered a more worthy conclusion and are associated with the restoration of a certain “harmony” on the territory of the enemy (which does not exclude the possibility of the battle of enemy figures).



The examples above illustrate that enemy figures can (and should) be involved. In the first case, an arithmetic progression is constructed (16, 36, 56). The second example combines geometric (4, 12, 36) and harmonic (4, 6, 12) progressions. In the third case, all three types of progression are present (you yourself can find them). In case there are difficulties with verbal counting, tables of all possible solutions in Rhythmachy have been built (however, Rhythmachy is unlikely to be of interest to people with problems with verbal counting).

Possible options


During its heyday, Rhythmachy has already undergone many changes. Subsequent renovations did not improve the situation. There are many differences in the description of the rules of this game. Some sources give alternative options for the initial arrangement of pieces (I even came across an option that describes the game on an 8 x 14 board):



There are options with simplified rules for moving pieces. According to these rules, figures move in a straight line (orthogonal and diagonal) by a given number of cells. Any figure, in the way of movement, stops moving. There are no jumps, like a Knight's move in Chess. The pyramid, as usual, combines the moves of the figures present in its set.



In addition, there is an option with the usual rules for the movement of figures, in which a special Pyramid move is added to 3 cells diagonally. Such a move is possible provided that not one of the figures originally included in the Pyramid is taken. With the safety of the initial set of figures of the Pyramid, another option is associated. It is allowed to use the total value of the Pyramid, only on condition that none of its figures has not yet been "eaten". There are simpler options in which the Pyramid is completely removed if any of its components is attacked.

Some of the options are related to changes in the rules for taking figures. So, in one of the options for taking a piece " by Siege" (осада), необходимо блокировать все направления её «естественного» перемещения. При этом, не обязательно располагать свои фигуры вплотную к блокируемой (примеры ниже иллюстрируют концепцию). Это довольно интересное правило.



Есть вариант, в котором взятие "by Ambush" (засада) осуществляется подобно играм «зажимного» типа (при этом, правила перемещения фигур игнорируются):



Имеется большое количество расхождений в определениях "Glorious Victories". Часто, в условия таких побед, включается предварительное уничтожение Пирамиды противника. Также, существует условие завершения игры, ограничивающееся взятием Пирамиды. Вариантов этой игры много. Ознакомление с нюансами правил, перед началом игры, в любом случае, будет совсем не лишним.

Details for the curious


The implementation of the game naturally divided into several stages. The easiest way was to implement moving shapes. This is where I started. Here's what the Squares move looks like:

Moving Squares
: leap-n ( 'leap-dir 'shift-dir count ? -- )
	IF
		BEGIN
			OVER EXECUTE IF
				on-board? IF
					1- DUP 0> IF
						FALSE
					ELSE
						2DROP TRUE TRUE
					ENDIF
				ELSE
					2DROP FALSE TRUE
				ENDIF
			ELSE
				2DROP FALSE TRUE
			ENDIF
		UNTIL
		IF
			EXECUTE IF
				on-board? empty? AND IF
					from
					here DUP last-position !
					move
					capture-all
					add-move
				ENDIF
			ENDIF
		ELSE
			DROP
		ENDIF
	ELSE
		2DROP DROP
	ENDIF
;
: shift-n ( 'shift-dir count ? -- )
	IF
		BEGIN
			OVER EXECUTE IF
				on-board? empty? AND IF
					1- DUP 0> IF
						FALSE
					ELSE
						2DROP TRUE TRUE
					ENDIF
				ELSE
					2DROP FALSE TRUE
				ENDIF
			ELSE
				2DROP FALSE TRUE
			ENDIF
		UNTIL
		IF
			from
			here DUP last-position !
			move
			capture-all
			add-move
		ENDIF
	ELSE
		2DROP
	ENDIF
;
: s-move-n ( -- ) ['] North 3 TRUE shift-n ;
: s-move-s ( -- ) ['] South 3 TRUE shift-n ;
: s-move-w ( -- ) ['] West  3 TRUE shift-n ;
: s-move-e ( -- ) ['] East  3 TRUE shift-n ;
: s-move-nw ( -- ) ['] Northwest ['] North 2 TRUE leap-n ;
: s-move-ne ( -- ) ['] Northeast ['] North 2 TRUE leap-n ;
: s-move-sw ( -- ) ['] Southwest ['] South 2 TRUE leap-n ;
: s-move-se ( -- ) ['] Southeast ['] South 2 TRUE leap-n ;
: s-move-wn ( -- ) ['] Northwest ['] West  2 TRUE leap-n ;
: s-move-ws ( -- ) ['] Southwest ['] West  2 TRUE leap-n ;
: s-move-en ( -- ) ['] Northeast ['] East  2 TRUE leap-n ;
: s-move-es ( -- ) ['] Southeast ['] East  2 TRUE leap-n ;
{moves s-moves
	{move} s-move-n
	{move} s-move-s
	{move} s-move-w
	{move} s-move-e
	{move} s-move-nw
	{move} s-move-ne
	{move} s-move-sw
	{move} s-move-se
	{move} s-move-wn
	{move} s-move-ws
	{move} s-move-en
	{move} s-move-es
moves}


Some difficulty arose with the Pyramids, since their set of moves is determined by what pieces are included in them at the moment. I had to describe all possible moves for them, and when making a move, check for the presence of the corresponding figure in the set:

Moving Pyramids
: is-correct-type? ( piece-type -- ? )
	not-empty? IF
		piece-type PYRAMID > IF
			here SWAP a1 to
			BEGIN
				friend-p IF
					DUP is-piece-type? IF
						DROP TRUE TRUE
					ELSE
						FALSE
					ENDIF
				ELSE
					DROP FALSE TRUE
				ENDIF
			UNTIL
			SWAP to
		ELSE
			DROP FALSE
		ENDIF
	ELSE
		DROP FALSE
	ENDIF
;
: pr-move-ne ( -- ) ['] Northeast ROUND is-correct-type? leap-0 ;
: pr-move-se ( -- ) ['] Southeast ROUND is-correct-type? leap-0 ;
: pr-move-nw ( -- ) ['] Northwest ROUND is-correct-type? leap-0 ;
: pr-move-sw ( -- ) ['] Southwest ROUND is-correct-type? leap-0 ;
{moves p-moves
	{move} pr-move-ne
	{move} pr-move-se
	{move} pr-move-nw
	{move} pr-move-sw
	...
moves}


The implementation of the battle of figures turned into a real "race for survival." It was required to implement four completely different (and very non-trivial) methods of capture. At the same time, I wanted to limit the number of field views in order to minimize the overhead of calculations (the move is calculated disgracefully long already). As a result, I settled on the idea of ​​pre-filling several arrays, with the following checks of the conditions of the battle of figures:

Battle methods
MAXV []		attacking-values[]
MAXS []		current-positions[]
MAXS []		current-values[]
MAXS []		eruption-values[]
: fill-current ( pos -- )
	1 current-count !
	DUP 0 current-positions[] !
	DUP piece-type-at PYRAMID > IF
		a1 to 0
		BEGIN
			enemy-p IF
				not-empty? IF
					piece piece-value
					current-count @ MAXS < IF
						here current-count @ current-positions[] !
						DUP  current-count @ current-values[] !
						current-count ++
					ENDIF
					+
				ENDIF
				FALSE
			ELSE
				TRUE
			ENDIF
		UNTIL
	ELSE
		DUP piece-at piece-value
	ENDIF
	0 current-values[] !
	to
;
: get-eruption-values ( n -- )
	0 sum-value !
	PYRAMID is-piece-type? IF
		here a1 to
		BEGIN
			friend-p IF
				not-empty? eruption-count @ MAXE < AND IF
					OVER piece piece-value 
					DUP sum-value @ + sum-value !
					*
					eruption-count @ eruption-values[] !
					eruption-count ++
				ENDIF				         
				FALSE
			ELSE
				TRUE
			ENDIF
		UNTIL
		to
		sum-value @
	ELSE
		piece piece-value
	ENDIF
	eruption-count @ MAXE < IF
		*
		eruption-count @ eruption-values[] !
		eruption-count ++
	ELSE
		2DROP
	ENDIF
;
: get-attacking-values ( piece-type -- )
	0 sum-value    !
	FALSE sum-flag !
	PYRAMID is-piece-type? IF
		here a1 to
		BEGIN
			friend-p IF
				not-empty? attacking-count @ MAXV < AND IF
					piece piece-value
					sum-value @ + sum-value !
					OVER is-piece-type? IF
						TRUE sum-flag !
						piece piece-value
						attacking-count @ attacking-values[] !
						attacking-count ++
					ELSE
					ENDIF
				ENDIF				         
				FALSE
			ELSE
				TRUE
			ENDIF
		UNTIL
		to DROP
		sum-flag @ attacking-count @ MAXV < AND IF
			sum-value @
			attacking-count @ attacking-values[] !
			attacking-count ++
		ENDIF
	ELSE
		is-piece-type? attacking-count @ MAXV < AND IF
			piece piece-value
			attacking-count @ attacking-values[] !
			attacking-count ++
		ENDIF
	ENDIF
;
: check-siege-od ( 'dir -- )
	EXECUTE IF
		predict-move
		on-board? NOT friend? OR IF
			siege-counter --
		ENDIF
		on-board? friend? AND IF
			2 get-eruption-values
		ENDIF
		to
	ELSE
		siege-counter --
	ENDIF
;
: check-siege-dd ( 'dir -- )
	EXECUTE IF
		predict-move
		on-board? NOT friend? OR IF
			siege-counter --
		ENDIF
		on-board? friend? AND IF
			ROUND get-attacking-values
			ROUND check-equality-piece
		ENDIF
		to
	ELSE
		siege-counter --
	ENDIF
;
: check-siege ( pos -- )
	4 siege-counter !
	DUP to ['] North check-siege-od
	DUP to ['] South check-siege-od
	DUP to ['] West  check-siege-od
	DUP to ['] East  check-siege-od
	siege-counter @ 0= IF
		TRUE is-captured? !
	ENDIF
	4 siege-counter !
	DUP to ['] Northeast check-siege-dd
	DUP to ['] Southeast check-siege-dd
	DUP to ['] Northwest check-siege-dd
	DUP to ['] Southwest check-siege-dd
	siege-counter @ 0= IF
		TRUE is-captured? !
	ENDIF
	to
;
: check-equality-dd ( 'second-dir count 'first-dir -- )
	EXECUTE on-board? AND IF
		BEGIN
			1- DUP 0< IF
				TRUE			
			ELSE
				OVER EXECUTE on-board? AND IF
					predict-move
					friend? IF
						OVER count-to-piece-type
						DUP get-attacking-values
						check-equality-piece
					ENDIF
					to
	                                FALSE
				ELSE
					TRUE
				ENDIF
			ENDIF
		UNTIL
		2DROP
	ELSE
		2DROP
	ENDIF
;
: check-equality-od ( 'second-dir count 'first-dir -- )
	EXECUTE on-board? AND empty? AND IF
		BEGIN
			1- DUP 0< IF
				TRUE			
			ELSE
				OVER EXECUTE on-board? AND IF
					predict-move
					friend? IF
						OVER count-to-factor get-eruption-values
						OVER count-to-piece-type
						DUP get-attacking-values
						check-equality-piece
						TRUE
					ELSE
						not-empty?
					ENDIF
					SWAP to
				ELSE
					TRUE
				ENDIF
			ENDIF
		UNTIL
		2DROP
	ELSE
		2DROP
	ENDIF
;
: check-equality ( pos -- )
	DUP to ['] North 2 ['] North     check-equality-od
	DUP to ['] North 2 ['] Northwest check-equality-dd
	DUP to ['] North 2 ['] Northeast check-equality-dd
	DUP to ['] South 2 ['] South     check-equality-od
	DUP to ['] South 2 ['] Southwest check-equality-dd
	DUP to ['] South 2 ['] Southeast check-equality-dd
	DUP to ['] West  2 ['] West      check-equality-od
	DUP to ['] West  2 ['] Northwest check-equality-dd
	DUP to ['] West  2 ['] Southwest check-equality-dd
	DUP to ['] East  2 ['] East      check-equality-od
	DUP to ['] East  2 ['] Northeast check-equality-dd
	DUP to ['] East  2 ['] Southeast check-equality-dd
	to
;
: check-ambush-prod ( value -- ? )
	value-1 @ value-2 @ * OVER = IF
		DROP TRUE
	ELSE
		DUP value-1 @ * value-2 @ = IF
			DROP TRUE
		ELSE
			value-2 @ * value-1 @ = IF
				TRUE
			ELSE
				FALSE
			ENDIF
		ENDIF
	ENDIF
;
: check-ambush-cond ( value -- ? )
	value-1 @ value-2 @ + OVER = IF
		DROP TRUE
	ELSE
		DUP value-1 @ + value-2 @ = IF
			DROP TRUE
		ELSE
			DUP value-2 @ + value-1 @ = IF
				DROP TRUE
			ELSE
				check-ambush-prod
			ENDIF
		ENDIF
	ENDIF
;
: check-ambush-pair ( -- )
	current-count @
	BEGIN
		1-
		DUP current-positions[] @ 0< NOT IF
			DUP current-values[] @ check-ambush-cond IF
				DUP 0> IF
					DUP current-positions[] @ 
					DUP enemy-at? IF
						DUP ChangePieces
						capture-at
					ELSE
						DROP
					ENDIF
					-1 OVER current-positions[] !
				ELSE
					TRUE is-captured? !
				ENDIF
			ENDIF
		ENDIF
		DUP 0> NOT
	UNTIL
	DROP
;
: check-ambush ( -- )
	attacking-count @
	BEGIN
		1-
		attacking-count @
		BEGIN
			1-
			2DUP < IF
				2DUP 
				attacking-values[] @ value-1 !
				attacking-values[] @ value-2 !
				check-ambush-pair
			ENDIF
			DUP 0> NOT
		UNTIL
		DROP
		DUP 0> NOT
	UNTIL
	DROP
;
: fill-eruption-values ( 'dir pos n -- )
	value-1 ! to 1
	BEGIN
		1+ OVER EXECUTE IF
			predict-move
			OVER value-1 @ > on-board? friend? AND AND IF
				OVER get-eruption-values
			ENDIF
			to
			FALSE
		ELSE
			TRUE
		ENDIF
	UNTIL
	2DROP
;
: check-eruption-pair ( -- )
	current-count @
	BEGIN
		1-
		DUP current-positions[] @ 0< NOT IF
			DUP current-values[] @ value-1 @ = IF
				DUP 0> IF
					DUP current-positions[] @ 
					DUP enemy-at? IF
						DUP ChangePieces
						capture-at
					ELSE
						DROP
					ENDIF
					-1 OVER current-positions[] !
				ELSE
					TRUE is-captured? !
				ENDIF
			ENDIF
		ENDIF
		DUP 0> NOT
	UNTIL
	DROP
;
: check-eruption-values ( -- )
	eruption-count @
	BEGIN
		1-
		DUP eruption-values[] @ value-1 !
		check-eruption-pair
		DUP 0> NOT
	UNTIL
	DROP
;
: check-eruption ( pos -- )
	['] North OVER 4 fill-eruption-values
	['] South OVER 4 fill-eruption-values
	['] West  OVER 4 fill-eruption-values
	['] East  OVER 4 fill-eruption-values
	to
	check-eruption-values
;
: capture-all ( -- )
	here ROWS COLS *
	BEGIN
		1-
		DUP on-board-at? OVER enemy-at? AND IF
			0 attacking-count  !
			0 eruption-count   !
			FALSE is-captured? !
			DUP fill-current
			DUP check-siege
 			is-captured? @ NOT IF
				DUP check-equality
			ENDIF
			is-captured? @ NOT IF
				check-ambush
			ENDIF
			is-captured? @ NOT IF
				DUP check-eruption
			ENDIF
			is-captured? @ IF
				capture-piece
			ENDIF
		ENDIF
		DUP 0> NOT
	UNTIL
	DROP to
;


The capture of figures in the Pyramid, as well as the movement, had to be handled in a special way:

Taking figures
: capture-piece ( -- )
	current-count @
	BEGIN
		1- DUP 0> IF
			DUP current-positions[] @
			DUP 0< NOT IF
				DUP enemy-at? IF
					DUP ChangePieces
					capture-at
				ELSE
					DROP
				ENDIF
			ELSE
				DROP
			ENDIF
			FALSE
		ELSE
			DUP current-positions[] @
			DUP enemy-at? IF
				DUP ChangePieces
				capture-at
			ELSE
				DROP
			ENDIF
			TRUE
		ENDIF
	UNTIL
	DROP
;


Already in the process of debugging, I realized that all checks are performed at the time before the move. I had to write a small function that simulates the movement of a piece (this is not a very complete recalculation of the position on the board, but I managed to get by with a little blood):

Position change
: predict-move ( -- pos )
	here 
	DUP from = IF
		last-position @ to
	ELSE
		DUP last-position @ = IF
			from to
		ENDIF
	ENDIF
;


Against the background of all these horrors, checking the conditions for ending the game seemed a trivial task. There was some Axiom magic, but there is nothing that I’m afraid to show my parents:

Completion check
15	CONSTANT	WINC
1315	CONSTANT	WINW
984	CONSTANT	WINB
: WhitePieces++ ( -- ) WhitePieces ++ ;
: BlackPieces++ ( -- ) BlackPieces ++ ;
: WhiteValues++ ( -- ) WhiteValues ++ ;
: BlackValues++ ( -- ) BlackValues ++ ;
: ChangePieces ( pos -- )
	DUP piece-at piece-value SWAP
	player-at White = IF
		COMPILE WhitePieces++
		BEGIN
			1-
			COMPILE WhiteValues++
			DUP 0> NOT
		UNTIL
		DROP
	ELSE
		COMPILE BlackPieces++
		BEGIN
			1-
			COMPILE BlackValues++
			DUP 0> NOT
		UNTIL
		DROP
	ENDIF
;
: OnIsGameOver ( -- gameResult )
	#UnknownScore
	current-player White = IF
		WhitePieces @ WINC >= IF
			DROP
			#LossScore
		ENDIF
		WhiteValues @ WINW >= IF
			DROP
			#LossScore
		ENDIF
	ENDIF
	current-player Black = IF
		BlackPieces @ WINC >= IF
			DROP
			#LossScore
		ENDIF
		BlackValues @ WINB >= IF
			DROP
			#LossScore
		ENDIF
	ENDIF
;


From this moment, the program could already play (though it did it rather passively). The fact is that, in the absence of an evaluation function (and a custom implementation of AI), Axiom tries to perform a complete exhaustive search, to the terminal position. It is clear that the end of the game is far beyond the horizon of the possible depth of search, as a result of which the moves found do not differ in particular meaningfulness. In general, it remains to add AI teeth:

Evaluation function
: OnEvaluate ( -- score )
	current-player material-balance
;


Here we used the very convenient material-balance function (provided by Axiom), which uses the weight values ​​given to the figures (the same values ​​were used to implement the Rhythmachy rules):

Description of figures
{pieces
	{piece}		R0	{moves} r-moves	0   {value}
	{piece}		R1	{moves} r-moves	1   {value}
	{piece}		R2	{moves} r-moves	2   {value}
	{piece}		R3	{moves} r-moves	3   {value}
	{piece}		R4	{moves} r-moves	4   {value}
	{piece}		R5	{moves} r-moves	5   {value}
	{piece}		R6	{moves} r-moves	6   {value}
	{piece}		R7	{moves} r-moves	7   {value}
	{piece}		R8	{moves} r-moves	8   {value}
	{piece}		R9	{moves} r-moves	9   {value}
	{piece}		R16	{moves} r-moves	16  {value}
	{piece}		R25	{moves} r-moves	25  {value}
	{piece}		R36	{moves} r-moves	36  {value}
	{piece}		R49	{moves} r-moves	49  {value}
	{piece}		R64	{moves} r-moves	64  {value}
	{piece}		R81	{moves} r-moves	81  {value}
	{piece}		T0	{moves} t-moves	0   {value}
	{piece}		T6	{moves} t-moves	6   {value}
	{piece}		T9	{moves} t-moves	9   {value}
	{piece}		T12	{moves} t-moves	12  {value}
	{piece}		T16	{moves} t-moves	16  {value}
	{piece}		T20	{moves} t-moves	20  {value}
	{piece}		T25	{moves} t-moves	25  {value}
	{piece}		T30	{moves} t-moves	30  {value}
	{piece}		T36	{moves} t-moves	36  {value}
	{piece}		T42	{moves} t-moves	42  {value}
	{piece}		T49	{moves} t-moves	49  {value}
	{piece}		T56	{moves} t-moves	56  {value}
	{piece}		T64	{moves} t-moves	64  {value}
	{piece}		T72	{moves} t-moves	72  {value}
	{piece}		T81	{moves} t-moves	81  {value}
	{piece}		T90	{moves} t-moves	90  {value}
	{piece}		T100	{moves} t-moves	100 {value}
	{piece}		S0	{moves} s-moves	0   {value}
	{piece}		S15	{moves} s-moves	15  {value}
	{piece}		S25	{moves} s-moves	25  {value}
	{piece}		S28	{moves} s-moves	28  {value}
	{piece}		S36	{moves} s-moves	36  {value}
	{piece}		S45	{moves} s-moves	45  {value}
	{piece}		S49	{moves} s-moves	49  {value}
	{piece}		S64	{moves} s-moves	64  {value}
	{piece}		S66	{moves} s-moves	66  {value}
	{piece}		S81	{moves} s-moves	81  {value}
	{piece}		S120	{moves} s-moves	120 {value}
	{piece}		S121	{moves} s-moves	121 {value}
	{piece}		S153	{moves} s-moves	153 {value}
	{piece}		S169	{moves} s-moves	169 {value}
	{piece}		S225	{moves} s-moves	225 {value}
	{piece}		S289	{moves} s-moves	289 {value}
	{piece}		S361	{moves} s-moves	361 {value}
	{piece}		P0	{moves} p-moves	0   {value}
	{piece}		P91	{moves} p-moves	91  {value}
	{piece}		P190	{moves} p-moves	190 {value}
pieces}


This implementation lacks a lot (for example, checking for game completion by Glorious Victories ). I will try to add the missing functionality in the future. The current version of the source code can always be found here .

What is the result?


Rhythmachy interested me, first of all, with its complexity. Of course, there were no thoughts to implement it on ZRF . I had to master Axiom , for this! Currently, there is a simplified implementation that does not support Glorious Victories . Also, there is no firm belief that I found all the errors in the code (1000 lines in ForthScript - this is serious). This is a beta version, but overall it works:



You may notice that the game ends very quickly. It really is. In case both players play aggressively, according to the terms of Common Victories (and without limiting the number of pieces taken), the average duration of a game is ~ 10 moves. At the same time, the first player has a serious advantage:

Cumulative results following game 13 of 100:
Player 1 "Eval", wins = 13.
Player 2 "Eval", wins = 0.
Draws = 0

It's funny that if only one of the players is aggressive, the game drags on to more than 300 moves (an aggressive player almost always wins).

Cumulative results following game 22 of 100:
Player 1 "Rithmomachy", wins = 1.
Player 2 "Eval", wins = 21.
Draws = 0

It's hard for a person with a computer to play. Even with the highlight of the figures under battle, it can be difficult to figure out which particular piece to make a move in order to take (although it is also advisable not to substitute your own figures). Nowadays, this game is unlikely to be popular, but one cannot be taken away from it. She is great at developing her oral counting skills.

Also popular now: