Commit f22d48f3 authored by Alex Ochs's avatar Alex Ochs ✌🏼
Browse files

Initial commit

parents
# Vier Gewinnt! aka Connect Four! (PiS, SoSe 2020)
Autor: Alex Ochs, 5272927
Ich habe die Zulassung für PiS im SoSe 2020 bei Herrn Herzberg erhalten.
<Inhaltsverzeichnis>
## Einleitung
### Spielregeln
Ihnen sollte "Vier gewinnt" sicherlich bekannt sein.
Der Spieler (O) hat immer den ersten Zug.
Abwechselnd legt man ein Zeichen in eine verfügbare Reihe.
Der erste Spieler mit 4 Zeichen in einer Reihe (horizontal, vertikal, diagonal) gewinnt.
### Bedienungsanleitung
![Screenshot](screenshot.png)
Nach dem Start mit "gradle run" ist das GUI über localhost:7070 zu erreichen.
* **Neues Spiel:** Startet ein neues Spiel.
* **CPU Zug:** Lässt den Computer Ihren Zug spielen. (Der CPU spielt immer automatisch nach Ihrem Zug.)
* **Zug zurücknehmen:** Nimmt **Ihren** Zug zurück. Es wird also das vorletzte Bitboard ausgegeben (vor dem Zug des CPUs).
* **Test ausführen:** Lädt das von Ihnen gewählte Test-Bitboard.
* **Konsole:** Hier kommuniziert das Spiel mit Ihnen und informiert Sie über die momentane Spielsituation.
* **Letzte CPU Move Ratings:** Hier werden die CPU-Move-Ratings ausgegeben, nach denen sich der Computer/Algorithmus bei seinem letzten Zug entschieden hat. Für den Computer (X) ist eine niedrige Zahl gut, für den Spieler (O) eine hohe.
### Dateiübersicht
.
├── build.gradle
├── database
├── lastmoverating (wird erst nach CPU-Zug erstellt)
├── README.md
├── screenshot.png
└── src
└── main
├── kotlin
│   └── viergewinnt
│   ├── App.kt
│   ├── Bitboard.kt
│   ├── ConnectFour.kt
│   ├── IBitboard.kt
│   └── IConnectFour.kt
└── resources
└── public
├── index.html
├── scripts.js
└── styles.css
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Kotlin 5 43 43 370
Markdown 1 44 0 140
HTML 1 4 4 107
JavaScript 1 10 0 47
CSS 1 4 0 18
Gradle 1 9 14 18
-------------------------------------------------------------------------------
SUM: 10 114 61 700
-------------------------------------------------------------------------------
Ohne Md/Gradle: 542
JavaScript-Anteil: 140/542 = 8.67%
-------------------------------------------------------------------------------
## Spiel-Engine (ENG)
Feature | AB | H | MC | eD | B+I+Im | Summe
-----------|-----|-----|-----|-----|------|----
Umsetzung | 120 | 70 | 100 | 130 | 99.9 |
Gewichtung | 0.4 | 0.3 | 0.3 | 0.3 | 0.3 |
Ergebnis | ? | ? | ? | ? | ? | **128%**
Der Kern der Engine ist die *ConnectFour*-Klasse, sie repräsentiert eine Instanz des Spiels und ist vollkommen immutabel.
Neben Funktionen besitzt die Klasse eine "Root-Instanz" (um Züge zurückzunehmen), ein **Bitboard** und eine **Hash-Map**, welche sich alle Klassen-Instanzen teilen.
Die Interaktion zwischen GUI und Engine geschieht in *App.kt* bzw. *Javalin*.
Je nach Request erstelle ich eine neue *ConnectFour*-Instanz ausgehend von der Momentanen.
Alle notwendigen Daten der neuen Spiel-Instanz werden dann in einen JSON-String verpackt und an das GUI bzw. den Browser zurückgegeben.
**Alpha-Beta:** Der Alpha-Beta-Algorithmus ist in der *alphabeta*-Funktion von *ConnectFour* zu finden.
Als Basis habe ich [dieses Video](https://www.youtube.com/watch?v=l-hh51ncgDI) verwendet.
Sie wird stets mit einem bereits gespieltem Bitboard als Parameter aufgerufen.
Zuerst wird überprüft ob der bereits gemachte Zug zu einem Gewinn geführt hat, ist dies der Fall so gibt *alphabeta* den entsprechenden Wert zurück, andernfalls verläuft *alphabeta* wie ein gewohnter Alpha-Beta-Algorithmus ab.
Je nachdem wer nun am Zug ist wird *alphabeta* rekursiv für alle möglichen Züge aufgerufen und der bestmögliche Zug zurückgegeben.
Möchte man nun einen Zug dem Computer überlassen, so wird allerdings *cpuMove* aufgerufen und nicht *alphabeta*.
*cpuMove* ruft *alphabeta* für alle möglichen Züge auf und wählt daraus den besten Zug, allerdings geschehen auch noch einige Kleinigkeiten welche nicht in *alphabeta* inbegriffen sind wie das Laden/Speichern der DB und das Speichern der "Move-Ratings".
**Hash-Map:** Die Hash-Map wird als *companion object* in der *ConnectFour*-Klasse weitergegeben, d.h. alle *ConnectFour*-Instanzen teilen sich dieselbe Hash-Map.
Als Key nimmt sie das Ergebnis der *hashCode*-Funktion des Paares (Bitboard, Spieler am Zug).
Der dazugehörige Wert (Value) ist ein Rating für die Spielsituation.
Das Rating wird folgend in der Monte-Carlo-Erläuterung näher gebracht.
**Monte-Carlo:** Um das Rating für eine Spielsituation zu berechnen verwende ich Monte-Carlo-Simulationen.
Doch vorerst: Wie o.g. wird beim Aufruf von *alphabeta* erst überprüft ob das Spiel schon vorbei ist (also jemand gewonnen hat).
Ist dies der Fall so wird ±∞ - ±Counter bzw. Anzahl der Züge zurückgeben (das Endergebnis ist also abhängig von der Menge der Züge und dem Spieler der Gewonnen hat).
Erst wenn eine gegebene Alpha-Beta-Tiefe erreicht ist und es bis dahin keinen Gewinn gab, kommt die Monte-Carlo-Simulation ins Spiel.
Diese ist in der *montecarlo*-Funktion der *ConnectFour*-Klasse zu finden.
Von der momentanen Spielstellung aus wird eine gegebene Anzahl an Spielverläufen (mit zufälligen Zügen) simuliert.
Hat eine Simulation ein Ende erreicht (Gewinn oder keine möglichen Züge mehr), so wird das Rating bei einem Gewinn erhöht, Verlust erniedrigt und Gleichstand gleich gelassen.
Somit wird also eine Spielstellung mit z.B. 6 Siegen und 1 Niederlage besser gewertet als 10 Siege und 6 Niederlagen (6-1 > 10-6).
**Eigene Datenbank:** Die Datenbank ist eine Text-Datei und befindet sich im Root-Ordner des Projekts.
In Ihr wird die Hash-Map gespeichert.
Eine Zeile (die Erste) hat den Wert des Keys, in der folgenden befindet sich die dazugehörige Value.
Bei einem Aufruf der *cpuMove*-Funktion wird die Datenbank Zeile für Zeile eingelesen und in die Hash-Map übertragen (siehe *loadDB*-Funktion).
Nachdem der Computer seinen Zug berechnet hat wird die Hash-Map wieder in die Datenbank geschrieben (siehe *saveDB*-Funktion).
Bei Bedarf kann also die Datenbank gelöscht, Alpha-Beta-Tiefe und Monte-Carlo-Simulation neu bestimmt und die Datenbank wieder neu aufgebaut werden.
Zum Lese-/Schreibevorgang der Text-Datei:
Leider muss man hier die *File*-Klasse der *java.io*-Bibliothek nutzen, da Kotlin für solche Vorgänge keine eigenen Lösungen/Bibliotheken bietet bzw. die von Java nutzt.
**Bitboard:** Bei der Umsetzung des Bitboards habe ich mich **stark** an [D. Herzbergs Design und Pseudocode](https://github.com/denkspuren/BitboardC4/blob/master/BitboardDesign.md) gehalten.
Da die Bitboards sogar schon für "Vier gewinnt" konstruiert waren, musste ich also nicht viel an der Logik ändern, sondern hauptsächlich das Design in Kotlin übertragen.
Als Repräsentation werden zwei Longs benutzt, jeweils pro Spieler, und durch die *makeMove*-Funktion verändert.
*undoMove* gibt das vorherige Bitboard zurück, *isWin* überprüft ob der gegebene Spieler gewonnen hat und *listMoves* gibt eine Liste mit allen möglichen Reihen/Moves zurück.
**Interface:** Jeweils beide Klassen (*ConnectFour* und *Bitboard*) haben ihr dazugehöriges Interface (*IConnectFour* und *IBitboard*).
*Bitboard* ist eine 1:1 Übertragung des *IBitboard*-Interfaces, ohne extra Funktionen oder Daten.
*ConnectFour* hingegen besitzt einige (private) Funktionen die nicht im *IConnectFour*-Interface inbegriffen sind (z.B. *load-/saveDB*, *alphabeta*).
Das *ConnectFour*-Interface bietet dennoch eine stabile Basis falls man weitere *ConnectFour*-Varianten implementieren möchte.
**Immutabilität:** Beide Klassen (*ConnectFour* und *Bitboard*) sind komplett immutabel implementiert.
Alle Klassen-Variablen sind mit *val* initialisiert.
Alle Funktionen, welche die Spielsituation ändern (also alle *move*-Funktion), liefern eine neue Klassen-Instanz.
Alle elementaren Daten (außer die Hash-Map) einer Instanz sind somit alle statisch und immutabel.
## Tests (TST)
Szenario | 1 | 2 | 3 | 4 | 5 | Summe
---------|-----|-----|-----|-----|-----|-------
ok | X | X | - | X | X | 0.8
Die Tests werden wie folgt ausgeführt:
Drücken Sie auf den Button "Test ausführen" und wählen Sie ein eine Sichttiefe (Sichttiefe 1 ≠ Szenario 1).
1. Die Spiel-Engine kann im nächsten Zug gewinnen (Sichttiefe 1)
2. Die Spiel-Engine kann im übernächsten Zug gewinnen (Sichttiefe 3)
3. Die Spiel-Engine kann im überübernächsten Zug gewinnen (Sichttiefe 5)
4. Die Spiel-Engine vereitelt eine unmittelbare Gewinnbedrohung des Gegners (Sichttiefe 2)
5. Die Spiel-Engine vereitelt ein Drohung, die den Gegner im übernächsten Zug ansonsten einen Gewinn umsetzen lässt (Sichttiefe 4)
Es wird nun das gewählte Szenario erstellt (siehe *test*-Funktion in *ConnectFour*).
Folgend wird über den "CPU Zug"-Button der CPU zum Zug aufgefordert.
Die Testausführung/-wertung findet weniger in der Konsole und mehr im GUI statt.
Die Auswertung erfolgt durch Beobachtung des Spielbretts und den Move-Ratings.
Beispielsweise würde bei Sichttiefe 1 nur ein Zug ganz rechts den Test erfüllen (Gewinn mit nächstem Zug), bei Sichttiefe 2 ein Zug ganz links (Gewinn des Gegners vereiteln).
Die Testausführung protokolliert sich über die Konsole wie folgt (gelesen von unten nach oben):
Der CPU hat einen Zug in Reihe 1 gemacht. Berechnungsdauer: 367ms.
[TEST] Sichttiefe: 2, drücke auf 'CPU Zug' um das Szenario schrittweise auszuspielen.
Das Spiel ist vorbei, der Computer hat gewonnen!
[TEST] Sichttiefe: 1, drücke auf 'CPU Zug' um das Szenario schrittweise auszuspielen.
## Umsetzung der GUI
Für die Oberfläche benutze ich Bootstrap 5.
Die Website ist aufgeteilt in *index.html*, *scripts.js* und *styles.css*.
Ich wollte ein einfaches und überschaubares Design.
Im Fokus steht die Mitte mit dem Spielbrett und den Buttons zur rechten Seite.
Darunter befindet sich die Konsole welche zur Kommunikation und Einsicht dient, und die Move-Ratings des letzten CPU-Zugs um die Denkweise des Computers transparenter zu gestalten.
Um das Spielbrett umzusetzen benutze ich eine Tabelle, wobei ein Klick auf eine Reihe die *makeMove*-Funktion mit der jeweiligen Reihe ausführt.
Die Buttons lösen alle eine JavaScript-Funktion aus mit der jeweiligen Request an den Server.
Die Response vom Server ist immer ein JSON-String mit verschiedenen Daten der *ConnectFour*-Instanz (generiert von der *createJSON*-Funktion in *App.kt*).
Dazu gehören:
* Spielbrett/Bitboard als HTML-Tabelle
* Output für die Konsole
* Die Höhen der Reihen
* Ein Status des Spiels (z.B. HUMAN TURN, GAME OVER, etc.)
* Move-Ratings des letzten CPU-Zugs
Wenn das GUI/der Browser seine Response erhalten hat, wird der JSON-String geparsed und alle Elemente der Website aktualisiert.
So entsteht eine einfache und transparente UX durch das GUI.
## Hinweise
Falls der CPU zu lange braucht um einen Zug zu berechnen (was eigentlich nicht der Fall sein sollte, falls eine Datenbank und realistische Werte benutzt werden), können Alpha-Beta-Tiefe und die Anzahl der Simulationen in Monte-Carlo geändert werden.
Andersrum kann bei schnellen Zügen die Genauigkeit erhöht werden indem man die Werte erhöht.
* Tiefe in der *cpuMove*-Funktion von *ConnectFour* für jeweils beide Spieler ändern (Standard: 3)
* Anzahl der Simulationen in der *montecarlo*-Funktion von *ConnectFour* ändern (Standard: 2000)
Die mitgegebene Datenbank wurde zum Teil mit Tiefe 5 und 3000 Simulationen erstellt.
Falls der CPU aus irgendeinem Grund sich öfters zu unerwarteten Zügen entscheidet oder die Ratings fehlerhaft scheinen lohnt sich ein Wiederaufbau der Datenbank oder das Justieren der o.g. Variablen. (Natürlich sollte dies niemals passieren, falls jedoch einige Werte der Datenbank fehlerhaft sind/werden oder Sonstiges, möchte ich eine mögliche Lösung anbieten).
Die Test-Szenarien z.B. wurden alle öfters hintereinander richtig gespielt, jedoch klappt dies nicht immer da z.B. durch die Monte-Carlo-Simulation ein ungenauer Wert gespeichert wird und dieser Fehler sich dann in der Datenbank verbreitet (Eine mögliche Vermutung).
Zur Sichttiefe 5 habe ich leider keine Spielsituation gefunden.
## Quellennachweis
* https://github.com/denkspuren/BitboardC4/blob/master/BitboardDesign.md
* https://www.youtube.com/watch?v=l-hh51ncgDI
* https://en.wikipedia.org/wiki/Monte_Carlo_tree_search
* https://www.youtube.com/watch?v=Fbs4lnGLS8M
* https://v5.getbootstrap.com/
* https://www.w3schools.com/
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Kotlin application project to get you started.
*/
plugins {
// Apply the Kotlin JVM plugin to add support for Kotlin.
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
// Apply the application plugin to add support for building a CLI application.
id 'application'
}
repositories {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
jcenter()
}
dependencies {
// Align versions of all Kotlin components
implementation platform('org.jetbrains.kotlin:kotlin-bom')
// Use the Kotlin JDK 8 standard library.
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
// Use the Kotlin test library.
testImplementation 'org.jetbrains.kotlin:kotlin-test'
// Use the Kotlin JUnit integration.
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'
compile 'io.javalin:javalin:3.9.1'
compile "org.slf4j:slf4j-simple:1.8.0-beta4"
}
application {
// Define the main class for the application.
mainClassName = 'viergewinnt.AppKt'
}
This diff is collapsed.
package viergewinnt
import io.javalin.Javalin
fun main(args: Array<String>)
{
//setup javalin
val app = Javalin.create().start(7070)
app.config.addStaticFiles("/public")
//initialize a new game
var game = ConnectFour(null, Bitboard(null, longArrayOf(0L, 0L), intArrayOf(0, 7, 14, 21, 28, 35, 42), 0, ArrayList()))
//handler for a new game
app.get("/newgame") { ctx ->
game = ConnectFour(null, Bitboard(null, longArrayOf(0L, 0L), intArrayOf(0, 7, 14, 21, 28, 35, 42), 0, ArrayList()))
val jsonstring = createJSON(game.toString(), "Neues Spiel gestartet, du bist dran!", game.bitboard.height, "NEW GAME")
ctx.result(jsonstring)
}
//handler for making a move (only human)
app.get("/move") { ctx ->
val col = ctx.queryParam("column")?.toIntOrNull()
if(col != null)
{
game = game.playMove(col) as ConnectFour
if(game.isWin(0) || game.isWin(1))
ctx.result(createJSON(game.toString(), "Das Spiel ist vorbei, Du hast gewonnen!", game.bitboard.height, "GAME OVER"))
else if(game.listMoves().isEmpty())
ctx.result(createJSON(game.toString(), "Das Spiel ist vorbei, unentschieden!", game.bitboard.height, "GAME OVER"))
else
ctx.result(createJSON(game.toString(), "Du hast einen Zug in Reihe " + (col+1) + " gemacht. Der Computer wird nun seinen Zug berechnen.", game.bitboard.height, "CPU TURN"))
}
}
//handler for undoing a move
app.get("/undomove") { ctx ->
game = game.undoMove().undoMove() as ConnectFour
ctx.result(createJSON(game.toString(), "Du hast deinen letzten Zug zurückgenommen.", game.bitboard.height, "HUMAN TURN"))
}
//handler for cpu move
app.get("/cpu") { ctx ->
val cputime = kotlin.system.measureTimeMillis { game = game.cpuMove() as ConnectFour }
if(game.isWin(0) || game.isWin(1))
ctx.result(createJSON(game.toString(), "Das Spiel ist vorbei, der Computer hat gewonnen!", game.bitboard.height, "GAME OVER"))
else if(game.listMoves().isEmpty())
ctx.result(createJSON(game.toString(), "Das Spiel ist vorbei, unentschieden!", game.bitboard.height, "GAME OVER"))
else
{
if(game.bitboard.counter and 1 == 0)
{
ctx.result(createJSON(game.toString(), "Der CPU hat einen Zug in Reihe "+ (game.bitboard.moves.last()+1) +" gemacht. Berechnungsdauer: " + cputime + "ms.", game.bitboard.height, "HUMAN TURN"))
}
else
{
ctx.result(createJSON(game.toString(), "Der CPU hat einen Zug für dich in Reihe "+ (game.bitboard.moves.last()+1) +" gemacht. Berechnungsdauer: " + cputime + "ms. Er wird nun seinen Zug berechnen.", game.bitboard.height, "CPU TURN"))
}
}
}
//handler for tests
app.get("/test") { ctx ->
val depth = ctx.queryParam("tiefe")
game = game.test(depth!!)
ctx.result(createJSON(game.toString(), "[TEST] Sichttiefe: "+depth+", drücke auf 'CPU Zug' um das Szenario schrittweise auszuspielen.", game.bitboard.height, "TEST MODE"))
}
}
fun createJSON(board: String, console: String, heights: IntArray, status: String): String
{
var s = "{"
s += "\"board\": \""+board+"\","
s += "\"console\": \""+console+"\","
s += "\"heights\": ["+heights[0]+", "+(heights[1]-7)+","+(heights[2]-14)+","+(heights[3]-21)+","+(heights[4]-28)+","+(heights[5]-35)+"],"
s += "\"status\": \""+status+"\","
s += "\"ratings\": \""+ratingString()+"\""
s += "}"
return s
}
fun ratingString(): String
{
var res = ""
val lmr = java.io.File("lastmoverating")
if(!lmr.exists()) return ""
var i = 0L
lmr.forEachLine {
if(i and 1 == 0L) res += "Move: " + it.toInt()
if(i and 1 == 1L) res += " ==> " + it.toInt() + "<hr>"
i++
}
return res
}
\ No newline at end of file
package viergewinnt
/*
6 13 20 27 34 41 48 55 62 Additional row
+---------------------+
| 5 12 19 26 33 40 47 | 54 61 top row
| 4 11 18 25 32 39 46 | 53 60
| 3 10 17 24 31 38 45 | 52 59
| 2 9 16 23 30 37 44 | 51 58
| 1 8 15 22 29 36 43 | 50 57
| 0 7 14 21 28 35 42 | 49 56 63 bottom row
+---------------------+
0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 13 20 27 34 41 48
. . . . . . . 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 12 19 26 33 40 47
. . . . . . . 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 11 18 25 32 39 46
. . . . . . . 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 10 17 24 31 38 45
. . . O . . . 0 0 0 0 0 0 0 0 0 0 1 0 0 0 2 9 16 23 30 37 44
. . . X X . . 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 8 15 22 29 36 43
. . O X O . . 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 7 14 21 28 35 42
-------------
0 1 2 3 4 5 6
... 0000000 0000000 0000010 0000011 0000000 0000000 0000000 // encoding Xs
... 0000000 0000000 0000001 0000100 0000001 0000000 0000000 // encoding Os
col 6 col 5 col 4 col 3 col 2 col 1 col 0
*/
class Bitboard(private val _root: IBitboard?, private val _bitboard: LongArray, private val _height: IntArray, private val _counter: Int, private val _moves: ArrayList<Int>): IBitboard
{
override val root = _root
override val bitboard = _bitboard
override val height = _height
override val counter = _counter
override val moves = _moves
override fun makeMove(col: Int): IBitboard
{
//make variable copies that are passed to the new Bitboard
var newBitboard = bitboard.copyOf()
var newHeight = height.copyOf()
var newCounter = counter
//make move and do according changes
val move: Long = 1L shl height[col]
newHeight[col] = height[col] + 1
newBitboard[counter and 1] = bitboard[counter and 1] xor move
moves.clear()
moves.add(col) //führt zu overflow bei zu großer monte carlo simulation
newCounter = counter + 1
//return the new Bitboard
return Bitboard(this, newBitboard, newHeight, newCounter, moves)
}
override fun undoMove(): IBitboard
{
return root ?: this //if(root != null) return root else return this
}
override fun isWin(player: Int): Boolean //player: human = 0, cpu = 1 (human always starts)
{
val directions = intArrayOf(1, 7, 6, 8)
var bb: Long
for(direction in directions)
{
bb = bitboard[player] and (bitboard[player] shr direction)
if ((bb and (bb shr (2 * direction))) != 0L) return true
}
return false
}
override fun listMoves(): ArrayList<Int>
{
val moves: ArrayList<Int> = ArrayList()
val top = 0b1000000_1000000_1000000_1000000_1000000_1000000_1000000L
for(col in 0..6)
if ((top and (1L shl height[col])) == 0L) moves.add(col)
return moves
}
}
\ No newline at end of file
package viergewinnt
import IConnectFour
class ConnectFour(private val _root: IConnectFour?, private val _bitboard: Bitboard): IConnectFour
{
override val root = _root
override val bitboard = _bitboard
companion object { val map = HashMap<Int, Int>() }
override fun playMove(move: Int): IConnectFour
{
return ConnectFour(this, bitboard.makeMove(move) as Bitboard)
}
override fun cpuMove(): IConnectFour
{
loadDB()
var bestMoves = ArrayList<Int>()
val moveRating = ArrayList<Pair<Int, Int>>()
if(bitboard.counter and 1 == 1)
{
var eval = 999999999
for (move in listMoves()) {
var alphabeta = alphabeta(bitboard.makeMove(move) as Bitboard, 3, -999999999, 999999999)
moveRating.add(Pair(move, alphabeta))
if (alphabeta <= eval) {
if (alphabeta < eval) bestMoves.clear()
bestMoves.add(move)
eval = alphabeta
}
}
}
else
{
var eval = -999999999
for (move in listMoves())
{
var alphabeta = alphabeta(bitboard.makeMove(move) as Bitboard, 3, -999999999, 999999999)
moveRating.add(Pair(move, alphabeta))
if (alphabeta >= eval)
{
if (alphabeta > eval) bestMoves.clear()
bestMoves.add(move)
eval = alphabeta
}
}
}
saveDB(moveRating)
return this.playMove(bestMoves.random())
}
override fun undoMove(): IConnectFour
{
return root ?: this //if(root != null) return root else return this
}
override fun listMoves(): ArrayList<Int>
{
return bitboard.listMoves()
}
override fun isWin(player: Int): Boolean
{
return bitboard.isWin(player)
}
private fun alphabeta(bitboard: Bitboard, depth: Int, _alpha: Int, _beta: Int): Int
{
if(evaluate(bitboard) != 0) return evaluate(bitboard)
val key = Pair(bitboard.bitboard, bitboard.counter and 1).hashCode()
if(map.containsKey(key)) return map[key]!!
if(depth <= 1)
{
var rating = montecarlo(bitboard)
if(bitboard.counter-1 and 1 == 1) rating *= -1
map[key] = rating
return rating
}
var alpha = _alpha
var beta = _beta
if((bitboard.counter and 1) == 0) //human
{
var maxEval = -999999999
for(move in listMoves())
{
val eval = alphabeta(bitboard.makeMove(move) as Bitboard, depth-1, alpha, beta)
maxEval = kotlin.math.max(maxEval, eval)
alpha = kotlin.math.max(alpha, eval)
if(beta <= alpha) break
}
return maxEval
}
else //cpu
{
var minEval = 999999999
for(move in listMoves())
{
val eval = alphabeta(bitboard.makeMove(move) as Bitboard, depth-1, alpha, beta)
minEval = kotlin.math.min(minEval, eval)
beta = kotlin.math.min(beta, eval)
if(beta <= alpha) break
}
return minEval
}
}
private fun montecarlo(bitboard: Bitboard): Int
{
var rating = 0
for(i in 0..2000) //simulate games
{
var draw = false
var bb = bitboard
while(!bb.isWin(bb.counter-1 and 1))
{
if(bb.listMoves().isEmpty()) { draw = true; break }
bb = bb.makeMove(bb.listMoves().random()) as Bitboard
}
if(bb.counter-1 and 1 == bitboard.counter-1 and 1 && !draw) rating++ else rating--
}
return rating
}
private fun evaluate(bitboard: Bitboard): Int
{
if(bitboard.isWin(0)) return 999999999 - bitboard.counter
if(bitboard.isWin(1)) return -999999999 + bitboard.counter
else return 0
}
fun test(depth: String): ConnectFour
{
var testgame = ConnectFour(null, Bitboard(null, longArrayOf(0L, 0L), intArrayOf(0, 7, 14, 21, 28, 35, 42), 0, ArrayList()))
if(depth == "1")
{
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(1) as ConnectFour
}
else if(depth == "2")
{
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
}
else if(depth == "3")
{
//alternative
/*testgame = testgame.playMove(2) as ConnectFour
testgame = testgame.playMove(3) as ConnectFour
testgame = testgame.playMove(4) as ConnectFour
testgame = testgame.playMove(3) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(2) as ConnectFour
testgame = testgame.playMove(4) as ConnectFour*/
testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(2) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
testgame = testgame.playMove(4) as ConnectFour
testgame = testgame.playMove(6) as ConnectFour
}
else if(depth == "4")
{
testgame = testgame.playMove(3) as ConnectFour
testgame = testgame.playMove(3) as ConnectFour
testgame = testgame.playMove(0) as ConnectFour
}
else if(depth == "5")
{
/*testgame = testgame.playMove(0) as ConnectFour
testgame = testgame.playMove(2) as ConnectFour*/
testgame = testgame.playMove(1) as ConnectFour
testgame = testgame.playMove(2) as ConnectFour
testgame = testgame.playMove(1) as ConnectFour
testgame = testgame.playMove(1) as ConnectFour
testgame = testgame.playMove(3) as ConnectFour