King-Pen! (Puntos y cajas)


23

Este es un desafío del rey de la colina para Dots and Boxes (también conocido como Pen the Pig). El juego es simple, en tu turno solo dibuja una línea en una cerca vacía. Cada vez que completas un cuadrado obtienes un punto. Además, dado que estamos jugando según las reglas del campeonato , si completas al menos una casilla en tu turno, obtienes un turno adicional. Este es un torneo round robin, donde cada bot juega entre sí dos veces 12 veces en una cuadrícula de 9x9. Echa un vistazo a este partido entre dos titanes de peso pesado, donde ChainCollector hace carne picada del actual co-campeón Asdf: ingrese la descripción de la imagen aquí

Reglas

  1. Límite de tiempo de 0.5 segundos por movimiento.
  2. No interferir con otros bots.
  3. Use PigPen.random () y PigPen.random (int) para aleatoriedad.
  4. No escribir en archivos.
  5. Bot y todos sus datos persistentes se restablecerán cada vez que el oponente cambie (cada 12 rondas).

Bots

Cada bot extiende Player.java:

package pigpen;

public abstract class Player {

public abstract int[] pick(Board board, int id, int round); 

}

Boardes el tablero de juego, que sirve principalmente para darte acceso a Penclases, y ides tu ID de jugador (te dice si eres el primero o el segundo), roundte dice en qué ronda juegas contra el mismo oponente (1 o 2). El valor de retorno es an int[], donde el primer elemento es el penID (1 indexado) y el segundo elemento es el fenceID (0 indexado). Vea Pen.pick(int)una manera fácil de generar este valor de retorno. Vea la página de Github , por ejemplo, reproductores y JavaDoc. Como solo estamos usando una cuadrícula cuadrada, ignoremos las funciones y campos relacionados con los hexágonos.

Como correr

  1. Descargar Source de Github.
  2. Escriba su bot controlador (asegúrese de incluir package pigpen.players) y póngalo en la src/carpeta;
  3. Compilar con javac -cp src/* -d . src/*.java. Ejecutar con java pigpen.Tournament 4 9 9 false(los dos últimos números se pueden cambiar para ajustar el tamaño de la cuadrícula. La última variable solo debe establecerse en truesi desea utilizar el software pp_record).

Puntuaciones

  1. ChainCollector: 72
  2. Asdf: 57
  3. Lazybones: 51
  4. Finisher: 36
  5. = LinearPlayer: 18
  6. = BackwardPlayer: 18
  7. RandomPlayer: 0

Ver también:

Nota : este juego es un desafío competitivo y no es fácil de resolver, debido a que les da a los jugadores un turno adicional para completar un cuadro.

¡Gracias a Nathan Merrill y Darrel Hoffman por consultar sobre este desafío!

Actualizaciones :

  • Se agregó un moves(int player)método a la clase Tablero para obtener una lista de cada movimiento que un jugador ha realizado.

Recompensa indefinida (100 Rep) :

Primera persona en publicar una solución que gana cada ronda, y utiliza la estrategia (ajustar el juego en función de observar cómo juega el oponente).


2
BONDAD. Finisher es waaayyyy OP! : P
El'endia Starman

@ El'endiaStarman Lol, todo lo que hace es terminar un Bolígrafo con una cerca disponible, o de lo contrario elige un Bolígrafo con la mayor cantidad de cercas restantes. RandomPlayer es simplemente aleatorio.
geokavel

2
Si lo se. Es solo que el puntaje final es 79-2 y RandomPlayer solo obtuvo esos dos últimos cuadros porque tenía que hacerlo. : P
El'endia Starman

¡Hola! Quiero hacer mi propio bot, pero tengo una pregunta. ¿Pen.BOTTOM en la fila 0 col 0 devolverá los mismos valores que Pen.TOP en la fila 1 col 0?
tuskiomi

@tusk Sí, lo hace
geokavel

Respuestas:


6

Vago

Este bot es vago. Elige un lugar y una dirección al azar y continúa construyendo en esa dirección sin moverse demasiado. Solo hay 2 casos en los que hace algo diferente:

  • "ganar dinero" cerrando una clavija con solo 1 cerca restante
  • elija un nuevo lugar y dirección si no es posible colocar la cerca o si permitiría al otro bot "ganar dinero"
package pigpen.players;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import pigpen.Board;
import pigpen.Pen;
import pigpen.PigPen;
import pigpen.Player;

public class Lazybones extends Player {
    private static class Fence {
        private static boolean isOk(Board board, boolean vertical, int row, int col) {
            if (vertical) {
                Pen left = board.getPenAt(row, col - 1);
                Pen right = board.getPenAt(row, col);
                if (left.id() < 0 && right.id() < 0 ||
                        left.fences()[Pen.RIGHT] > 0 ||
                        right.fences()[Pen.LEFT] > 0 ||
                        left.remaining() == 2 ||
                        right.remaining() == 2) {
                    return false;
                }
            } else {
                Pen top = board.getPenAt(row - 1, col);
                Pen bottom = board.getPenAt(row, col);
                if (top.id() < 0 && bottom.id() < 0 ||
                        top.fences()[Pen.BOTTOM] > 0 ||
                        bottom.fences()[Pen.TOP] > 0 ||
                        top.remaining() == 2 ||
                        bottom.remaining() == 2) {
                    return false;
                }
            }
            return true;
        }

        private static Fence pickRandom(Board board) {
            List<Fence> ok = new ArrayList<>();
            List<Fence> notOk = new ArrayList<>();
            for (int row = 0; row < board.rows; row ++) {
                for (int col = 0; col < board.cols; col ++) {
                    (isOk(board, false, row, col) ? ok : notOk)
                            .add(new Fence(false, row, col));
                    (isOk(board, true, row, col) ? ok : notOk)
                            .add(new Fence(true, row, col));
                }
                (isOk(board, true, row, board.cols) ? ok : notOk)
                        .add(new Fence(true, row, board.cols));
            }
            for (int col = 0; col < board.cols; col ++) {
                (isOk(board, false, board.rows, col) ? ok : notOk)
                        .add(new Fence(false, board.rows, col));
            }
            if (ok.isEmpty()) {
                return notOk.get(PigPen.random(notOk.size()));
            } else {
                return ok.get(PigPen.random(ok.size()));
            }
        }

        private final boolean vertical;
        private final int row;
        private final int col;

        public Fence(boolean vertical, int row, int col) {
            super();
            this.vertical = vertical;
            this.row = row;
            this.col = col;
        }

        private Fence next(Board board, boolean negative) {
            int newRow = vertical ? (negative ? row - 1 : row + 1) : row;
            int newCol = vertical ? col : (negative ? col - 1 : col + 1);
            if (isOk(board, vertical, newRow, newCol)) {
                return new Fence(vertical, newRow, newCol);
            } else {
                return null;
            }
        }

        private int[] getResult(Board board) {
            if (vertical) {
                if (col < board.cols) {
                    return board.getPenAt(row, col).pick(Pen.LEFT);
                } else {
                    return board.getPenAt(row, col - 1).pick(Pen.RIGHT);
                }
            } else {
                if (row < board.rows) {
                    return board.getPenAt(row, col).pick(Pen.TOP);
                } else {
                    return board.getPenAt(row - 1, col).pick(Pen.BOTTOM);
                }
            }
        }
    }

    private Fence lastFence = null;
    private boolean negative = false;

    @Override
    public int[] pick(Board board, int id, int round) {
        List<Pen> money = board.getList().stream()
                .filter(p -> p.remaining() == 1).collect(Collectors.toList());
        if (!money.isEmpty()) {
            return money.get(PigPen.random(money.size())).pick(Pen.TOP);
        }
        if (lastFence != null) {
            lastFence = lastFence.next(board, negative);
        }
        if (lastFence == null) {
            lastFence = Fence.pickRandom(board);
            negative = PigPen.random(2) == 0;
        }
        return lastFence.getResult(board);
    }
}

Wow, buen trabajo! LazyBones posee el finalizador (ver nueva animación).
geokavel

Por cierto, para que todos lo sepan, otra forma de colocar el lápiz a la izquierda de un lápiz determinado es pen.n(Pen.LEFT)(función de vecino).
geokavel

Además, creo que es innecesario cuando revisas la valla inferior de un Bolígrafo y la valla superior de la que está debajo, ¡se garantiza que tienen el mismo valor!
geokavel

El pick()método ahora tiene un int roundparámetro al final, así que si pudiera agregarlo.
geokavel

Tengo que verificar ambas vallas, porque cualquiera de los objetos de la pluma puede estar fuera del tablero (id == -1). Por la misma razón, no puedo usar la función de vecino.
Sleafar

6

ChainCollector

A este bot le gustan las cadenas 1 . Él quiere tanto de ellos como sea posible. A veces incluso sacrifica una pequeña parte de una cadena para ganar una más grande.

[1] Una cadena consiste en plumas conectadas por cercas abiertas, donde cada pluma tiene 1 o 2 cercas abiertas. Si se puede terminar un solo bolígrafo que pertenece a la cadena, entonces, debido a la regla del campeonato, también se puede terminar toda la cadena.

package pigpen.players;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;

import pigpen.Board;
import pigpen.Pen;
import pigpen.Player;

public class ChainCollector extends Player {
    private enum Direction {
        TOP, RIGHT, BOTTOM, LEFT;

        public Direction opposite() {
            return values()[(ordinal() + 2) % 4];
        }
    }

    private enum ChainEndType {
        OPEN, CLOSED, LOOP
    }

    private static class PenEx {
        private final int id;
        private final List<Fence> openFences = new ArrayList<>();
        private boolean used = false;

        public PenEx(int id) {
            super();
            this.id = id;
        }

        public void addOpenFence(Direction direction, PenEx child) {
            openFences.add(new Fence(this, direction, child));
            if (child != null) {
                child.openFences.add(new Fence(child, direction.opposite(), this));
            }
        }
    }

    private static class Fence {
        public final PenEx parent;
        public final Direction direction;
        public final PenEx child;

        public Fence(PenEx parent, Direction direction, PenEx child) {
            super();
            this.parent = parent;
            this.direction = direction;
            this.child = child;
        }

        public int[] getMove() {
            if (parent == null) {
                return new int[] { child.id, direction.opposite().ordinal() };
            } else {
                return new int[] { parent.id, direction.ordinal() };
            }
        }
    }

    private static class Moves {
        private final TreeMap<Integer, List<Fence>> map = new TreeMap<>();

        public void add(int score, Fence move) {
            List<Fence> list = map.get(score);
            if (list == null) {
                list = new ArrayList<>();
                map.put(score, list);
            }
            list.add(move);
        }

        public boolean isEmpty() {
            return map.isEmpty();
        }

        public boolean hasExactlyOne() {
            return map.size() == 1 && map.firstEntry().getValue().size() == 1;
        }

        public int getLowestScore() {
            return map.firstKey();
        }

        public int[] getLowMove() {
            return map.firstEntry().getValue().get(0).getMove();
        }

        public int[] getHighMove() {
            return map.lastEntry().getValue().get(0).getMove();
        }
    }

    private static class BoardEx {
        private final List<PenEx> pens = new ArrayList<>();
        private final Moves neutralMoves = new Moves();
        private final Moves finisherMoves = new Moves();
        private final Moves safeFinisherMoves = new Moves();
        private final Moves sacrificeMoves = new Moves();
        private final Moves badMoves = new Moves();

        public BoardEx(Board board) {
            super();
            PenEx[][] tmp = new PenEx[board.rows][board.cols];
            for (int row = 0; row < board.rows; ++row) {
                for (int col = 0; col < board.cols; ++col) {
                    Pen pen = board.getPenAt(row, col);
                    int[] fences = pen.fences();
                    PenEx penEx = new PenEx(pen.id());
                    tmp[row][col] = penEx;
                    pens.add(penEx);
                    if (fences[Pen.TOP] == 0) {
                        penEx.addOpenFence(Direction.TOP, row == 0 ? null : tmp[row - 1][col]);
                    }
                    if (row == board.rows - 1 && fences[Pen.BOTTOM] == 0) {
                        penEx.addOpenFence(Direction.BOTTOM, null);
                    }
                    if (fences[Pen.LEFT] == 0) {
                        penEx.addOpenFence(Direction.LEFT, col == 0 ? null : tmp[row][col - 1]);
                    }
                    if (col == board.cols - 1 && fences[Pen.RIGHT] == 0) {
                        penEx.addOpenFence(Direction.RIGHT, null);
                    }
                }
            }
        }

        private ChainEndType followChain(Fence begin, List<Fence> result) {
            Fence current = begin;
            for (;;) {
                current.parent.used = true;
                result.add(current);
                if (current.child == null) {
                    return ChainEndType.OPEN;
                }
                List<Fence> childFences = current.child.openFences;
                switch (childFences.size()) {
                    case 1:
                        current.child.used = true;
                        return ChainEndType.CLOSED;
                    case 2:
                        if (current.child == begin.parent) {
                            return ChainEndType.LOOP;
                        } else {
                            current = current.direction.opposite() == childFences.get(0).direction ?
                                    childFences.get(1) : childFences.get(0);
                        }
                        break;
                    case 3:
                    case 4:
                        return ChainEndType.OPEN;
                    default:
                        throw new IllegalStateException();
                }
            }
        }

        public void findChains() {
            for (PenEx pen : pens) {
                if (!pen.used && pen.openFences.size() > 0) {
                    if (pen.openFences.size() < 3) {
                        List<Fence> fences = new ArrayList<>();
                        ChainEndType type1 = pen.openFences.size() == 1 ?
                                ChainEndType.CLOSED : followChain(pen.openFences.get(1), fences);
                        if (type1 == ChainEndType.LOOP) {
                            badMoves.add(fences.size(), fences.get(0));
                        } else {
                            Collections.reverse(fences);
                            ChainEndType type2 = followChain(pen.openFences.get(0), fences);
                            if (type1 == ChainEndType.OPEN && type2 == ChainEndType.CLOSED) {
                                type1 = ChainEndType.CLOSED;
                                type2 = ChainEndType.OPEN;
                                Collections.reverse(fences);
                            }
                            if (type1 == ChainEndType.OPEN) {
                                badMoves.add(fences.size() - 1, fences.get(fences.size() / 2));
                            } else if (type2 == ChainEndType.CLOSED) {
                                finisherMoves.add(fences.size() + 1, fences.get(0));
                                if (fences.size() == 3) {
                                    sacrificeMoves.add(fences.size() + 1, fences.get(1));
                                } else {
                                    safeFinisherMoves.add(fences.size() + 1, fences.get(0));
                                }

                            } else {
                                finisherMoves.add(fences.size(), fences.get(0));
                                if (fences.size() == 2) {
                                    sacrificeMoves.add(fences.size(), fences.get(1));
                                } else {
                                    safeFinisherMoves.add(fences.size(), fences.get(0));
                                }
                            }
                        }
                    } else {
                        pen.used = true;
                        for (Fence fence : pen.openFences) {
                            if (fence.child == null || fence.child.openFences.size() > 2) {
                                neutralMoves.add(fence.child == null ? 0 : fence.child.openFences.size(), fence);
                            }
                        }
                    }
                }
            }
        }

        public int[] bestMove() {
            if (!neutralMoves.isEmpty()) {
                if (!finisherMoves.isEmpty()) {
                    return finisherMoves.getHighMove();
                }
                return neutralMoves.getHighMove();
            }
            if (!safeFinisherMoves.isEmpty()) {
                return safeFinisherMoves.getHighMove();
            }
            if (badMoves.isEmpty() && !finisherMoves.isEmpty()) {
                return finisherMoves.getHighMove();
            }
            if (!sacrificeMoves.isEmpty()) {
                if (sacrificeMoves.hasExactlyOne()) {
                    if (badMoves.getLowestScore() - sacrificeMoves.getLowestScore() >= 2) {
                        return sacrificeMoves.getLowMove();
                    } else {
                        return finisherMoves.getHighMove();
                    }
                } else {
                    return finisherMoves.getHighMove();
                }
            }
            if (!badMoves.isEmpty()) {
                return badMoves.getLowMove();
            }
            return null;
        }
    }

    @Override
    public int[] pick(Board board, int id, int round) {
        BoardEx boardEx = new BoardEx(board);
        boardEx.findChains();
        return boardEx.bestMove();
    }
}

Wow, gracias por tu entrada. Me alegra que alguien haya invertido tanto tiempo en un proyecto que creé. Creo que la introducción de este bot ha afectado la generación de números aleatorios, de modo que Asdf ahora vence a Lazybones en ambas ocasiones por un ligero margen.
geokavel

Bueno, la idea del bot parecía bastante simple antes de comenzar, y luego quise terminarla. ;) Con la aleatoriedad involucrada, probablemente deberías dejar que los bots jueguen más de 2 juegos para obtener resultados más precisos.
Sleafar

Buen pensamiento. Lo aumenté a 12 rondas por enfrentamiento, y ahora, como puedes ver, Asdf tiene una ligera ventaja. Incluso en 100 rondas, solo gana 13 juegos más que Lazybones.
geokavel

3

Acabador

package pigpen.players;

import pigpen.*;

import java.util.*;

/**
 * Picks a Pen with only one fence remaining. 
 * Otherwise picks one with the most fences remaining
 */
public class Finisher extends Player implements Comparator<Pen> {


  public int[] pick(Board board, int id) {
     return Collections.max(board.getList(),this).pick(Pen.TOP);

  }

  @Override
  public int compare(Pen p1, Pen p2) {
    //1 remaining is best, all remaining is second.
    int r1 = p1.remaining();
    int r2 = p2.remaining();
    if(r1 == 1) r1 = 7;
    if(r2 == 1) r2 = 7;
    return Integer.compare(r1,r2);
 }


}

Utiliza un comparador para elegir el bolígrafo con la mayor cantidad de cercas disponibles, pero le da prioridad al bolígrafo con solo 1 cerca disponible. (7 se usa en lugar de 5 para permitir que este código funcione también en modo hexágono)


3

Asdf

Asigna un puntaje a cada cerca y luego saca lo mejor de ellos. Por ejemplo: un bolígrafo con una valla abierta tiene una puntuación de 10, mientras que un bolígrafo con 2 vallas abiertas tiene una puntuación de -8.

Parece que Lazybones usa una estrategia similar, porque se vincula con este bot.

package pigpen.players;

import java.util.*;
import pigpen.*;

public class Asdf extends Player {
    private final List<Score> scores = new ArrayList<>();

    @Override
    public int[] pick(Board board, int id, int round) {
        scores.clear();
        List<Pen> pens = board.getList();

        pens.stream().filter(x -> !x.closed()).forEach((Pen p) -> evaluate(p));
        Optional<Score> best = scores.stream().max(Comparator.comparingInt(p -> p.points));

        if (best.isPresent()) {
            Score score = best.get();
            return score.pen.pick(score.fence);
        }
        return null;
    }

    private void evaluate(Pen pen) {
        int[] fences = pen.fences();
        for (int i = 0; i < fences.length; i++) {
            if (fences[i] == 0) {
                int points = getPoints(pen);
                Pen neighbour = pen.n(i);
                if (neighbour.id() != -1) {
                    points += getPoints(neighbour);
                }
                scores.add(new Score(pen, i, points));
            }
        }
    }

    private int getPoints(Pen pen) {
        switch (pen.remaining()) {
            case 1: return 10;
            case 2: return -1;
            case 3: return 1;
        }
        return 0;
    }

    class Score {
        private Pen pen;
        private int fence;
        private int points;

        Score(Pen pen, int fence, int points) {
            this.pen = pen;
            this.fence = fence;
            this.points = points;
        }
    }
}

Aquí están los puntajes. Es interesante que quien vaya segundo obtenga el doble de puntos. Asdf vs. Lazybones: 27 - 54; Lazybones vs.Asdf: 27 - 54
geokavel

@geokavel Sí, porque entonces los bots se ven obligados a hacer un "mal giro", por lo que el oponente puede cerrar un bolígrafo.
CommonGuy

¿Es este el mejor algoritmo posible, entonces?
justhalf

@justhalf No lo es, porque la gente juega este juego en campeonatos. Creo que estos algoritmos definitivamente se pueden ampliar. Vea los enlaces que proporcioné para más información.
geokavel

0

Jugador lineal

package pigpen.players;

import pigpen.*;

/**
 * Picks the first available fence in the first available Pen
 */ 
public class LinearPlayer extends Player {


@Override
public int[] pick(Board board, int id) {
    for(int p = 1;p<=board.size;p++) {
        Pen pen = board.get(p);
            if(!pen.closed()) {
                int[] fences = pen.fences();
                    for(int i =0;i<fences.length;i++) {
                        if(fences[i] == 0) {
                            return new int[]{pen.id(),i};
                        }
                    }
                }
        }
    return new int[]{1,0};
    } 
}

La forma más fácil de escribir este bot es en realidad return null, porque una entrada no válida seleccionará automáticamente la primera cerca disponible. Este código no utiliza ningún método de acceso directo y genera manualmente el valor de retorno.


0

BackwardPlayer

package pigpen.players;

import pigpen.*;

/**
 * Picks the first available fence in the last available Pen
 */
 public class BackwardPlayer extends Player {

public int[] pick(Board board, int id) {
    for(int i = board.size;i>0;i--) {
        Pen p = board.get(i);
        if(!p.closed()) {
            return p.pick(Pen.TOP);
        }
    }
    return new int[] {1,0};
}
}

Este código utiliza el método de acceso directo Pen.pick(int)para generar el valor de retorno. Si la cerca superior no está disponible, elegirá la cerca disponible más cercana en sentido horario.


0

RandomPlayer

package pigpen.players;

import pigpen.*;


/** 
 * Picks the first available fence in a random Pen 
 */
public class RandomPlayer extends Player {
    public int[] pick(Board board, int id) {
        int pen = PigPen.random(board.size)+1;
        return board.get(pen).pick(Pen.TOP);
    }
}

La misma idea que BackwardPlayer, pero selecciona aleatoriamente un lápiz. Tenga en cuenta que los +1Pen son indexados en 1.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.