Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private const val TURN_LIST_PATTERN = /*language=regexp*/ """$TURN_PATTERN(?:\s+

private const val ORIGIN_PATTERN = /*language=regexp*/ """@\s*\((-?\d+),\s*(-?\d+)\)"""

private val BKE_FORMAT = """^([bdpq<>])?\s*(CW|CCW)?\s*(?:$ORIGIN_PATTERN\s*:?)?\s*($TURN_LIST_PATTERN)$""".toRegex()
private val BKE_FORMAT = """^\s*([bdpq<>])?\s*(CW|CCW)?\s*(?:$ORIGIN_PATTERN\s*:?)?\s*($TURN_LIST_PATTERN)\s*$""".toRegex()

enum class Chirality(val symbol: String) {
Clockwise("CW"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ fun String.parseRectilinearNotation(): Board {
state = state.handleChar(ch, offset, cursor, buffer)
}

requireHexo(buffer.isEmpty()) { "Unterminated symbol at end of input" }
state.handleEOF(cursor, buffer)

requireHexo(buffer.isEmpty()) { "Unterminated symbol at end of input: `$buffer`" }
return board
}

Expand Down Expand Up @@ -75,12 +77,15 @@ private enum class ParserState {
buffer.append(ch)
return this
} else {
cursor.step(buffer.toString().toInt())
buffer.clear()

handleEOF(cursor, buffer)
return Normal.handleChar(ch, offset, cursor, buffer)
}
}

override fun handleEOF(cursor: Cursor, buffer: StringBuilder) {
cursor.step(buffer.toString().toInt())
buffer.clear()
}
},
Label {
override fun handleChar(ch: Char, offset: Int, cursor: Cursor, buffer: StringBuilder): ParserState {
Expand Down Expand Up @@ -135,6 +140,7 @@ private enum class ParserState {
;

abstract fun handleChar(ch: Char, offset: Int, cursor: Cursor, buffer: StringBuilder): ParserState
open fun handleEOF(cursor: Cursor, buffer: StringBuilder) {}
}

private class Cursor(private val board: Board) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,6 @@ class RectilinearNotationParserTest {
val e = assertFailsWith<HexoNotationException> {
val _ = "x.[ab".parseRectilinearNotation()
}
assertEquals("Unterminated symbol at end of input", e.message)
assertEquals("Unterminated symbol at end of input: `ab`", e.message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ private fun List<Pair<CellOwner, List<CellCoordinate>>>.chooseBKELayout(origin:
val forbiddenOrigins = cells.toSet()

if (origin != null && origin !in forbiddenOrigins) {
return BKELayout(origin, Direction.Right)
return Direction.entries
.map { direction -> BKELayout(origin, direction) }
.chooseBest(cells)
}

val first = cells.first()
Expand All @@ -96,21 +98,23 @@ private fun List<Pair<CellOwner, List<CellCoordinate>>>.chooseBKELayout(origin:
Direction.entries.map { direction -> BKELayout(first - direction.direction * ring, direction) }
}
.filter { it.origin !in forbiddenOrigins }
.minWith { a, b ->
val scoreCompare = compareScores(a.score(cells), b.score(cells))
if (scoreCompare != 0) {
scoreCompare
} else {
compareValuesBy(a, b, { it.baseline.ordinal }, { it.origin.q }, { it.origin.r })
}
}
.chooseBest(cells)
}

private fun BKELayout.score(cells: List<CellCoordinate>) = cells.flatMap {
val (ring, sector, sectorOffset) = it.ringOffset(origin, baseline)
listOf(ring, sector * ring + sectorOffset)
}

private fun List<BKELayout>.chooseBest(cells: List<CellCoordinate>) = minWith { a, b ->
val scoreCompare = compareScores(a.score(cells), b.score(cells))
if (scoreCompare != 0) {
scoreCompare
} else {
compareValuesBy(a, b, { it.baseline.ordinal }, { it.origin.q }, { it.origin.r })
}
}

private fun compareScores(a: List<Int>, b: List<Int>): Int {
a.zip(b).forEach { (left, right) ->
left.compareTo(right).takeIf { it != 0 }?.let { return it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package de.mineking.hexo.render.test

import de.mineking.hexo.board.Board
import de.mineking.hexo.board.CellHighlight
import de.mineking.hexo.board.Direction
import de.mineking.hexo.core.CellOwner
import de.mineking.hexo.parse.parseBKENotation
import de.mineking.hexo.parse.parseRectilinearNotation
import de.mineking.hexo.parse.parseRectilinearStateBKETurnNotation
import de.mineking.hexo.render.RectilinearNotationType
Expand Down Expand Up @@ -73,9 +71,18 @@ class IntegrationTest {
}

@Test
fun `bke test`() {
fun `bke test 1`() {
val input = "o A0"
val parsed = input.parseRectilinearStateBKETurnNotation()
val rendered = parsed.renderRectilinearStateBKETurnNotation(RectilinearNotationType.Compact)

assertEquals(input, rendered)
}

@Test
fun `bke test 2`() {
val input = "o A0 A2 x A1 A4 o B1.0 B4.0"
val parsed = input.parseBKENotation(null, Direction.Right)
val parsed = input.parseRectilinearStateBKETurnNotation()
val rendered = parsed.renderRectilinearStateBKETurnNotation(RectilinearNotationType.Compact)

assertEquals(input, rendered)
Expand Down Expand Up @@ -105,6 +112,24 @@ class IntegrationTest {
assertEquals("x, > @(-5, 0) o A0", rendered)
}

@Test
fun `rectilinear state bke turn optimizes fixed origin direction`() {
val board = Board()
board[0, 0].owner = CellOwner.X
board[1, 0].apply {
owner = CellOwner.O
turn = 0
}
board[1, 1].apply {
owner = CellOwner.O
turn = 1
}

val rendered = board.renderRectilinearStateBKETurnNotation(RectilinearNotationType.Compact)

assertEquals("x, q @(1, 0) o A0", rendered)
}

@Test
fun `bke origin is not placed on a bke move`() {
val board = Board()
Expand Down
52 changes: 45 additions & 7 deletions web/src/jsMain/kotlin/de/mineking/hexo/web/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import de.mineking.hexo.api.createRepositories
import de.mineking.hexo.board.CellCoordinate
import de.mineking.hexo.board.CellHighlight
import de.mineking.hexo.board.Direction
import de.mineking.hexo.board.HexoNotationException
import de.mineking.hexo.board.HighlightLine
import de.mineking.hexo.board.clone
import de.mineking.hexo.board.contains
Expand All @@ -20,11 +21,14 @@ import de.mineking.hexo.parse.parseRectilinearStateBKETurnNotation
import de.mineking.hexo.render.compose.Board
import de.mineking.hexo.render.compose.BoardRightClickEvent
import de.mineking.hexo.render.compose.BoardViewport
import de.mineking.hexo.web.components.Dialog
import kotlinx.browser.window
import org.jetbrains.compose.web.attributes.readOnly
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.dom.TextArea
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.events.MouseEvent
Expand All @@ -36,13 +40,40 @@ private const val PROXY = "https://hexo.mineking.dev/proxy"

fun main() {
js("require('./style.css')")

val params = URLSearchParams(window.location.search)
val initial = params.get("position")?.replace("_", "/") ?: ""
window.history.pushState(null, "", window.location.pathname)

val (initialBoard, initialError) = try {
val board = when {
initial.isBlank() -> HexoBoard()
else -> initial.parseRectilinearStateBKETurnNotation()
}

board to null
} catch (e: HexoNotationException) {
HexoBoard() to e.message
}

renderComposable(rootElementId = "root") {
val params = URLSearchParams(window.location.search)
val initial = params.get("position")?.replace("_", "/") ?: "0"
window.history.pushState(null, "", window.location.pathname)
var error by remember { mutableStateOf(initialError) }

val client = remember { HexoApiClient(host = PROXY, socketIOOptions = null) }
MainLayout(client, initial)
MainLayout(client, initialBoard)

if (error != null) {
Dialog(title = "Invalid Position", onClose = { error = null }) {
TextArea {
value(error ?: "")
classes(
"w-full", "resize-y", "rounded-lg", "border-3", "p-3", "text-sm", "text-rose-100",
"outline-none", "transition", "font-mono", "bg-slate-950", "border-rose-400",
)
readOnly()
}
}
}
}
}

Expand All @@ -52,13 +83,20 @@ enum class CellPlacementMode {
}

@Composable
private fun MainLayout(client: HexoApiClient, initialPosition: String) {
private fun MainLayout(client: HexoApiClient, initialBoard: HexoBoard) {
val repositories = remember(client) { client.createRepositories() }

val viewport = remember { mutableStateOf<BoardViewport?>(null) }
val placementMode = remember { mutableStateOf(CellPlacementMode.Turn) }
val placementMode = remember {
mutableStateOf(
when {
initialBoard.cells.values.any { it.owner != null } -> CellPlacementMode.Turn
else -> CellPlacementMode.State
},
)
}

val board = remember { mutableStateOf(initialPosition.parseRectilinearStateBKETurnNotation()) }
val board = remember { mutableStateOf(initialBoard) }
var temporaryLine by remember { mutableStateOf<HighlightLine?>(null) }

val transformedBoard = remember(board.value, temporaryLine) {
Expand Down
35 changes: 22 additions & 13 deletions web/src/jsMain/kotlin/de/mineking/hexo/web/Sidebar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.browser.window
import kotlinx.dom.addClass
import kotlinx.dom.removeClass
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.placeholder
import org.jetbrains.compose.web.attributes.readOnly
import org.jetbrains.compose.web.dom.A
Expand All @@ -36,8 +37,8 @@ import org.w3c.dom.events.MouseEvent
import org.w3c.dom.url.URL
import de.mineking.hexo.board.Board as HexoBoard

private const val DEFAULT_SIDEBAR_WIDTH = 384
private const val MIN_SIDEBAR_WIDTH = 320
private const val DEFAULT_SIDEBAR_WIDTH = 380
private const val MIN_SIDEBAR_WIDTH = 330
private const val MAX_SIDEBAR_WIDTH = 560
private const val GITHUB_URL = "https://github.com/MineKing9534/HeXO-Renderer"

Expand Down Expand Up @@ -95,7 +96,6 @@ fun Sidebar(
NotationField(
formationRepository = formationRepository,
finishedGameRepository = finishedGameRepository,
board = board,
notation = notation,
parseError = parseError,
onChange = { cause, value ->
Expand Down Expand Up @@ -239,19 +239,26 @@ private fun ParseStatus(valid: Boolean) {
private fun NotationField(
formationRepository: FormationRepository,
finishedGameRepository: FinishedGameRepository,
board: HexoBoard,
notation: String,
parseError: String?,
onChange: (BoardUpdateCause, String) -> Unit,
) {
var focused by remember { mutableStateOf(false) }

Div({ classes("space-y-2") }) {
Div({ classes("flex", "items-center", "justify-between", "gap-3") }) {
Div({ classes("text-sm", "font-semibold", "uppercase", "text-slate-400") }) {
Div({ classes("relative", "min-h-8", "overflow-hidden") }) {
Div({ classes("pt-1.5", "text-sm", "font-semibold", "uppercase", "text-slate-400") }) {
Text("Notation")
}
NotationActions(formationRepository, finishedGameRepository, board, onChange)
Div({
classes(
"absolute", "right-0", "top-0", "z-10", "max-w-full",
"before:pointer-events-none", "before:absolute", "before:-left-5", "before:top-0", "before:h-full", "before:w-5",
"before:bg-linear-to-r", "before:from-transparent", "before:to-slate-900/95", "before:content-['']",
)
}) {
NotationActions(formationRepository, finishedGameRepository, notation, onChange)
}
}

TextArea {
Expand All @@ -277,15 +284,17 @@ private fun NotationField(
private fun NotationActions(
formationRepository: FormationRepository,
finishedGameRepository: FinishedGameRepository,
board: HexoBoard,
notation: String,
onChange: (BoardUpdateCause, String) -> Unit,
) {
@Composable
fun Button(label: String, onClick: () -> Unit) {
fun Button(label: String, enabled: Boolean = true, onClick: () -> Unit) {
Button({
if (!enabled) disabled()
classes(
"rounded-md", "border", "border-slate-700", "bg-slate-950", "px-2.5", "py-1", "text-nowrap",
"text-xs", "font-medium", "text-slate-300", "transition", "hover:bg-slate-800", "hover:text-slate-100",
"text-xs", "font-medium", "text-slate-300", "disabled:text-slate-400", "transition",
"not-disabled:hover:bg-slate-800", "not-disabled:hover:text-slate-100",
)
onClick { onClick() }
}) {
Expand All @@ -294,11 +303,11 @@ private fun NotationActions(
}

var importDialogOpen by remember { mutableStateOf(false) }
Div({ classes("flex", "gap-2") }) {
Div({ classes("flex", "max-w-full", "justify-end", "gap-2") }) {
var link by remember { mutableStateOf<String?>(null) }
Button("Copy Link") {
Button("Copy Link", enabled = notation.isNotBlank()) {
val url = URL(window.location.href)
url.searchParams.set("position", board.renderRectilinearStateBKETurnNotation(RectilinearNotationType.Compact).replace("/", "_"))
url.searchParams.set("position", notation.replace("/", "_"))
link = url.toString()
}

Expand Down