Play Antichess!

19

1

https://en.wikipedia.org/wiki/Losing_chess

This is basically Chess Tournament, but for antichess ;)

Antichess is one of the many chess variants that have been invented. The goal is to lose all your pieces (this may seem a little odd, but it's called antichess for a reason).

The rules

The rules of antichess are very similar to standard chess - but with a few fairly minor differences. The goal as I mentioned above is to lose all your pieces. To make this happen, if your opponent has an opportunity to capture one of your pieces, that is the only move he can make. If you give him multiple chances in one turn, the other player may choose his turn. Another thing that is changed is that the king has no special powers - as in you cannot checkmate your opponent, and you can't force him into check.

The following changes to the standard game will also apply (they help simplify the game):

  • En passant will be ignored.
  • Castling isn't possible.
  • The Fifty-move rule applies automatically (meaning the game ends in a draw).
  • Pawns will be able to choose what they promote to.
  • If a player needs longer than 2 seconds to move, he will lose the game.
  • Returning an invalid move will result in losing the game.
  • To win, your opponents must capture all your pieces.
  • White begins the game.
  • White is placed "at the bottom" of the field (y=0), black is located at the top (y=7).
  • Accessing other resources than your bot (internet, files, other bots, ...) is prohibited.

Scoring

  • Winning grants you 3 points, a draw 1 point and losing 0 points.
  • Each submission will play against each other submission 10 times (5 times as white, 5 as black).

Writing your bot

Controller code is here: https://github.com/JJ-Atkinson/SimpleAntichessKOTH

You can write your bot in either Java or Groovy. To write a bot you must extend the Player class. The player class has one abstract method Move getMove(Board board, Player enemy, Set<Move> validMoves).

Here is a quick rundown on useful methods:

Player:

  • List<Piece> getPieces(Board board): Return all your pieces that are on the board.
  • PieceUpgradeType pieceUpgradeType: If/when one of your pawns reaches the end of the board, you will need to define this to the type of piece you want to upgrade to. You have the choice of ROOK, KNIGHT, QUEEN, BISHOP, and KING.

Board:

  • Field getFieldAtLoc(Location loc): Return the Field at the location. This has a matching getAt method so that if you are using groovy you can write board[loc].
  • Field getFieldAtLoc(int x, int y): Return the Field at the location. This has a matching getAt method so that if you are using groovy you can write board[x, y].
  • Board movePiece(Player player, Move move): Make a move on the board so you can see how it would play out. It returns the new board.

If you want to see your opponents pieces, just write enemy.getPieces(board). To add your bot to the lineup add the following line to PlayerFactory:

put(YourBot.class, { new YourBot() } )

Debugging your bot:

I have included a couple of tools to aid in debugging your bots. To see your game played out live you can set the Game#DEBUG flag to true. You will get an output something like this:

Game started. Players: [OnePlayBot(WHITE), SacrificeBot(BLACK)]
...
BLACKs turn.
validMoves: [Move(Piece(BLACK, PAWN, Loc(0, 6)), Loc(0, 5)), ...]
board:
RKBQIBKR
PPPPPPPP
--------
--------
--------
p-------
-ppppppp
rkbqibkr

captureless turns: 1
chosen move: Move(Piece(BLACK, PAWN, Loc(7, 6)), Loc(7, 4))
Game over? false

==============================

WHITEs turn.
validMoves: [Move(Piece(WHITE, ROOK, Loc(0, 0)), Loc(0, 1)), ...]
board:
RKBQIBKR
PPPPPPP-
--------
-------P
--------
p-------
-ppppppp
rkbqibkr

...

(White is upper case, the king is shown with i)

If your console supports utf-8 special chars, you can even show the board with the chess chars by using Board#USE_UTF8_TO_STRING:

♜♞♝♛♚♝—♜
♟—♟♟♟♟♟♟
————————
—♟——————
————————
♙———————
—♙♙♙♙♔♙♙
♖♘♗♕—♗♘♖

(it looks better with a mono spaced font)

To prevent a flood of unwanted output, you should change the Main#main function to something like this:

new Game(new MyBot(), new SacrificeBot()).run()

Put your bot on the left to play as white, put it on the right to play as black.

Building the controller:

The controller is written in groovy, so you must have java and groovy installed. If you don't want to install groovy you can use the gradle build file that comes with the controller (this has not been tested). If you don't want to use groovy or gradle you can use the latest release jar (https://github.com/JJ-Atkinson/SimpleAntichessKOTH/releases). If you do this you need to make your own main method and add your bot manually to the player factory. Example:

PlayerFactory.players.put(YourBot.class, { new YourBot() } )
new Runner().runGames();

(Note that you can still set the debug flags and stuff)

Any and all bug finding is appreciated!

Scores:

SearchBot -> 101
SacrificeBot -> 81
MeasureBot -> 37
RandomBot -> 28
OnePlayBot -> 24

Please note that I'm always willing to have new submissions!

J Atkin

Posted 2015-12-21T02:35:09.720

Reputation: 4 846

If you like groovy and IntelliJ ... you should take a look at Kotlin

– TheNumberOne – 2015-12-21T13:47:18.230

I have seen Kotlin before, but never looked at it thoroughly. It kinda looks like a scala/groovy mashup (but thats OK - groovy and scala are my favorite languages ;) – J Atkin – 2015-12-21T14:24:18.957

I've never used scala before ... but it is much easier to call Kotlin code from java than it is to call goovy code from java. – TheNumberOne – 2015-12-21T14:37:19.307

Interesting, I think I made my interface java friendly (all the types are there). I will have to try Kotlin later (I'm reading Learn you a haskell for great good right now) – J Atkin – 2015-12-21T14:50:06.577

1You can upgrade to a king?!? Surely not... – wizzwizz4 – 2015-12-25T07:30:26.603

1@wizzwizz4 In antichess, you can. – ProgramFOX – 2015-12-25T07:34:00.293

In Board::isHomeRow(), shouldn't it be loc.y == 0 || loc.y == BOARD_LENGTH - 1? – Sleafar – 2015-12-29T21:02:34.607

I think that is the code that I am using... – J Atkin – 2015-12-30T19:09:48.500

Answers

6

SearchBot

Slowest bot so far, but still faster than 2 seconds per move and it beats all currently posted bots. It looks at what happens after any of the valid moves and what could happen after any move after those moves and decides what would be the best outcome. Unfortunately it cannot search deeper because it would take more than 2 seconds then.

package com.ppcgse.koth.antichess.player

import com.ppcgse.koth.antichess.controller.Board
import com.ppcgse.koth.antichess.controller.Color
import com.ppcgse.koth.antichess.controller.Location
import com.ppcgse.koth.antichess.controller.Move
import com.ppcgse.koth.antichess.controller.Piece
import com.ppcgse.koth.antichess.controller.PieceType
import com.ppcgse.koth.antichess.controller.PieceUpgradeType
import com.ppcgse.koth.antichess.controller.Player

import groovy.lang.Tuple

/**
 * Created by ProgramFOX on 12/22/15.
 */

 class SearchBot extends Player {
    {pieceUpgradeType = PieceUpgradeType.KING}

    @Override
    Move getMove(Board board, Player opponent, Set<Move> validMoves) {
        return getMoveInternal(board, this, opponent, validMoves, 2)[0]
    }

    Tuple getMoveInternal(Board board, Player whoseTurn, Player opponent, Set<Move> validMoves, Integer depth) {
        def bestScore = null
        def currentlyChosenMove = null
        validMoves.each { m ->
            def opponentPiecesValueBefore = opponent.getPieces(board).sum { getPieceValue(it.getType()) }
            def newBoard = board.movePiece(whoseTurn, m)
            def opponentPiecesValueAfter = opponent.getPieces(newBoard).sum { getPieceValue(it.getType()) }
            if (opponentPiecesValueAfter == null) {
                opponentPiecesValueAfter = 0
            }
            def score = opponentPiecesValueAfter - opponentPiecesValueBefore
            if (whoseTurn.getTeam() == Color.BLACK) {
                score = -score
            }
            if (depth > 1) {
                def validMovesNow = genValidMoves(opponent, whoseTurn, newBoard)
                def goDeeper = true
                if (validMovesNow == null || validMovesNow.size() == 0) {
                    def toAdd = -999
                    if (whoseTurn.getTeam() == Color.BLACK) {
                        toAdd = -toAdd
                    }
                    score += toAdd
                    goDeeper = false
                }
                if (goDeeper) {
                    score += getMoveInternal(newBoard, opponent, whoseTurn, validMovesNow, depth - 1)[1]
                }
            }
            if (bestScore == null) {
                bestScore = score
                currentlyChosenMove = m
            }
            if ((whoseTurn.getTeam() == Color.WHITE && score > bestScore) || (whoseTurn.getTeam() == Color.BLACK && score < bestScore))  {
                bestScore = score
                currentlyChosenMove = m
            }
        }
        return new Tuple(currentlyChosenMove, bestScore)
    }

    Double getPieceValue(PieceType pieceType) {
        switch (pieceType) {
            case PieceType.KING:
                return 1
            case PieceType.PAWN:
                return 1.5
            case PieceType.KNIGHT:
                return 2.5
            case PieceType.BISHOP:
                return 3
            case PieceType.ROOK:
                return 5
            case PieceType.QUEEN:
                return 9
            default:
                return 0
        }
    }

    // Copied from Game.groovy and a bit modified.
    // I actually need this.
    Set<Move> genValidMoves(Player player, Player enemy, Board board) {
        def allMoves = player.getPieces(board).collect { [it, it.getValidDestinationSet(board)] }
        def attackMoves = allMoves
                .collect { pair ->
            def piece = pair[0]
            def dests = pair[1]
            [piece, dests.findAll { board.getFieldAtLoc(it as Location)?.piece?.team == enemy.team }]
        }.findAll { it[1] }

        if (attackMoves.isEmpty())
            return allMoves.collect {
                Piece piece = it[0] as Piece
                return it[1].collect { loc -> new Move(piece, loc as Location) }
            }.flatten() as Set<Move>
        else
            return attackMoves.collect {
                Piece piece = it[0] as Piece
                return it[1].collect { loc -> new Move(piece, loc as Location) }
            }.flatten() as Set<Move>
    }
 }

ProgramFOX

Posted 2015-12-21T02:35:09.720

Reputation: 8 017

4

SacrificeBot

This bot will check all the moves that the other player has and will check to see if any of them intersect (i.e. the piece will be killed). (This does a heck of a lot better than I ever expected ;)

package com.ppcgse.koth.antichess.player

import com.ppcgse.koth.antichess.controller.Board
import com.ppcgse.koth.antichess.controller.Color
import com.ppcgse.koth.antichess.controller.Location
import com.ppcgse.koth.antichess.controller.Move
import com.ppcgse.koth.antichess.controller.Piece
import com.ppcgse.koth.antichess.controller.PieceType
import com.ppcgse.koth.antichess.controller.PieceUpgradeType
import com.ppcgse.koth.antichess.controller.Player

import java.util.concurrent.ThreadLocalRandom

/**
 * Created by Jarrett on 12/19/15.
 */
class SacrificeBot extends Player {

    {pieceUpgradeType = PieceUpgradeType.ROOK}

    @Override
    Move getMove(Board board, Player enemy, Set<Move> validMoves) {
        def enemyPieces = enemy.getPieces(board)
        def pawnMoves = getPawnsMoves(board, enemyPieces)
        def enemyPlayerValidMoves = (enemyPieces
                                        .collect { it.getValidDestinationSet(realBoard) }
                                        .flatten() as List<Location>)
        enemyPlayerValidMoves += pawnMoves

        def sacrificeMove = validMoves
                                .find {enemyPlayerValidMoves.contains(it.destination)}

        if (sacrificeMove)
            return sacrificeMove
        else
            return randomMove(validMoves)
    }

    def randomMove(Set<Move> validMoves) {
        return validMoves[ThreadLocalRandom.current().nextInt(validMoves.size())];
    }

    def getPawnsMoves(Board board, List<Piece> allPieces) {
        def direction = getTeam() == Color.BLACK ? 1 : -1;
        def pawns = allPieces.findAll {it.type == PieceType.PAWN}
        def pawnAttacks = (pawns.collect {
                                    [it.loc.plus(-1, direction), it.loc.plus(1, direction)]
                                }.flatten()
                                ).findAll {
                                    ((Location) it).isValid()
                                }
        return pawnAttacks as List<Location>
    }
}

J Atkin

Posted 2015-12-21T02:35:09.720

Reputation: 4 846

3

RandomBot

This is the manditory random bot. It will always upgrade to a rook.

package com.ppcgse.koth.antichess.player

import com.ppcgse.koth.antichess.controller.PieceUpgradeType
import com.ppcgse.koth.antichess.controller.Player
import com.ppcgse.koth.antichess.controller.Move
import com.ppcgse.koth.antichess.controller.ReadOnlyBoard

import java.util.concurrent.ThreadLocalRandom;

public class TestBot extends Player {

    {pieceUpgradeType = PieceUpgradeType.ROOK}

    @Override
    public Move getMove(ReadOnlyBoard board, Player enemy, Set<Move> moves) {
        return moves[ThreadLocalRandom.current().nextInt(moves.size())];
    }

}

J Atkin

Posted 2015-12-21T02:35:09.720

Reputation: 4 846

3

OnePlayBot

Dead simple bot with only one play. It will upgrade to a rook.

package com.ppcgse.koth.antichess.player

import com.ppcgse.koth.antichess.controller.Move
import com.ppcgse.koth.antichess.controller.PieceUpgradeType
import com.ppcgse.koth.antichess.controller.Player
import com.ppcgse.koth.antichess.controller.ReadOnlyBoard

public class OnePlayBot extends Player {

    {pieceUpgradeType = PieceUpgradeType.ROOK}

    @Override
    public Move getMove(ReadOnlyBoard board, Player enemy, Set<Move> moves) {
        return new ArrayList<Move>(moves).get(0);
    }

}

J Atkin

Posted 2015-12-21T02:35:09.720

Reputation: 4 846

3

MeasureBot

This is the bot that I started with; I was working on expanding it but then I came across the deep-clone bug, and then I thought "well, let's just submit this bot already, it does perform better than RandomBot and OnePlayBot, and I can always submit a new bot later", so here it is:

package com.ppcgse.koth.antichess.player

import com.ppcgse.koth.antichess.controller.Board
import com.ppcgse.koth.antichess.controller.Move
import com.ppcgse.koth.antichess.controller.Piece
import com.ppcgse.koth.antichess.controller.PieceType
import com.ppcgse.koth.antichess.controller.PieceUpgradeType
import com.ppcgse.koth.antichess.controller.Player

import java.util.concurrent.ThreadLocalRandom

/**
 * Created by ProgramFOX on 12/21/15.
 */

 class MeasureBot extends Player {
    {pieceUpgradeType = PieceUpgradeType.KING}

    @Override
    Move getMove(Board board, Player opponent, Set<Move> validMoves) {
        def opponentPieces = opponent.getPieces(board)
        def mustCapture = opponentPieces.find { it.loc == validMoves[0].destination } != null
        def chosen = null
        if (mustCapture) {
            def piecesThatCanBeTaken = opponentPieces.findAll { validMoves.collect { it.getDestination() }.contains(it.loc) }
            def lowestAmount = getPieceValue(piecesThatCanBeTaken.sort { getPieceValue(it.getType()) }[0].getType())
            def piecesWithLowestValue = piecesThatCanBeTaken.findAll { getPieceValue(it.getType()) == lowestAmount }
            def chosenOnes = validMoves.findAll { m -> piecesWithLowestValue.find { it.loc ==  m.destination } != null }
            chosen = chosenOnes.sort { getPieceValue(it.piece.getType()) }.reverse()[0]
        } else {
            chosen = randomMove(validMoves);
        }
        return chosen
    }

    Double getPieceValue(PieceType pieceType) {
        switch (pieceType) {
            case PieceType.KING:
                return 1
            case PieceType.PAWN:
                return 1.5
            case PieceType.KNIGHT:
                return 2.5
            case PieceType.BISHOP:
                return 3
            case PieceType.ROOK:
                return 5
            case PieceType.QUEEN:
                return 9
            default:
                return 0
        }
    }

    def randomMove(Set<Move> validMoves) {
        return validMoves[ThreadLocalRandom.current().nextInt(validMoves.size())];
    }
 }

MeasureBot looks if it needs to capture something: if it does not, it just makes a random move. If it does, it will decide what piece to take: it will choose one with a lower piece value because those can capture fewer of its own pieces. And if there are multiple ways to take a piece with the lowest possible value, it will capture it with the piece with the highest possible value: if it does this, it will bring the capturing piece closer to other pieces (in the beginning of the game, at least) and you'd rather lose a higher-valued piece than a lower-valued one.

This is a list of the piece values I used:

  • King: 1
  • Pawn: 1.5
  • Knight: 2.5
  • Bishop: 3
  • Rook: 5
  • Queen: 9

When a pawn promotes, it will always promote to a king, because it's the lowest-valued piece.

ProgramFOX

Posted 2015-12-21T02:35:09.720

Reputation: 8 017