|
| 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 | +} |
0 commit comments