King-Pen! (Kropki i pudełka)

23

Jest to wyzwanie króla wzgórza dla kropek i pudeł (zwanych również Pen the Pig). Gra jest prosta, w swojej turze po prostu narysuj linię na pustym ogrodzeniu. Za każdym razem, gdy wypełnisz kwadrat, dostajesz punkt. Ponadto, ponieważ gramy zgodnie z zasadami mistrzostw , jeśli wykonasz przynajmniej jedno pole w swojej turze, otrzymasz dodatkową turę. Jest to okrągły turniej robin, w którym każdy bot gra ze sobą dwa razy 12 razy na siatce 9x9. Sprawdź mecz dwóch tytanów wagi ciężkiej, gdzie ChainCollector produkuje mięso mielącego panującego współwyciężyciela Asdf: wprowadź opis zdjęcia tutaj

Zasady

  1. Limit 0,5 sekundy na ruch.
  2. Bez ingerencji w inne boty.
  3. Użyj PigPen.random () i PigPen.random (int) dla losowości.
  4. Brak zapisu do plików.
  5. Bot i wszystkie jego trwałe dane będą resetowane przy każdej zmianie przeciwnika (co 12 rund).

Boty

Każdy bot rozszerza Player.java:

package pigpen;

public abstract class Player {

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

}

Boardjest planszą, która służy głównie do zapewnienia dostępu do Penklas, i idjest twoim ID gracza (informuje, czy jesteś pierwszy, czy drugi), roundinformuje, w której rundzie grasz z tym samym przeciwnikiem (1 lub 2). int[]Zwracana jest wartość , gdzie pierwszym elementem jest penID (indeksowany 1), a drugim elementem jest fenceID (indeksowany 0). Znajdź Pen.pick(int)prosty sposób na wygenerowanie tej zwracanej wartości. Zobacz stronę Github, na przykład odtwarzacze i JavaDoc. Ponieważ używamy tylko kwadratowej siatki, zignoruj ​​wszelkie funkcje i pola związane z sześciokątami.

Jak biegać

  1. Pobierz źródło z Github.
  2. Napisz bota kontrolera (pamiętaj o dołączeniu package pigpen.players) i umieść go w src/folderze;
  3. Kompiluj z javac -cp src/* -d . src/*.java. Uruchom z java pigpen.Tournament 4 9 9 false(dwie ostatnie liczby można zmienić, aby dostosować rozmiar siatki. Ostatnia zmienna powinna być ustawiona na, truejeśli chcesz używać oprogramowania pp_record.)

Wyniki

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

Zobacz też:

Uwaga : ta gra jest wyzwaniem konkurencyjnym i nie jest łatwa do rozwiązania, ponieważ daje graczom dodatkową turę za wypełnienie pudełka.

Dziękujemy Nathanowi Merrillowi i Darrel Hoffman za konsultacje w sprawie tego wyzwania!

Aktualizacje :

  • Dodano moves(int player)metodę do klasy Board, aby uzyskać listę każdego ruchu wykonanego przez gracza.

Nieokreślona nagroda (100 powtórzeń) :

Pierwsza osoba, która opublikuje rozwiązanie, które wygrywa każdą rundę i stosuje strategię (dostosowanie gry na podstawie obserwacji gry przeciwnika).

geokavel
źródło
2
DOBROĆ. Finisher jest waaayyyy OP! : P
El'endia Starman,
@ El'endiaStarman Lol, wszystko, co robi, to dokończyć długopis z jednym dostępnym ogrodzeniem lub w inny sposób wybrać długopis z największą liczbą pozostałych ogrodzeń. RandomPlayer jest po prostu losowy.
geokavel,
2
Tak, wiem. Tyle tylko, że końcowy wynik to 79-2, a RandomPlayer dostał tylko te dwa ostatnie pola, ponieważ musiał . : P
El'endia Starman,
Dzień dobry! Chcę stworzyć własnego bota, ale mam pytanie. czy Pen.BOTTOM w wierszu 0 kolumna 0 zwróci takie same wartości jak Pen.TOP w wierszu 1 kolumna 0?
tuskiomi
@tusk Tak, to robi
geokavel,

Odpowiedzi:

6

Leń

Ten bot jest leniwy. Wybiera losowe miejsce i kierunek i kontynuuje budowanie w tym kierunku, nie poruszając się zbytnio. Są tylko 2 przypadki, w których robi coś innego:

  • „zarabiaj”, zamykając kołek z tylko 1 ogrodzeniem
  • wybierz nowe miejsce i kierunek, jeśli umieszczenie ogrodzenia nie jest możliwe lub pozwoliłoby drugiemu botowi na „zarabianie pieniędzy”
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);
    }
}
Sleafar
źródło
Wow, niezła robota! LazyBones jest właścicielem finishera (zobacz nową animację).
geokavel
Nawiasem mówiąc, tak jak wszyscy wiedzą, innym sposobem na przeniesienie pióra na lewo od danego pióra jest pen.n(Pen.LEFT)(funkcja sąsiada).
geokavel,
Ponadto uważam, że nie jest konieczne, aby sprawdzić dolne ogrodzenie długopisu i górne ogrodzenie tego pod nim, mają one taką samą wartość!
geokavel,
pick()Metoda ma teraz int roundparametr na końcu, więc jeśli można proszę dodać, że.
geokavel
Muszę sprawdzić oba ogrodzenia, ponieważ dowolny obiekt pióra może znajdować się poza planszą (id == -1). Z tego samego powodu nie mogę użyć funkcji sąsiada.
Sleafar,
6

ChainCollector

Ten bot lubi łańcuchy 1 . Chce ich jak najwięcej. Czasami poświęca nawet niewielką część łańcucha, aby wygrać większą.

[1] Łańcuch składa się z długopisów połączonych otwartymi ogrodzeniami, przy czym każde pióro ma 1 lub 2 otwarte ogrodzenia. Jeśli pojedynczy długopis należący do łańcucha można ukończyć, to dzięki regule mistrzowskiej można również ukończyć cały łańcuch.

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();
    }
}
Sleafar
źródło
Wow, dziękuję za twój wpis. Jestem pokorny, że ktoś poświęcił tak dużo czasu na projekt, który stworzyłem. Myślę, że wprowadzenie tego bota wpłynęło na generowanie liczb losowych, tak że Asdf teraz bije Lazybones dwa razy nieznacznie.
geokavel,
Cóż, pomysł na bota wyglądał dość prosto, zanim zacząłem, a potem chciałem go zakończyć. ;) Przy losowości powinieneś prawdopodobnie pozwolić botom grać w więcej niż 2 gry, aby uzyskać dokładniejsze wyniki.
Sleafar,
Dobra myśl. Zwiększyłem go do 12 rund na pojedynek, a teraz, jak widać, Asdf ma niewielką przewagę. Nawet w 100 rundach wygrywa tylko 13 gier więcej niż Lazybones.
geokavel
3

Apreter

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);
 }


}

Używa Komparatora do wybierania Pióra z najbardziej dostępnymi ogrodzeniami, ale daje pierwszeństwo Piórem z dostępnym tylko 1 ogrodzeniem. (7 jest używane zamiast 5, aby umożliwić działanie tego kodu również w trybie sześciokąta)

geokavel
źródło
3

Asdf

Przypisuje wynik do każdego ogrodzenia, a następnie wybiera najlepsze z nich. Na przykład: długopis z jednym otwartym płotem ma wynik 10, a długopis z 2 otwartym płotem ma wynik -8.

Wygląda na to, że Lazybones stosuje podobną strategię, ponieważ wiąże się z tym botem.

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;
        }
    }
}
CommonGuy
źródło
Oto wyniki. Interesujące jest to, że ktokolwiek jest drugi, dostaje dwa razy więcej punktów. Asdf vs. Lazybones: 27 - 54; Lazybones vs. Asdf: 27 - 54
geokavel
@geokavel Tak, ponieważ wtedy boty są zmuszane do wykonania „złego obrotu”, aby przeciwnik mógł zamknąć długopis.
CommonGuy,
Czy to najlepszy algorytm?
justhalf
@ justhalf To nie jest, ponieważ ludzie grają w tę grę podczas mistrzostw. Myślę, że te algorytmy można zdecydowanie rozszerzyć. Zobacz linki, które podałem, aby uzyskać więcej informacji.
geokavel
0

LinearPlayer

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};
    } 
}

Najłatwiejszym sposobem na napisanie tego bota jest return null, ponieważ nieprawidłowy wpis automatycznie wybierze pierwsze dostępne ogrodzenie. Ten kod nie używa żadnych metod skrótów i ręcznie generuje zwracaną wartość.

geokavel
źródło
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};
}
}

Ten kod używa metody skrótu Pen.pick(int)do generowania wartości zwracanej. Jeśli górne ogrodzenie jest niedostępne, wybierze najbliższe dostępne ogrodzenie, postępując zgodnie z ruchem wskazówek zegara.

geokavel
źródło
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);
    }
}

Ten sam pomysł, co BackwardPlayer, ale losowo wybiera pióro. Zwróć uwagę, +1ponieważ Pen są indeksowane 1.

geokavel
źródło