Skip to content

Commit 4a730b5

Browse files
committed
Added new STTT API
1 parent 1be1781 commit 4a730b5

File tree

19 files changed

+682
-198
lines changed

19 files changed

+682
-198
lines changed

app/libs/SuperTTTApi.jar

-36.3 KB
Binary file not shown.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.flaghacker.sttt.bots
2+
3+
import com.flaghacker.sttt.common.Board
4+
import com.flaghacker.sttt.common.Bot
5+
import com.flaghacker.sttt.common.Player
6+
import com.flaghacker.sttt.common.Timer
7+
import java.lang.Double.NEGATIVE_INFINITY
8+
import java.lang.Double.POSITIVE_INFINITY
9+
import java.lang.Math.max
10+
import java.util.*
11+
12+
class MMBot(private val depth: Int) : Bot {
13+
override fun move(board: Board, timer: Timer): Byte? {
14+
if(depth==0) throw IllegalArgumentException("Minimum MMBot depth is 1")
15+
return negaMax(board, depth, NEGATIVE_INFINITY, POSITIVE_INFINITY, playerSign(board.nextPlayer())).move
16+
}
17+
18+
private class ValuedMove(val move: Byte, val value: Double)
19+
20+
private fun negaMax(board: Board, depth: Int, a: Double, b: Double, player: Int): ValuedMove {
21+
if (depth == 0 || board.isDone())
22+
return ValuedMove(board.lastMove()!!, player * value(board))
23+
24+
val children = children(board)
25+
26+
var bestValue = NEGATIVE_INFINITY
27+
var bestMove: Byte? = null
28+
29+
var newA = a
30+
for (child in children) {
31+
val value = -negaMax(child, depth - 1, -b, -newA, -player).value
32+
33+
if (value > bestValue || bestMove == null) {
34+
bestValue = value
35+
bestMove = child.lastMove()
36+
}
37+
newA = max(newA, value)
38+
if (newA >= b)
39+
break
40+
}
41+
42+
return ValuedMove(bestMove!!, bestValue)
43+
}
44+
45+
private fun children(board: Board): List<Board> {
46+
val moves = board.availableMoves()
47+
val children = ArrayList<Board>(moves.size)
48+
49+
for (move in moves) {
50+
val child = board.copy()
51+
child.play(move)
52+
children.add(child)
53+
}
54+
55+
return children
56+
}
57+
58+
private fun value(board: Board) = when (board.isDone()) {
59+
true -> Double.POSITIVE_INFINITY * playerSign(board.wonBy())
60+
false -> {
61+
(0..80).sumByDouble {
62+
TILE_VALUE * tileFactor(it % 9) * tileFactor(it / 9) * playerSign(board.tile(it.toByte())).toDouble()
63+
} + (0..8).sumByDouble {
64+
MACRO_VALUE * tileFactor(it) * playerSign(board.macro(it.toByte())).toDouble()
65+
}
66+
}
67+
}
68+
69+
private fun playerSign(player: Player) = when (player) {
70+
Player.NEUTRAL -> 0
71+
Player.PLAYER -> 1
72+
Player.ENEMY -> -1
73+
}
74+
75+
private fun tileFactor(os: Int) = when {
76+
os == 4 -> CENTER_FACTOR
77+
os % 2 == 0 -> CORNER_FACTOR
78+
else -> EDGE_FACTOR
79+
}
80+
81+
override fun toString() = "MMBotJava"
82+
83+
companion object {
84+
private const val TILE_VALUE = 1.0
85+
private const val MACRO_VALUE = 10e9
86+
87+
private const val CENTER_FACTOR = 4.0
88+
private const val CORNER_FACTOR = 3.0
89+
private const val EDGE_FACTOR = 1.0
90+
}
91+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.flaghacker.sttt.bots
2+
3+
import com.flaghacker.sttt.common.Board
4+
import com.flaghacker.sttt.common.Bot
5+
import com.flaghacker.sttt.common.Timer
6+
import java.util.*
7+
8+
class RandomBot : Bot {
9+
private val random: Random
10+
11+
constructor() {
12+
random = Random()
13+
}
14+
15+
constructor(seed: Int) {
16+
random = Random(seed.toLong())
17+
}
18+
19+
override fun move(board: Board, timer: Timer): Byte? {
20+
val moves = board.availableMoves()
21+
return moves[random.nextInt(moves.size)]
22+
}
23+
24+
override fun toString(): String {
25+
return "RandomBot"
26+
}
27+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package com.flaghacker.sttt.common
2+
3+
import java.io.Serializable
4+
import java.util.*
5+
6+
typealias Coord = Byte
7+
8+
fun toCoord(x: Int, y: Int) = (((x / 3) + (y / 3) * 3) * 9 + ((x % 3) + (y % 3) * 3)).toByte()
9+
fun Int.toPair() = toByte().toPair()
10+
fun Coord.toPair(): Pair<Int, Int> {
11+
val om = this / 9
12+
val os = this % 9
13+
return Pair((om % 3) * 3 + (os % 3), (om / 3) * 3 + (os / 3))
14+
}
15+
16+
class Board : Serializable {
17+
/*
18+
Each element represents a row of macros (3 rows of 9 tiles)
19+
The first 3 Ints hold the macros for Player
20+
The next 3 Ints hold the macros for Enemy
21+
22+
In each Int the bit representation is as follows:
23+
aaaaaaaaabbbbbbbbbcccccccccABC with:
24+
a/b/c: bit enabled if the player is the owner of the tile
25+
A/B/C: bit enabled if the player won the macro
26+
*/
27+
private var rows: Array<Int> = Array(6) { 0 }
28+
private var macroMask = 0b111111111
29+
private var nextPlayer: Player = Player.PLAYER
30+
private var lastMove: Coord? = null
31+
private var wonBy = Player.NEUTRAL
32+
33+
constructor()
34+
35+
fun copy() = Board(this)
36+
37+
constructor(board: Board) {
38+
rows = board.rows.copyOf()
39+
wonBy = board.wonBy
40+
nextPlayer = board.nextPlayer
41+
macroMask = board.macroMask
42+
lastMove = board.lastMove
43+
}
44+
45+
constructor(board: Array<Array<Player>>, macroMask: Int, lastMove: Coord?) {
46+
if (board.size != 9 && board.all { it.size != 9 })
47+
throw IllegalArgumentException("Input board is the wrong size (input: $board)")
48+
else if (macroMask < 0 || macroMask > 0b111111111)
49+
throw IllegalArgumentException("Incorrect input macro mask (input: $macroMask)")
50+
51+
val xCount1 = board.sumBy { it.filterNot { it == Player.NEUTRAL }.sumBy { if (it == Player.PLAYER) 1 else -1 } }
52+
var xCount = 0
53+
for (i in 0 until 81) {
54+
val macroShift = (i / 9) % 3 * 9
55+
val coords = i.toPair()
56+
val owner = board[coords.first][coords.second]
57+
58+
if (owner != Player.NEUTRAL) {
59+
xCount += if (owner == Player.PLAYER) 1 else -1
60+
rows[i / 27 + owner.ordinal * 3] += 1 shl i % 27
61+
if (wonGrid((rows[i / 27 + owner.ordinal * 3] shr macroShift) and 0b111111111, i % 9)) {
62+
rows[i / 27 + owner.ordinal * 3] += (1 shl (27 + macroShift / 9)) //27 + macro number
63+
if (wonGrid(winGrid(owner), i / 9)) wonBy = nextPlayer
64+
}
65+
}
66+
}
67+
68+
println(xCount1 == xCount)
69+
println("$xCount $xCount1")
70+
71+
this.lastMove = lastMove
72+
this.macroMask = macroMask
73+
nextPlayer = when (xCount) {
74+
-1, 0 -> Player.PLAYER
75+
1 -> Player.ENEMY
76+
else -> throw IllegalArgumentException("Input board is invalid (input: $board)")
77+
}
78+
}
79+
80+
fun macroMask() = macroMask
81+
fun isDone() = wonBy != Player.NEUTRAL || availableMoves().isEmpty()
82+
fun nextPlayer() = nextPlayer
83+
fun lastMove() = lastMove
84+
fun wonBy() = wonBy
85+
86+
fun flip(): Board {
87+
val board = copy()
88+
89+
val newRows = Array(6) { 0 }
90+
for (i in 0..2) newRows[i] = board.rows[i + 3]
91+
for (i in 3..5) newRows[i] = board.rows[i - 3]
92+
board.rows = newRows
93+
board.wonBy = board.wonBy.otherWithNeutral()
94+
board.nextPlayer = board.nextPlayer.otherWithNeutral()
95+
96+
return board
97+
}
98+
99+
fun availableMoves(): List<Coord> {
100+
val output = ArrayList<Coord>()
101+
102+
for (macro in 0 until 9) {
103+
if (macroMask.getBit(macro)) {
104+
val row = rows[macro / 3] or rows[macro / 3 + 3]
105+
(0 until 9).map { it + macro * 9 }.filter { !row.getBit(it % 27) }.mapTo(output) { it.toByte() }
106+
}
107+
}
108+
109+
return output
110+
}
111+
112+
fun macro(index: Byte): Player = when {
113+
rows[index / 3].getBit(27 + index % 3) -> Player.PLAYER
114+
rows[3 + index / 3].getBit(27 + index % 3) -> Player.ENEMY
115+
else -> Player.NEUTRAL
116+
}
117+
118+
fun tile(index: Coord): Player = when {
119+
rows[index / 27].getBit(index % 27) -> Player.PLAYER
120+
rows[3 + index / 27].getBit(index % 27) -> Player.ENEMY
121+
else -> Player.NEUTRAL
122+
}
123+
124+
fun play(index: Coord): Boolean {
125+
val row = index / 27 //Row (0,1,2)
126+
val macroShift = (index / 9) % 3 * 9 //Shift to go to the right micro (9om)
127+
val moveShift = index % 9 //Shift required for index within matrix (os)
128+
val shift = moveShift + macroShift //Total move offset in the row entry
129+
val pRow = nextPlayer.ordinal * 3 + row //Index of the row entry in the rows array
130+
131+
//If the move is not available throw exception
132+
if ((rows[row] or rows[row + 3]).getBit(shift) || !macroMask.getBit((index / 27) * 3 + (macroShift / 9)))
133+
throw RuntimeException("Position $index not available")
134+
else if (wonBy != Player.NEUTRAL)
135+
throw RuntimeException("Can't play; game already over")
136+
137+
//Write move to board & check for macro win
138+
rows[pRow] += (1 shl shift)
139+
val macroWin = wonGrid((rows[pRow] shr macroShift) and 0b111111111, moveShift)
140+
141+
//Check if the current player won
142+
if (macroWin) {
143+
rows[pRow] += (1 shl (27 + macroShift / 9))
144+
if (wonGrid(winGrid(nextPlayer), index / 9)) wonBy = nextPlayer
145+
}
146+
147+
//Prepare the board for the next player
148+
val winGrid = winGrid(Player.PLAYER) or winGrid(Player.ENEMY)
149+
val freeMove = winGrid.getBit(moveShift) || macroFull(moveShift)
150+
macroMask = if (freeMove) (0b111111111 and winGrid.inv()) else (1 shl moveShift)
151+
lastMove = index
152+
nextPlayer = nextPlayer.other()
153+
154+
return macroWin
155+
}
156+
157+
private fun Int.getBit(index: Int) = ((this shr index) and 1) == 1
158+
private fun Int.isMaskSet(mask: Int) = this and mask == mask
159+
private fun macroFull(om: Int) = (rows[om / 3] or rows[3 + om / 3]).shr((om % 3) * 9).isMaskSet(0b111111111)
160+
private fun winGrid(player: Player) = (rows[0 + 3 * player.ordinal] shr 27)
161+
.or((rows[1 + 3 * player.ordinal] shr 27) shl 3)
162+
.or((rows[2 + 3 * player.ordinal] shr 27) shl 6)
163+
164+
private fun wonGrid(grid: Int, index: Int) = when (index) {
165+
4 -> grid.getBit(1) && grid.getBit(7) //Center: line |
166+
|| grid.getBit(3) && grid.getBit(5) //Center: line -
167+
|| grid.getBit(0) && grid.getBit(8) //Center: line \
168+
|| grid.getBit(6) && grid.getBit(2) //Center: line /
169+
3, 5 -> grid.getBit(index - 3) && grid.getBit(index + 3) //Horizontal side: line |
170+
|| grid.getBit(4) && grid.getBit(8 - index) //Horizontal side: line -
171+
1, 7 -> grid.getBit(index - 1) && grid.getBit(index + 1) //Vertical side: line |
172+
|| grid.getBit(4) && grid.getBit(8 - index) //Vertical side: line -
173+
else -> { //Corners
174+
val x = index % 3
175+
val y = index / 3
176+
grid.getBit(4) && grid.getBit(8 - index) //line \ or /
177+
|| grid.getBit(3 * y + (x + 1) % 2) && grid.getBit(3 * y + (x + 2) % 4) //line -
178+
|| grid.getBit(x + ((y + 1) % 2) * 3) && grid.getBit(x + ((y + 2) % 4) * 3) //line |
179+
}
180+
}
181+
182+
override fun toString() = (0 until 81).map { it to toCoord(it % 9, it / 9) }.joinToString("") {
183+
when {
184+
(it.first == 0 || it.first == 80) -> ""
185+
(it.first % 27 == 0) -> "\n---+---+---\n"
186+
(it.first % 9 == 0) -> "\n"
187+
(it.first % 3 == 0 || it.first % 6 == 0) -> "|"
188+
else -> ""
189+
} + when {
190+
rows[it.second / 27].getBit(it.second % 27) -> "X"
191+
rows[(it.second / 27) + 3].getBit(it.second % 27) -> "O"
192+
else -> " "
193+
}
194+
}
195+
196+
override fun hashCode() = 31 * Arrays.hashCode(rows) + macroMask
197+
override fun equals(other: Any?): Boolean {
198+
if (this === other) return true
199+
if (javaClass != other?.javaClass) return false
200+
201+
other as Board
202+
203+
if (!Arrays.equals(rows, other.rows)) return false
204+
if (macroMask != other.macroMask) return false
205+
206+
return true
207+
}
208+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.flaghacker.sttt.common
2+
3+
import java.io.Serializable
4+
5+
interface Bot : Serializable {
6+
fun move(board: Board, timer: Timer): Byte?
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.flaghacker.sttt.common
2+
3+
import org.json.JSONArray
4+
import org.json.JSONObject
5+
6+
fun Board.toJSON(): JSONObject {
7+
val json = JSONObject()
8+
val jsonBoard = JSONArray()
9+
for (i in 0 until 81) jsonBoard.put(tile(i.toByte()).toNiceString())
10+
11+
json.put("board", jsonBoard)
12+
json.put("macroMask", macroMask())
13+
json.put("lastMove",lastMove()?.toInt())
14+
15+
return json
16+
}
17+
18+
class JSONBoard {
19+
companion object {
20+
fun fromJSON(json: JSONObject): com.flaghacker.sttt.common.Board {
21+
val board = Array(9, { Array(9, { Player.NEUTRAL }) })
22+
for (i in 0 until 81)
23+
board[i.toPair().first][i.toPair().second] = fromNiceString(json.getJSONArray("board").getString(i))
24+
val macroMask = json.getInt("macroMask")
25+
val lastMove = json.getInt("lastMove")
26+
27+
return Board(board, macroMask, lastMove.toByte())
28+
}
29+
}
30+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.flaghacker.sttt.common
2+
3+
fun fromNiceString(string: String): Player {
4+
Player.values().filter { it.toNiceString() == string }.forEach { return it }
5+
throw IllegalArgumentException(string + " is not a valid com.flaghacker.sttt.common.KotlinPlayer")
6+
}
7+
8+
enum class Player(val niceString: String) {
9+
PLAYER("X"),
10+
ENEMY("O"),
11+
NEUTRAL(" ");
12+
13+
fun other(): Player = when {
14+
this == PLAYER -> ENEMY
15+
this == ENEMY -> PLAYER
16+
else -> throw IllegalArgumentException("player should be one of [PLAYER, ENEMY]; was " + this)
17+
}
18+
19+
fun otherWithNeutral(): Player = if (this == NEUTRAL) NEUTRAL else this.other()
20+
fun toNiceString(): String = niceString
21+
}

0 commit comments

Comments
 (0)