Amoeba : A Modern TSCP Type Engine

Discussion of chess software programming and technical issues.

Moderator: Ras

User avatar
hgm
Posts: 28321
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Amoeba : A Modern TSCP Type Engine

Post by hgm »

Introducing many auxiliary types would be one way to do it. But perhaps it would be simpler to generate Pawn moves and castlings separately from the universal loop I described above. The latter would then not have to test for the MOVE/CAPTURE_ONLY exceptions.

For the Pawns we could make use of an auxiliary board containing flags. Four bits in the flag for each square could be the castling 'spoilers', and their value for the toSquare would be ORed into a castling-rights byte to flag that the corresponding castling is no longer possible because there now is a piece on the involved square that has moved. (If the King or Rook would have left the relevant squares you would notice that by the squares being empty.) Two other bits could indicate 2nd and 7th rank, and could be tested by the Pawn code to see if the code for generating double pushes or promotions would have to be invoked.

Code: Select all

opponent = sideToMove ^ 0xc0;
for(piece=0; piece<16; piece++) {
  fromSquare = location[piece];
  if(fromSquare == -1) continue; // piece not present
  type = pieceType[piece];
  direction = firstDirection[type];
  if(type < KNIGHT) { // Pawn
    if(spoilers[fromSquare] & sideToMove) effect = 5; // promotion to Queen
    else effect = 0;
    while((step = moveTable[direction].step)) {
      toSquare = fromSquare + step;
      victim = board[toSquare];
      if(victim & sideToMove) continue; // also catches edge guards
      if(victim) {
        if(moveTable[direction].capability & MOVE_ONLY) continue;
        AddCapture(fromSquare, toSquare, victim, effect);
      } else {
        if(moveTable[direction].capability & CAPTURE_ONLY) continue;
        if(effect) AddCapture(fromSquare, toSquare, 0, effect); // promotion to Q added as capture
        else AddMove(fromSquare, toSquare, 0, 0);
        if(spoiler[fromSquare] & opponent) { // starting rank
          toSquare += step;
          victim = board[toSquare];
          if(!victim) AddMove(fromSquare, toSquare, 0, 1); // add double push with e.p. setting as effect
        }
      }
      for(i=2; i<effect; i++) AddMove(fromSquare, toSquare, victim, i); // add underpromotions as non-captures
    }
  } else { // non-Pawn
    while((step = moveTable[direction].step)) {
      range = moveTable[direction].range;
      toSquare = fromSquare;
      do {
        toSquare += step;
        victim = board[toSquare];
        if(victim & sideToMove) break;
        if(victim) {
          AddCapture(fromSquare, toSquare, victim, 0);
        } else {
          AddMove(fromSquare, toSquare, 0, 0);
        }
      } while(--range);
    }
  }
}
Castlings and e.p. captures would be generated by dedicated code, executed only when rights to perform these moves exist. The castling rights would be updated in MakeMove through rights |= spoiler[toSquare], and the e.p. rights would be created in MakeMove when the special code for effect 1 is performed. (Which would test for enemy Pawns next to the toSquare, and enable the right or left e.p. capture by ORing the rights byte with 0x10 or 0x20. The bits 0x0F would be the castling spoilers, while the 0xC0 would be contaminated by the 2nd and 7th-rank indicators and be ignored.)
Mike Sherwin
Posts: 965
Joined: Fri Aug 21, 2020 1:25 am
Location: Planet Earth, Sol system
Full name: Michael J Sherwin

Re: Amoeba : A Modern TSCP Type Engine

Post by Mike Sherwin »

hgm wrote: Sun Feb 16, 2025 10:47 pm Introducing many auxiliary types would be one way to do it. But perhaps it would be simpler to generate Pawn moves and castlings separately from the universal loop I described above. The latter would then not have to test for the MOVE/CAPTURE_ONLY exceptions.

For the Pawns we could make use of an auxiliary board containing flags. Four bits in the flag for each square could be the castling 'spoilers', and their value for the toSquare would be ORed into a castling-rights byte to flag that the corresponding castling is no longer possible because there now is a piece on the involved square that has moved. (If the King or Rook would have left the relevant squares you would notice that by the squares being empty.) Two other bits could indicate 2nd and 7th rank, and could be tested by the Pawn code to see if the code for generating double pushes or promotions would have to be invoked.

Code: Select all

opponent = sideToMove ^ 0xc0;
for(piece=0; piece<16; piece++) {
  fromSquare = location[piece];
  if(fromSquare == -1) continue; // piece not present
  type = pieceType[piece];
  direction = firstDirection[type];
  if(type < KNIGHT) { // Pawn
    if(spoilers[fromSquare] & sideToMove) effect = 5; // promotion to Queen
    else effect = 0;
    while((step = moveTable[direction].step)) {
      toSquare = fromSquare + step;
      victim = board[toSquare];
      if(victim & sideToMove) continue; // also catches edge guards
      if(victim) {
        if(moveTable[direction].capability & MOVE_ONLY) continue;
        AddCapture(fromSquare, toSquare, victim, effect);
      } else {
        if(moveTable[direction].capability & CAPTURE_ONLY) continue;
        if(effect) AddCapture(fromSquare, toSquare, 0, effect); // promotion to Q added as capture
        else AddMove(fromSquare, toSquare, 0, 0);
        if(spoiler[fromSquare] & opponent) { // starting rank
          toSquare += step;
          victim = board[toSquare];
          if(!victim) AddMove(fromSquare, toSquare, 0, 1); // add double push with e.p. setting as effect
        }
      }
      for(i=2; i<effect; i++) AddMove(fromSquare, toSquare, victim, i); // add underpromotions as non-captures
    }
  } else { // non-Pawn
    while((step = moveTable[direction].step)) {
      range = moveTable[direction].range;
      toSquare = fromSquare;
      do {
        toSquare += step;
        victim = board[toSquare];
        if(victim & sideToMove) break;
        if(victim) {
          AddCapture(fromSquare, toSquare, victim, 0);
        } else {
          AddMove(fromSquare, toSquare, 0, 0);
        }
      } while(--range);
    }
  }
}
Castlings and e.p. captures would be generated by dedicated code, executed only when rights to perform these moves exist. The castling rights would be updated in MakeMove through rights |= spoiler[toSquare], and the e.p. rights would be created in MakeMove when the special code for effect 1 is performed. (Which would test for enemy Pawns next to the toSquare, and enable the right or left e.p. capture by ORing the rights byte with 0x10 or 0x20. The bits 0x0F would be the castling spoilers, while the 0xC0 would be contaminated by the 2nd and 7th-rank indicators and be ignored.)
Should (victim & sideToMove) be (victim & opponent) instead?
Should for(piece=0; piece<16; piece++) { } also select from pieces 16 to 31 somehow?
for (piece = 0 + 16 * sideToMove; piece < 16 + 16 * sideToMove; piece++) { }?
abulmo2
Posts: 460
Joined: Fri Dec 16, 2016 11:04 am
Location: France
Full name: Richard Delorme

Re: Amoeba : A Modern TSCP Type Engine

Post by abulmo2 »

mar wrote: Sat Feb 15, 2025 11:15 pm I'd perhaps change the name because there already is an engine called Amoeba (author Richard Delorme)
Right, It might be confusing to use the same name.
Richard Delorme
abulmo2
Posts: 460
Joined: Fri Dec 16, 2016 11:04 am
Location: France
Full name: Richard Delorme

Re: Amoeba : A Modern TSCP Type Engine

Post by abulmo2 »

Mike Sherwin wrote: Sat Feb 15, 2025 11:03 pm Does anyone have an opinion about a project like this for creating a modern TSCP type engine?
There are already a lot of simple engines, with no more lines of code than TSCP, but much more modern, using bitboard, tuned parameters, and accurate evaluation function: Akimbo (in Rust, old versions using a hand crafted evaluation), Dumb (in D), Olithink in C, seawall or prelude in C++, etc. They are all about 1000 Elo stronger than TSCP, quite readable and of much higher quality. Another good engine is of course Stockfish. The code is longer, but readable and well commented. In my humble opinion, TSCP is almost an example of what not to do to program a chess engine.
Richard Delorme
User avatar
hgm
Posts: 28321
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Amoeba : A Modern TSCP Type Engine

Post by hgm »

Mike Sherwin wrote: Mon Feb 17, 2025 2:07 am Should (victim & sideToMove) be (victim & opponent) instead?
Should for(piece=0; piece<16; piece++) { } also select from pieces 16 to 31 somehow?
for (piece = 0 + 16 * sideToMove; piece < 16 + 16 * sideToMove; piece++) { }?
You are right about the piece numbers; I wanted the counter (say p) to run from 0 to 16, but then do

piece = sideToMove + p;

but I forgot the latter. Note that sideToMove would be 0x40 or 0x80 in this design. The test (victim &sideToMove) is correct: you test for your own pieces here, where an edge guard has both color bits set (0xC0), and is thus always considered your own piece. And will then block the move.

I am starting to like your idea with the plethora of piece types better, though. I now also tried some code for that. It would use the very simple loop I posted in the beginning for all piece types. But because many pieces can promote now, instead of AddMove or AddCapture it would always call AddWithPromotion(from, to, piece, victim). It would then be up to that function to decide if there is a promotion.

Code: Select all

char promoChoice[] = { T_QUEEN, T_KNIGHT, T_ROOK, T_BISHOP, 0 };
void AddPromotion(int fromSquare, int toSquare, int piece, int victim)
{
  int effect = promoTable[piece][toSquare];
  if(effect < 0) { // invalid effect, flags promotion with choice
    int promoPtr = 0;
    while((effect = promoChoice[promoPtr++])) // loop over possible choices
      AddMove(fromSquare, toSquare, effect, victim);
  } else AddMove(fromSquare, toSquare, effect, victim); // promotion without choice ('morphing')
}
Effects 1-N would indicate promotion to the type with that number, effect 0 would mean no promotion. This seems codewise simpler than having the separate loop for Pawns. The desired Pawn behavior can be achieved by starting with a 'charged Pawn' type on 2nd rank, which has range = 2 for its non-capture move. It would have a board-size promoTable[T_CHARGED_PAWN] that specifies effect T_PAWN everywhere, so that all its moves would make it morph into a normal Pawn. The normal Pawn would have a promoTable[T_PAWN] that specifies -1 on the final rank to indicate it promotes with choice there.

This initially present 'charged King' and 'charged Rook' pieces would have tables that makes them morph into normal Kings and Rooks everywhere. Only the charged types could participate in castling, and the castling code will test for that. The other uncharged piece types would use a (shared) promoTable that is completely filled with zeros

One of the reasons I like this (besides simplicity) is its extreme flexibility. This would make the engine very easy to adapt to all kind of variants with complex promotion/morphing rules. It could be extended to

Code: Select all

  int effect = promoTable[piece][toSquare];
  if(effect < 0) { // invalid effect, flags promotion with choice
    if(effect > -64) { // codes -63 to -1 indicate promotion sets
      int promoPtr = firstPromotion[~effect]; // use the specified promotion set
      while((effect = promoChoice[promoPtr++])) 
        AddMove(fromSquare, toSquare, effect, victim);
    } else if(effect == -64) { // capture-triggered effects
      effect = captureMatrix[piece][victim];
    } else if(effect == -65) {
      ...
    } else return; // reserved code; reject move
  } else AddMove(fromSquare, toSquare, effect, victim); // promotion without choice ('morphing')
}
This would allow different piece types to have different promotion choices. And it could defer the decision about morphing to a table that makes it dependent on what you captured, instead of where you landed. And it could be used to trigger other effects, such as confining pieces to part of the board, as in Chinese Chess, or instant wins by reaching a square as in King of the Hill.

[Edit] This did not solve the issue of how to set the e.p. flag on a double push yet. But this can be done by defining a special effect on the 4th rank of promoTable[T_CHARGED_PAWN], that causes morphing into T_PAWN, in combination with setting the e.p. flag. On the 3rd rank it would merely morph into T_PAWN. We would of course need separate white and black versions of both the charged and the normal Pawns.

If we have

Code: Select all

enum Type  { T_NONE, T_CHARGED_WPAWN, T_CHARGED_BPAWN, T_CHARGED_ROOK, T_CHARGED_KING, T_WPAWN, T_BPAWN, T_KNIGHT, T_BISHOP, T_ROOK, T_QUEEN, T_KING }; 
Then effects 5 - 11 (T_WPAWN - T_KING) would cause the moving piece to change to those types. Effects 1 and 2 could then be used to indicate promotion to T_WPAWN and T_BPAWN combined with setting the e.p. flag.

Code: Select all

int MakeMove(Move move, Undo *u)
{
  int epFlag = 0, newType;
  u->piece = board[move.fromSquare]; board[move.fromSquare] = EMPTY;
  u->victim = board[move.toSquare]; location[u->victim] = INVALID;
  board[move.toSquare] = u->piece; location[u->piece] = move.toSquare;
  u->type = type[u->piece];
  switch(move.specialEffect) {
    default: // e.p. capture
      u->epSquare = move.specialEffect - 20; // codes >= 20 indicate e.p. target
      u->epVictim = board[u->epSquare]; // remove the e.p.-captured piece
      board[u->epSquare] = EMPTY;
      location[u->epVictim] = INVALID;
    case 0: // this is actually the common case
      newType = u->type; // non-promotion
      break;
    case 1:
    case 2:
      newType = move.specialEffect + 4; // double push
      epFlag = 1;
      break;
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
    case 10:
    case 11:
      newType = move.specialEffect; // plain pomotion/morphing
      break;
    case 12: // castling
      newType = T_KING;
      ... // move and morph Rook
  }
  type[u->piece] = newType;
  return epFlag;
}
User avatar
hgm
Posts: 28321
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Amoeba : A Modern TSCP Type Engine

Post by hgm »

A bit more subtle version of the 'double-push effect' tests whetmher there are i.ndeed adjacent Pawns:

Code: Select all

enum Rights { EP_LEFT = 1, EP_RIGHT = 2 };

  case 2:
    newType = move.specialEffect + 4;
    if(ENEMY_PAWN(board[fromSquare]-1])) epFlag |= EP_LEFT;
    if(ENEMY_PAWN(board[fromSquare]+1])) epFlag |= EP_RIGHT;
    break;
The dedicated code for generating e.p. captures in the move generator of the next ply could then be:

Code: Select all

if(epFlag & EP_LEFT) AddMove(lastToSquare-1, lastToSquare ^1, 0, lastToSquare + 20);
if(epFlag & EP_RIGHT) AddMove(lastToSquare+1, lastToSquare ^1, 0, lastToSquare + 20);
This assuming that the toSquare of the previous ply is passed to the daughter node.
Mike Sherwin
Posts: 965
Joined: Fri Aug 21, 2020 1:25 am
Location: Planet Earth, Sol system
Full name: Michael J Sherwin

Re: Amoeba : A Modern TSCP Type Engine

Post by Mike Sherwin »

hgm wrote: Mon Feb 17, 2025 10:10 am I am starting to like your idea with the plethora of piece types better, though.
It makes sense because they are different piece types. A white pawn on the second rank is not the same type as a white pawn on any other rank. I like your wording about being charged with different abilities and morphing. That is exactly what happens.

You reminded me of an idea I had 40 years ago for a comic book character. He or she would be called Biorhyth. The character would have a biorhythm with varying periods for each super power--strength, speed, intelligence, toughness, hearing etcetera. Over a period of days he would go from very strong to very weak and back again. When all his biorhythms peaked together he would be invincible. When they all troughed together he would hide in a closet. It would be the most dynamic comic book character ever! At the bottom of every page would be a bio chart so the reader could see the hero's current status. So maybe we can rename the engine Biorhyth? :D
Mike Sherwin
Posts: 965
Joined: Fri Aug 21, 2020 1:25 am
Location: Planet Earth, Sol system
Full name: Michael J Sherwin

Re: Amoeba : A Modern TSCP Type Engine

Post by Mike Sherwin »

abulmo2 wrote: Mon Feb 17, 2025 9:09 am
mar wrote: Sat Feb 15, 2025 11:15 pm I'd perhaps change the name because there already is an engine called Amoeba (author Richard Delorme)
Right, It might be confusing to use the same name.
Is Amoeba open source and downloadable?
User avatar
lithander
Posts: 912
Joined: Sun Dec 27, 2020 2:40 am
Location: Bremen, Germany
Full name: Thomas Jahn

Re: Amoeba : A Modern TSCP Type Engine

Post by lithander »

Mike Sherwin wrote: Mon Feb 17, 2025 5:35 pm
abulmo2 wrote: Mon Feb 17, 2025 9:09 am
mar wrote: Sat Feb 15, 2025 11:15 pm I'd perhaps change the name because there already is an engine called Amoeba (author Richard Delorme)
Right, It might be confusing to use the same name.
Is Amoeba open source and downloadable?
https://github.com/abulmo/amoeba
Minimal Chess (simple, open source, C#) - Youtube & Github
Leorik (competitive, in active development, C#) - Github & Lichess
User avatar
hgm
Posts: 28321
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Amoeba : A Modern TSCP Type Engine

Post by hgm »

Castling generation could go like this:

Code: Select all

if(!inCheck) {
  piece = sideToMove+15; // King
  if(type[piece] == T_CHARGED_KING) {
    fromSquare = location[piece];
    if(type[board[fromSquare+3]] == T_CHARGED_ROOK) {
      if(board[fromSquare+1] == EMPTY && board[fromSquare+2] == EMPTY)
        AddMove(fromSquare, fromSquare+2, 12, 0); // K-side castling
    }
    if(type[board[fromSquare-4]] == T_CHARGED_ROOK) {
      if(board[fromSquare-1] == EMPTY && board[fromSquare-2] == EMPTY && board[fromSquare-3] == EMPTY)
        AddMove(fromSquare, fromSquare-2, 13, 0); // Q-side castling
    }
  }
}
Note that this tests for virginity of the involved pieces, a free path between those, and castling out of check. But not for castling through check. And also not for castling into check, but so far the move generator did not do that for any move.

Now for simplicity I would make this a King-capture engine: testing the legality of the move is outsourced to the daughter node. This should check for King capture during move generation. This can be done by testing whether 'victim' on capture is equal to the piece number of the enemy King, aborting the move generation if this is the case. And then communicate that to the Search routine by returning an error code, which would then immediately return a +INFINITY score to inform the parent the move that led to it was illegal.

In the sameway it should flag that a castling was illegal when it manages to capture the Rook that just castled. This could be achieved by a trick with very little code. Namely by replacing, during move generation after castling, that Rook by a second King. Then the normal test for King capture after every generated capture move would also trigger on capture of that Rook. That this would have to be done can be flagged by some other bits in the epFlags byte that is passed to the daughter node.

The code in MakeMove for performing castling would then look like:

Code: Select all

  case 12:
    newType = T_KING;
    rookToSquare = fromSquare + 1;
    rookFromSquare = fromSquare + 3;
    epFlag = CASTLE_RIGHT;
    rook = board[rookFromSquare];
    board[rookToSquare] = rook; location[rook] = rookToSquare;
    board[rookFromSquare] = EMPTY;
    type[rook] = T_ROOK;
    break;
  case 13:
    newType = T_KING;
    rookToSquare = fromSquare - 1;
    rookFromSquare = fromSquare - 4;
    epFlag = CASTLE_LEFT;
    rook = board[rookFromSquare];
    board[rookToSquare] = rook; location[rook] = rookToSquare;
    board[rookFromSquare] = EMPTY;
    type[rook] = T_ROOK;
    break;
In the daughter node the epFlag would have the following effect (if we encode CASTLE_LEFT as 2 and CASTLE_RIGHT as 4):

Code: Select all

  if(epFlag & (CASTLE_LEFT|CASTLE_RIGHT)) { // previous move was castling
    int rookSquare = lastToSquare + epFlag - 3; // square next to King
    int rook = board[rookSquare];
    board[rookSquare] = opponent + 15; // fake enemy King is there too
    kingCapture = GenerateMoves(sideToMove);
    board[rookSquare] = rook;
  } else kingCapture = GenerateMoves(sideToMove);
  if(kingCapture) return INFINITY;