King-Pen! (Dots and Boxes)

23

Ini adalah tantangan raja bukit untuk Dots and Boxes (alias Pen the Pig). Permainan ini sederhana, pada gilirannya Anda hanya menggambar garis di pagar yang kosong. Setiap kali Anda menyelesaikan kotak Anda mendapatkan poin. Juga, karena kami bermain berdasarkan aturan kejuaraan , jika Anda menyelesaikan setidaknya satu kotak pada giliran Anda, Anda mendapat giliran tambahan. Ini adalah turnamen round robin, di mana setiap bot memainkan satu sama lain bot dua kali 12 kali pada kotak 9x9. Lihatlah pertandingan ini di antara dua titans kelas berat, di mana ChainCollector membuat daging cincang dari co-champion Asdf: masukkan deskripsi gambar di sini

Aturan

  1. Batas waktu 0,5 detik per gerakan.
  2. Tidak mengganggu bot lainnya.
  3. Gunakan PigPen.random () dan PigPen.random (int) untuk keacakan.
  4. Tidak ada tulisan untuk file.
  5. Bot dan semua data gigihnya akan diatur ulang setiap kali lawan berubah (setiap 12 putaran).

Bot

Setiap bot memperluas Player.java:

package pigpen;

public abstract class Player {

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

}

Boardadalah papan permainan, yang utamanya berfungsi untuk memberi Anda akses ke Penkelas, dan idmerupakan kartu ID Anda (memberi tahu Anda jika Anda yang pertama atau kedua), roundmemberi tahu Anda babak mana permainan Anda melawan lawan yang sama (1 atau 2). Nilai kembali adalah int[], di mana elemen pertama adalah penID (1-diindeks), dan elemen kedua adalah pagarID (0-diindeks). Lihat Pen.pick(int)cara mudah untuk menghasilkan nilai pengembalian ini. Lihat halaman Github untuk contoh pemain dan JavaDoc. Karena kita hanya menggunakan kotak persegi, abaikan fungsi dan bidang apa pun yang terkait dengan segi enam.

Cara Menjalankan

  1. Unduh Sumber dari Github.
  2. Tulis bot pengontrol Anda (pastikan untuk memasukkan package pigpen.players) dan letakkan di src/folder;
  3. Kompilasi dengan javac -cp src/* -d . src/*.java. Jalankan dengan java pigpen.Tournament 4 9 9 false(dua angka terakhir dapat diubah untuk menyesuaikan ukuran kisi. Variabel terakhir hanya boleh diatur truejika Anda ingin menggunakan perangkat lunak pp_record.)

Skor

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

Lihat juga:

Catatan : permainan ini adalah tantangan kompetitif dan tidak mudah dipecahkan, karena memberi pemain giliran ekstra untuk menyelesaikan kotak.

Terima kasih kepada Nathan Merrill dan Darrel Hoffman yang telah berkonsultasi tentang tantangan ini!

Pembaruan :

  • Menambahkan moves(int player)metode ke kelas Dewan untuk mendapatkan daftar setiap gerakan yang dilakukan pemain.

Hadiah Tidak Terbatas (100 Rep) :

Orang pertama yang memposting solusi yang memenangkan setiap putaran, dan menggunakan strategi (menyesuaikan permainan berdasarkan mengamati bagaimana lawan bermain).

geokavel
sumber
2
KEBAIKAN. Finisher adalah waaayyyy OP! : P
El'endia Starman
@ El'endiaStarman Lol, yang dia lakukan hanyalah menyelesaikan Pena dengan satu pagar tersedia, atau mengambil Pena dengan pagar yang tersisa. RandomPlayer hanya acak.
geokavel
2
Ya aku tahu. Hanya saja skor akhirnya adalah 79-2 dan RandomPlayer hanya mendapatkan dua kotak terakhir karena harus . : P
El'endia Starman
Halo! Saya ingin membuat bot sendiri, tetapi saya punya pertanyaan. akankah Pen.BOTTOM pada baris 0 col 0 mengembalikan nilai yang sama dengan Pen.TOP pada baris 1 col 0?
tuskiomi
@tusk Ya, benar
geokavel

Jawaban:

6

Pemalas

Bot ini malas. Dia memilih tempat dan arah acak dan terus membangun ke arah itu tanpa bergerak terlalu banyak. Hanya ada 2 kasus di mana ia melakukan sesuatu yang berbeda:

  • "hasilkan uang" dengan menutup patok dengan hanya 1 pagar yang tersisa
  • pilih tempat dan arah baru jika menempatkan pagar tidak memungkinkan atau akan membiarkan bot lain "menghasilkan uang"
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
sumber
Wow, kerja bagus! LazyBones memiliki finisher (lihat animasi baru).
geokavel
By the way, supaya semua orang tahu, cara lain untuk mendapatkan Pena di sebelah kiri pena yang diberikan adalah pen.n(Pen.LEFT)(fungsi tetangga).
geokavel
Juga, saya pikir itu tidak perlu ketika Anda memeriksa pagar bawah Pena dan pagar atas yang di bawahnya, mereka dijamin memiliki nilai yang sama!
geokavel
The pick()Metode sekarang memiliki int roundparameter di akhir, jadi jika Anda bisa tolong menambahkan bahwa.
geokavel
Saya harus memeriksa kedua pagar, karena salah satu objek pena dapat berada di luar papan (id == -1). Untuk alasan yang sama saya tidak bisa menggunakan fungsi tetangga.
Sleafar
6

ChainCollector

Bot ini menyukai rantai 1 . Dia ingin sebanyak mungkin dari mereka. Kadang-kadang dia bahkan mengorbankan sebagian kecil dari rantai untuk memenangkan yang lebih besar.

[1] Rantai terdiri dari pena yang dihubungkan oleh pagar terbuka, di mana setiap pena memiliki 1 atau 2 pagar terbuka. Jika satu pena milik rantai dapat diselesaikan, maka karena aturan kejuaraan seluruh rantai dapat diselesaikan juga.

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
sumber
Wow, terima kasih atas kiriman Anda. Saya merasa rendah hati bahwa seseorang telah menghabiskan begitu banyak waktu dalam proyek yang saya buat. Saya pikir pengenalan bot ini telah mempengaruhi generasi nomor acak, sehingga Asdf sekarang mengalahkan Lazybones dua kali dengan selisih yang sedikit.
geokavel
Ide untuk bot itu terlihat sangat sederhana sebelum saya mulai, dan kemudian saya ingin menyelesaikannya. ;) Dengan keacakan, Anda mungkin harus membiarkan bot memainkan lebih dari 2 game untuk mendapatkan hasil yang lebih akurat.
Sleafar
Pikiran yang bagus. Saya meningkatkannya menjadi 12 putaran per pertarungan, dan sekarang, seperti yang Anda lihat, Asdf memiliki sedikit keunggulan. Bahkan pada 100 putaran, itu hanya memenangkan 13 pertandingan lebih banyak dari Lazybones.
geokavel
3

Finisher

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


}

Menggunakan Pembanding untuk memilih Pena dengan pagar yang paling tersedia, tetapi mengutamakan Pena dengan hanya 1 pagar yang tersedia. (7 digunakan daripada 5 untuk memungkinkan kode ini bekerja dalam mode segi enam juga)

geokavel
sumber
3

Asdf

Tetapkan skor untuk setiap pagar dan kemudian pilih yang terbaik dari mereka. Misalnya: Pena dengan satu pagar terbuka memiliki skor 10, sedangkan pena dengan 2 pagar terbuka memiliki skor -8.

Sepertinya Lazybones menggunakan strategi yang sama, karena ini terkait dengan bot ini.

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
sumber
Inilah nilainya. Sangat menarik bahwa siapa pun yang berada di urutan kedua mendapat poin dua kali lebih banyak. Asdf vs. Lazybones: 27 - 54; Lazybones vs. Asdf: 27 - 54
geokavel
@geokavel Ya, karena bot dipaksa untuk melakukan "giliran buruk", sehingga lawan dapat menutup pena.
CommonGuy
Apakah ini algoritma yang sebaik mungkin?
justhalf
@justhalf Bukan, karena orang memainkan game ini di kejuaraan. Saya pikir algoritma ini pasti dapat diperluas. Lihat tautan yang saya berikan untuk info lebih lanjut.
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};
    } 
}

Cara termudah untuk menulis bot ini sebenarnya return null, karena entri yang tidak valid akan secara otomatis memilih pagar pertama yang tersedia. Kode ini tidak menggunakan metode pintas apa pun dan secara manual menghasilkan nilai kembali.

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

Kode ini menggunakan metode pintas Pen.pick(int)untuk menghasilkan nilai kembali. Jika pagar atas tidak tersedia, itu akan memilih pagar terdekat yang tersedia searah jarum jam.

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

Ide yang sama dengan BackwardPlayer, tetapi secara acak memilih pena. Perhatikan +1karena Pena 1-diindeks.

geokavel
sumber