File Coverage

blib/lib/Chess/Plisco/Engine/Position.pm
Criterion Covered Total %
statement 118 122 96.7
branch 8 10 80.0
condition 11 12 91.6
subroutine 18 20 90.0
pod 2 5 40.0
total 157 169 92.9


line stmt bran cond sub pod time code
1             #! /bin/false
2              
3             # Copyright (C) 2021 Guido Flohr ,
4             # all rights reserved.
5              
6             # This program is free software. It comes without any warranty, to
7             # the extent permitted by applicable law. You can redistribute it
8             # and/or modify it under the terms of the Do What the Fuck You Want
9             # to Public License, Version 2, as published by Sam Hocevar. See
10             # http://www.wtfpl.net/ for more details.
11              
12             package Chess::Plisco::Engine::Position;
13             $Chess::Plisco::Engine::Position::VERSION = '0.4';
14 11     11   342158 use strict;
  11         112  
  11         350  
15 11     11   1541 use integer;
  11         59  
  11         57  
16              
17 11     11   4268 use Chess::Plisco qw(:all);
  11         43  
  11         5169  
18             # Macros from Chess::Plisco::Macro are already expanded here!
19 11     11   3889 use Chess::Plisco::Engine::Tree;
  11         35  
  11         335  
20              
21 11     11   66 use base qw(Chess::Plisco);
  11         20  
  11         1275  
22              
23 11     11   71 use constant CP_POS_GAME_PHASE => 14;
  11         21  
  11         657  
24 11     11   55 use constant CP_POS_OPENING_SCORE => 15;
  11         21  
  11         414  
25 11     11   53 use constant CP_POS_ENDGAME_SCORE => 16;
  11         32  
  11         445  
26              
27 11     11   54 use constant PAWN_PHASE => 0;
  11         29  
  11         474  
28 11     11   57 use constant KNIGHT_PHASE => 1;
  11         28  
  11         385  
29 11     11   51 use constant BISHOP_PHASE => 1;
  11         20  
  11         427  
30 11     11   53 use constant ROOK_PHASE => 2;
  11         20  
  11         375  
31 11     11   50 use constant QUEEN_PHASE => 4;
  11         20  
  11         611  
32 11         615 use constant TOTAL_PHASE => PAWN_PHASE * 16
33             + KNIGHT_PHASE * 4 + BISHOP_PHASE * 4
34 11     11   55 + ROOK_PHASE * 4 + QUEEN_PHASE * 2;
  11         22  
35 11         24275 use constant PHASE_INC => [
36             0,
37             PAWN_PHASE,
38             KNIGHT_PHASE,
39             BISHOP_PHASE,
40             ROOK_PHASE,
41             QUEEN_PHASE,
42             0,
43 11     11   61 ];
  11         21  
44              
45              
46             my @op_value = (0, 82, 337, 365, 477, 1025, 0);
47             my @eg_value = (0, 94, 281, 297, 512, 936, 0);
48              
49             # piece/sq tables
50             # values from Rofchade: http://www.talkchess.com/forum3/viewtopic.php?f=2&t=68311&start=19
51              
52             my @op_pawn_table = (
53             0, 0, 0, 0, 0, 0, 0, 0,
54             98, 134, 61, 95, 68, 126, 34, -11,
55             -6, 7, 26, 31, 65, 56, 25, -20,
56             -14, 13, 6, 21, 23, 12, 17, -23,
57             -27, -2, -5, 12, 17, 6, 10, -25,
58             -26, -4, -4, -10, 3, 3, 33, -12,
59             # 47, 81, 62, 59, 67, 106, 120, 60
60             -35, -1, -20, -23, -15, 24, 38, -22,
61             0, 0, 0, 0, 0, 0, 0, 0,
62             );
63              
64             my @eg_pawn_table = (
65             0, 0, 0, 0, 0, 0, 0, 0,
66             178, 173, 158, 134, 147, 132, 165, 187,
67             94, 100, 85, 67, 56, 53, 82, 84,
68             32, 24, 13, 5, -2, 4, 17, 17,
69             13, 9, -3, -7, -7, -8, 3, -1,
70             4, 7, -6, 1, 0, -5, -1, -8,
71             13, 8, 8, 10, 13, 0, 2, -7,
72             0, 0, 0, 0, 0, 0, 0, 0,
73             );
74              
75             my @op_knight_table = (
76             -167, -89, -34, -49, 61, -97, -15, -107,
77             -73, -41, 72, 36, 23, 62, 7, -17,
78             -47, 60, 37, 65, 84, 129, 73, 44,
79             -9, 17, 19, 53, 37, 69, 18, 22,
80             -13, 4, 16, 13, 28, 19, 21, -8,
81             -23, -9, 12, 10, 19, 17, 25, -16,
82             -29, -53, -12, -3, -1, 18, -14, -19,
83             -105, -21, -58, -33, -17, -28, -19, -23,
84             );
85              
86             my @eg_knight_table = (
87             -58, -38, -13, -28, -31, -27, -63, -99,
88             -25, -8, -25, -2, -9, -25, -24, -52,
89             -24, -20, 10, 9, -1, -9, -19, -41,
90             -17, 3, 22, 22, 22, 11, 8, -18,
91             -18, -6, 16, 25, 16, 17, 4, -18,
92             -23, -3, -1, 15, 10, -3, -20, -22,
93             -42, -20, -10, -5, -2, -20, -23, -44,
94             -29, -51, -23, -15, -22, -18, -50, -64,
95             );
96              
97             my @op_bishop_table = (
98             -29, 4, -82, -37, -25, -42, 7, -8,
99             -26, 16, -18, -13, 30, 59, 18, -47,
100             -16, 37, 43, 40, 35, 50, 37, -2,
101             -4, 5, 19, 50, 37, 37, 7, -2,
102             -6, 13, 13, 26, 34, 12, 10, 4,
103             0, 15, 15, 15, 14, 27, 18, 10,
104             4, 15, 16, 0, 7, 21, 33, 1,
105             -33, -3, -14, -21, -13, -12, -39, -21,
106             );
107              
108             my @eg_bishop_table = (
109             -14, -21, -11, -8, -7, -9, -17, -24,
110             -8, -4, 7, -12, -3, -13, -4, -14,
111             2, -8, 0, -1, -2, 6, 0, 4,
112             -3, 9, 12, 9, 14, 10, 3, 2,
113             -6, 3, 13, 19, 7, 10, -3, -9,
114             -12, -3, 8, 10, 13, 3, -7, -15,
115             -14, -18, -7, -1, 4, -9, -15, -27,
116             -23, -9, -23, -5, -9, -16, -5, -17,
117             );
118              
119             my @op_rook_table = (
120             32, 42, 32, 51, 63, 9, 31, 43,
121             27, 32, 58, 62, 80, 67, 26, 44,
122             -5, 19, 26, 36, 17, 45, 61, 16,
123             -24, -11, 7, 26, 24, 35, -8, -20,
124             -36, -26, -12, -1, 9, -7, 6, -23,
125             -45, -25, -16, -17, 3, 0, -5, -33,
126             -44, -16, -20, -9, -1, 11, -6, -71,
127             -19, -13, 1, 17, 16, 7, -37, -26,
128             );
129              
130             my @eg_rook_table = (
131             13, 10, 18, 15, 12, 12, 8, 5,
132             11, 13, 13, 11, -3, 3, 8, 3,
133             7, 7, 7, 5, 4, -3, -5, -3,
134             4, 3, 13, 1, 2, 1, -1, 2,
135             3, 5, 8, 4, -5, -6, -8, -11,
136             -4, 0, -5, -1, -7, -12, -8, -16,
137             -6, -6, 0, 2, -9, -9, -11, -3,
138             -9, 2, 3, -1, -5, -13, 4, -20,
139             );
140              
141             my @op_queen_table = (
142             -28, 0, 29, 12, 59, 44, 43, 45,
143             -24, -39, -5, 1, -16, 57, 28, 54,
144             -13, -17, 7, 8, 29, 56, 47, 57,
145             -27, -27, -16, -16, -1, 17, -2, 1,
146             -9, -26, -9, -10, -2, -4, 3, -3,
147             -14, 2, -11, -2, -5, 2, 14, 5,
148             -35, -8, 11, 2, 8, 15, -3, 1,
149             -1, -18, -9, 10, -15, -25, -31, -50,
150             );
151              
152             my @eg_queen_table = (
153             -9, 22, 22, 27, 27, 19, 10, 20,
154             -17, 20, 32, 41, 58, 25, 30, 0,
155             -20, 6, 9, 49, 47, 35, 19, 9,
156             3, 22, 24, 45, 57, 40, 57, 36,
157             -18, 28, 19, 47, 31, 34, 39, 23,
158             -16, -27, 15, 6, 9, 17, 10, 5,
159             -22, -23, -30, -16, -16, -23, -36, -32,
160             -33, -28, -22, -43, -5, -32, -20, -41,
161             );
162              
163             my @op_king_table = (
164             -65, 23, 16, -15, -56, -34, 2, 13,
165             29, -1, -20, -7, -8, -4, -38, -29,
166             -9, 24, 2, -16, -20, 6, 22, -22,
167             -17, -20, -12, -27, -30, -25, -14, -36,
168             -49, -1, -27, -39, -46, -44, -33, -51,
169             -14, -14, -22, -46, -44, -30, -15, -27,
170             1, 7, -8, -64, -43, -16, 9, 8,
171             -15, 36, 12, -54, 8, -28, 24, 14,
172             );
173              
174             my @eg_king_table = (
175             -74, -35, -18, -18, -11, 15, 4, -17,
176             -12, 17, 14, 17, 17, 38, 23, 11,
177             10, 17, 23, 15, 20, 45, 44, 13,
178             -8, 22, 24, 27, 26, 33, 26, 3,
179             -18, -4, 21, 24, 27, 23, 9, -11,
180             -19, -3, 11, 21, 23, 16, 7, -9,
181             -27, -11, 4, 13, 14, 4, -5, -17,
182             -53, -34, -21, -11, -28, -14, -24, -43
183             );
184              
185             my @op_pesto_table = (
186             undef,
187             \@op_pawn_table,
188             \@op_knight_table,
189             \@op_bishop_table,
190             \@op_rook_table,
191             \@op_queen_table,
192             \@op_king_table
193             );
194              
195             my @eg_pesto_table = (
196             undef,
197             \@eg_pawn_table,
198             \@eg_knight_table,
199             \@eg_bishop_table,
200             \@eg_rook_table,
201             \@eg_queen_table,
202             \@eg_king_table
203             );
204              
205             my @op_table;
206             my @eg_table;
207              
208             # Init tables.
209             for (my $piece = CP_PAWN; $piece <= CP_KING; ++$piece) {
210             for (my $shift = 0; $shift < 64; ++$shift) {
211             my $windex = (CP_WHITE << 9) | ($piece << 6) | $shift;
212             my $bindex = (CP_BLACK << 9) | ($piece << 6) | $shift;
213             $op_table[$windex] = $op_value[$piece] + $op_pesto_table[$piece]->[$shift ^ 56];
214             $eg_table[$windex] = $eg_value[$piece] + $eg_pesto_table[$piece]->[$shift ^ 56];
215             $op_table[$bindex] = $op_value[$piece] + $op_pesto_table[$piece]->[$shift];
216             $eg_table[$bindex] = $eg_value[$piece] + $eg_pesto_table[$piece]->[$shift];
217             }
218             }
219              
220             my @pieces = (' ', 'P', 'N', 'B', 'R', 'Q', 'K');
221             for (my $i = 0; $i < @op_table; ++$i) {
222             my $op_score = $op_table[$i];
223             next if !defined $op_score;
224             my $eg_score = $eg_table[$i];
225             my $shift = $i & 0x3f;
226             my $piece = ($i >> 6) & 0x7;
227             my $color = ($i >> 9);
228             my $piece_char = $pieces[$piece];
229             $piece_char = lc $piece_char if $color;
230             my $square = Chess::Plisco->shiftToSquare($shift);
231             }
232              
233             # For all combinations of victim and promotion piece, calculate the change in
234             # game phase. These values are positive and meant to be added to the phase;
235             my @move_phase_deltas = (0) x 369;
236             foreach my $victim (CP_NO_PIECE, CP_PAWN .. CP_QUEEN) {
237             foreach my $promote (CP_NO_PIECE, CP_KNIGHT .. CP_QUEEN) {
238             next if $promote && $victim == CP_PAWN;
239             next if !$victim && !$promote;
240             my $delta = -PHASE_INC->[$victim];
241             if ($promote) {
242             $delta -= (Chess::Plisco::Engine::Position::PAWN_PHASE
243             - PHASE_INC->[$promote]);
244             }
245             $move_phase_deltas[($victim << 3) | $promote] = $delta;
246             }
247             }
248              
249             # Lookup tables for the resulting opening and endgame scores for each
250             # possible move.
251             my @opening_deltas;
252             my @endgame_deltas;
253              
254             foreach my $move (Chess::Plisco->moveNumbers) {
255             my $is_ep;
256             my $color = 1 & ($move >> 21);
257             my $captured = 0x7 & ($move >> 18);
258             if ($captured == CP_KING) {
259             $captured = CP_PAWN;
260             $is_ep = 1;
261             }
262             my ($to, $from, $promote, $piece) = (
263             Chess::Plisco->moveTo($move),
264             Chess::Plisco->moveFrom($move),
265             Chess::Plisco->movePromote($move),
266             Chess::Plisco->movePiece($move),
267             );
268              
269             my $from_index = ($color << 9) | ($piece << 6) | $from;
270             my $to_index = ($color << 9) | ($piece << 6) | $to;
271             my $opening_delta = $op_table[$from_index] - $op_table[$to_index];
272             my $endgame_delta = $eg_table[$from_index] - $eg_table[$to_index];
273             if ($is_ep) {
274             my $ep_to;
275             if ($color == CP_WHITE) {
276             $ep_to = $to - 8;
277             } else {
278             $ep_to = $to + 8;
279             }
280             my $ep_index = ($color << 9) | (CP_PAWN) << 6 | $ep_to;
281             $opening_delta -= $op_table[$ep_index];
282             $endgame_delta -= $eg_table[$ep_index];
283             } elsif ($captured) {
284             # The captured piece must be viewed from the other side.
285             my $captured_index = ((!$color) << 9) | ($captured << 6) | $to;
286             $opening_delta -= $op_table[$captured_index];
287             $endgame_delta -= $eg_table[$captured_index];
288             }
289              
290             if ($promote) {
291             my $promote_index = ($color << 9) | ($promote << 6) | $to;
292             my $promote_pawn_index = ($color << 9) | (CP_PAWN << 6) | $to;
293             $opening_delta -= $op_table[$promote_index]
294             - $op_table[$promote_pawn_index];
295             $endgame_delta -= $eg_table[$promote_index]
296             - $eg_table[$promote_pawn_index];
297             }
298              
299             # Handle castlings.
300             if (CP_KING == $piece && CP_E8 == $from) {
301             if (CP_C8 == $to) {
302             my $rook_a8_index = (CP_BLACK << 9) | (CP_ROOK << 6) | CP_A8;
303             my $rook_d8_index = (CP_BLACK << 9) | (CP_ROOK << 6) | CP_D8;
304             $opening_delta -= $op_table[$rook_d8_index]
305             - $op_table[$rook_a8_index];
306             $endgame_delta -= $eg_table[$rook_d8_index]
307             - $eg_table[$rook_a8_index];
308             } elsif (CP_G8 == $to) {
309             my $rook_h8_index = (CP_BLACK << 9) | (CP_ROOK << 6) | CP_H8;
310             my $rook_f8_index = (CP_BLACK << 9) | (CP_ROOK << 6) | CP_F8;
311             $opening_delta -= $op_table[$rook_f8_index]
312             - $op_table[$rook_h8_index];
313             $endgame_delta -= $eg_table[$rook_f8_index]
314             - $eg_table[$rook_h8_index];
315             }
316             } elsif (CP_KING == $piece && CP_E1 == $from) {
317             if (CP_C1 == $to) {
318             my $rook_a1_index = (CP_WHITE << 9) | (CP_ROOK << 6) | CP_A1;
319             my $rook_d1_index = (CP_WHITE << 9) | (CP_ROOK << 6) | CP_D1;
320             $opening_delta -= $op_table[$rook_d1_index]
321             - $op_table[$rook_a1_index];
322             $endgame_delta -= $eg_table[$rook_d1_index]
323             - $eg_table[$rook_a1_index];
324             } elsif (CP_G1 == $to) {
325             my $rook_h1_index = (CP_WHITE << 9) | (CP_ROOK << 6) | CP_H1;
326             my $rook_f1_index = (CP_WHITE << 9) | (CP_ROOK << 6) | CP_F1;
327             $opening_delta -= $op_table[$rook_f1_index]
328             - $op_table[$rook_h1_index];
329             $endgame_delta -= $eg_table[$rook_f1_index]
330             - $eg_table[$rook_h1_index];
331             }
332             }
333              
334             $opening_deltas[$move] = $color ? $opening_delta : -$opening_delta;
335             $endgame_deltas[$move] = $color ? $endgame_delta : -$endgame_delta;
336             }
337              
338              
339              
340             sub new {
341 111     111 1 45910 my ($class, @args) = @_;
342              
343 111         390 my $self = $class->SUPER::new(@args);
344              
345 111         173 my $op_phase = 0;
346 111         160 my $op_score = 0;
347 111         136 my $eg_score = 0;
348 111         161 my $white = $self->[CP_POS_WHITE_PIECES];
349 111         182 my $black = $self->[CP_POS_BLACK_PIECES];
350              
351 111         234 foreach my $piece (CP_PAWN .. CP_KING) {
352 666         772 my $pieces = $self->[$piece];
353 666         746 my $white_pieces = $pieces & $white;
354 666         737 my $black_pieces = $pieces & $black;
355 666         810 my $phase_inc = PHASE_INC->[$piece];
356 666         974 while ($white_pieces) {
357 627         635 my $shift = (do { my $B = $white_pieces & -$white_pieces; my $A = $B - 1 - ((($B - 1) >> 1) & 0x5555_5555_5555_5555); my $C = ($A & 0x3333_3333_3333_3333) + (($A >> 2) & 0x3333_3333_3333_3333); my $n = $C + ($C >> 32); $n = ($n & 0x0f0f0f0f) + (($n >> 4) & 0x0f0f0f0f); $n = ($n & 0xffff) + ($n >> 16); $n = ($n & 0xff) + ($n >> 8);});
  627         756  
  627         796  
  627         771  
  627         713  
  627         727  
  627         728  
  627         758  
358 627         766 my $idx = (CP_WHITE << 9) | ($piece << 6) | $shift;
359 627         754 $op_score += $op_table[$idx];
360 627         690 $eg_score += $eg_table[$idx];
361 627         738 $white_pieces = (($white_pieces) & (($white_pieces) - 1));
362 627         954 $op_phase += $phase_inc;
363             }
364 666         1045 while ($black_pieces) {
365 636         657 my $shift = (do { my $B = $black_pieces & -$black_pieces; my $A = $B - 1 - ((($B - 1) >> 1) & 0x5555_5555_5555_5555); my $C = ($A & 0x3333_3333_3333_3333) + (($A >> 2) & 0x3333_3333_3333_3333); my $n = $C + ($C >> 32); $n = ($n & 0x0f0f0f0f) + (($n >> 4) & 0x0f0f0f0f); $n = ($n & 0xffff) + ($n >> 16); $n = ($n & 0xff) + ($n >> 8);});
  636         700  
  636         811  
  636         778  
  636         781  
  636         755  
  636         781  
  636         777  
366 636         785 my $idx = (CP_BLACK << 9) | ($piece << 6) | $shift;
367 636         755 $op_score -= $op_table[$idx];
368 636         683 $eg_score -= $eg_table[$idx];
369 636         720 $black_pieces = (($black_pieces) & (($black_pieces) - 1));
370 636         986 $op_phase += $phase_inc;
371             }
372             }
373              
374 111         140 $self->[CP_POS_OPENING_SCORE] = $op_score;
375 111         145 $self->[CP_POS_ENDGAME_SCORE] = $eg_score;
376              
377 111         141 $self->[CP_POS_GAME_PHASE] = $op_phase;
378              
379 111         287 return $self;
380             }
381              
382             sub doMove {
383 3533635     3533635 1 4983783 my ($self, $move) = @_;
384              
385 3533635 100       6013953 my $state = $self->SUPER::doMove($move) or return;
386 1866910         2839873 ($move) = @$state;
387 1866910         3341132 $self->[CP_POS_GAME_PHASE] += $move_phase_deltas[
388             ((($move >> 18) & 0x7) << 3) | (($move >> 12) & 0x7)
389             ];
390 1866910         3106614 my $score_index = ($move & 0x1fffff) | (!(((($self->[CP_POS_INFO] & (1 << 4)) >> 4))) << 21);
391 1866910         3031018 $self->[CP_POS_OPENING_SCORE] += $opening_deltas[$score_index];
392 1866910         2706563 $self->[CP_POS_ENDGAME_SCORE] += $endgame_deltas[$score_index];
393              
394 1866910         4061854 return $state;
395             }
396              
397             sub evaluate {
398 742387     742387 0 1065832 my ($self) = @_;
399              
400 742387         1061118 my $material = (($self->[CP_POS_INFO] >> 19));
401 742387         913008 my $white_pieces = $self->[CP_POS_WHITE_PIECES];
402 742387         946532 my $black_pieces = $self->[CP_POS_BLACK_PIECES];
403 742387         875113 my $pawns = $self->[CP_POS_PAWNS];
404 742387         982060 my $knights = $self->[CP_POS_KNIGHTS];
405 742387         1022187 my $bishops = $self->[CP_POS_BISHOPS];
406 742387         914065 my $rooks = $self->[CP_POS_ROOKS];
407 742387         893083 my $queens = $self->[CP_POS_QUEENS];
408 742387         930884 my $kings = $self->[CP_POS_KINGS];
409              
410             # We simply assume that a position without pawns is in general a draw.
411             # If one side is a minor piece ahead, it is considered a draw, when there
412             # are no rooks or queens on the board. Important exception is KBB vs KN.
413             # But in that case the material delta is B + B - N which is greater
414             # than B. On the other hand KBB vs KB is a draw and the material balance
415             # in that case is exactly one bishop.
416             # These simple formulas do not take into account that there may be more
417             # than two knights or bishops for one side on the board but in the
418             # exceptional case that this happens, the result would be close enough
419             # anyway.
420 742387 100       1230348 if (!$pawns) {
421 397         525 my $delta = (do { my $mask = $material >> CP_INT_SIZE * CP_CHAR_BIT - 1; ($material + $mask) ^ $mask;});
  397         573  
  397         639  
422 397 50 100     1831 if ($delta < CP_PAWN_VALUE
      66        
      100        
      100        
423             || (!$rooks && !$queens
424             && (($delta <= CP_BISHOP_VALUE)
425             || ($delta == 2 * CP_KNIGHT_VALUE)
426             || ($delta == CP_KNIGHT_VALUE + CP_BISHOP_VALUE)))) {
427 37         147 return Chess::Plisco::Engine::Tree::DRAW();
428             }
429             }
430              
431 742350         998783 my $op_phase = $self->[CP_POS_GAME_PHASE];
432              
433 742350 50       1247187 $op_phase = TOTAL_PHASE if $op_phase > TOTAL_PHASE;
434 742350         905133 my $eg_phase = TOTAL_PHASE - $op_phase;
435              
436 742350         1163754 my $score = ($self->[CP_POS_OPENING_SCORE] * $op_phase
437             + $self->[CP_POS_ENDGAME_SCORE] * $eg_phase) / TOTAL_PHASE;
438              
439 742350 100       1717880 return (((($self->[CP_POS_INFO] & (1 << 4)) >> 4))) ? -$score : $score;
440             }
441              
442              
443              
444             sub openingDelta {
445 0     0 0   my ($self, $index) = @_;
446              
447 0           return $opening_deltas[$index];
448             }
449              
450             sub endgameDelta {
451 0     0 0   my ($self, $index) = @_;
452              
453 0           return $endgame_deltas[$index];
454             }
455              
456             1;