diff --git a/src/main/kotlin/sc/gui/controller/BlokusController.kt b/src/main/kotlin/sc/gui/controller/BlokusController.kt index 5a10cb0..5c0df8d 100644 --- a/src/main/kotlin/sc/gui/controller/BlokusController.kt +++ b/src/main/kotlin/sc/gui/controller/BlokusController.kt @@ -7,6 +7,7 @@ import javafx.beans.property.Property import javafx.beans.value.ObservableValue import org.slf4j.LoggerFactory import sc.api.plugins.Coordinates +import sc.api.plugins.IMove import sc.api.plugins.Team import sc.gui.GameOverEvent import sc.gui.GameReadyEvent @@ -254,6 +255,12 @@ class BlokusController : Controller() { fun scroll(deltaY: Double) { currentPiece.get().scroll(deltaY) } + + fun sendHumanMove(move: IMove) { + fire(HumanMoveAction(move.also { + logger.debug("Human Move: {}", it) + })) + } companion object { private val logger = LoggerFactory.getLogger(BlokusController::class.java) diff --git a/src/main/kotlin/sc/gui/view/PlayerOneView.kt b/src/main/kotlin/sc/gui/view/PlayerOneView.kt new file mode 100644 index 0000000..ac8348d --- /dev/null +++ b/src/main/kotlin/sc/gui/view/PlayerOneView.kt @@ -0,0 +1,39 @@ +package sc.gui.view + +import javafx.beans.binding.DoubleBinding +import javafx.geometry.Insets +import javafx.geometry.Pos +import sc.api.plugins.Team +import sc.gui.controller.BlokusController +import sc.gui.model.GameModel +import sc.gui.view.game.SkipMoveButton +import sc.plugin2027.Color +import tornadofx.* + +class PlayerOneView(gameController: BlokusController, gridSize: DoubleBinding): View() { + private val game: GameModel by inject() + + // This is a vbox since all elements are below each other. + override val root = vbox { + useMaxWidth = true + alignment = Pos.TOP_CENTER + add(playerLabel(game, Team.ONE)) + add(UndeployedPiecesFragment( + Color.BLUE, + gameController.undeployedPieces.getValue(Color.BLUE), + gameController.validPieces.getValue(Color.BLUE), + gridSize + )) + add(UndeployedPiecesFragment( + Color.RED, + gameController.undeployedPieces.getValue(Color.RED), + gameController.validPieces.getValue(Color.RED), + gridSize + )) + add(SkipMoveButton(gameController, gridSize, Team.ONE)) + // Run this later since scene is not ready yet + runLater { + this.padding = Insets(0.0, 0.0, 0.0, 0.0) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/sc/gui/view/PlayerTwoView.kt b/src/main/kotlin/sc/gui/view/PlayerTwoView.kt new file mode 100644 index 0000000..e7f6f67 --- /dev/null +++ b/src/main/kotlin/sc/gui/view/PlayerTwoView.kt @@ -0,0 +1,39 @@ +package sc.gui.view + +import javafx.beans.binding.DoubleBinding +import javafx.geometry.Insets +import javafx.geometry.Pos +import sc.api.plugins.Team +import sc.gui.controller.BlokusController +import sc.gui.model.GameModel +import sc.gui.view.game.SkipMoveButton +import sc.plugin2027.Color +import tornadofx.* + +class PlayerTwoView(gameController: BlokusController, gridSize: DoubleBinding): View() { + private val game: GameModel by inject() + + // This is a vbox since all elements are below each other. + override val root = vbox { + useMaxWidth = true + alignment = Pos.TOP_CENTER + add(playerLabel(game, Team.TWO)) + add(UndeployedPiecesFragment( + Color.YELLOW, + gameController.undeployedPieces.getValue(Color.YELLOW), + gameController.validPieces.getValue(Color.YELLOW), + gridSize + )) + add(UndeployedPiecesFragment( + Color.GREEN, + gameController.undeployedPieces.getValue(Color.GREEN), + gameController.validPieces.getValue(Color.GREEN), + gridSize + )) + add(SkipMoveButton(gameController, gridSize, Team.TWO)) + // Run this later since scene is not ready yet + runLater { + this.padding = Insets(0.0, 0.0, 0.0, 0.0) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/sc/gui/view/StatusView.kt b/src/main/kotlin/sc/gui/view/StatusView.kt index a6c4d15..2e27739 100644 --- a/src/main/kotlin/sc/gui/view/StatusView.kt +++ b/src/main/kotlin/sc/gui/view/StatusView.kt @@ -5,6 +5,7 @@ import javafx.geometry.Pos import javafx.scene.control.Label import javafx.scene.layout.Priority import javafx.scene.paint.Color +import javafx.scene.text.Font import javafx.scene.text.TextAlignment import sc.api.plugins.ITeam import sc.api.plugins.Team @@ -14,6 +15,41 @@ import sc.gui.model.GameModel import sc.gui.strings import tornadofx.* +fun decodeXmlEntities(toDecode: String): String { + // if ('&' !in toDecode) return toDecode + // Replace potentially bad characters + return toDecode.replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace("&", "&") + .replace(""", "\"") +} + +fun playerLabel(game: GameModel, team: Team) = + Label().apply { + // Add string to display for team including teamStats. + textProperty().bind( + game.gameState.stringBinding { state -> + state?.teamStats(team)?.takeUnless { it.isEmpty() }?.let { stats -> + stats.joinToString( + "\n", + "${decodeXmlEntities(game.playerNames[team.index])} (${strings["color.${team.color}"]})\n" + ) { stat -> + "${stat.label} ${stat.icon?.let { if(stat.value > 0) it.repeat(stat.value) else "-" } ?: stat.value}" + } + } + }) + // Handle theming + textFillProperty().bind(AppModel.darkMode.objectBinding { + if(it == true) + Color.hsb(Color.valueOf(team.color).hue, .4, 1.0) + else + Color.hsb(Color.valueOf(team.color).hue, .8, .6) + }) + font = Font(AppStyle.fontSizeBig.value) + + } + class StatusBinding(private val game: GameModel): StringBinding() { init { bind(game.gameStarted, game.currentTeam, game.gameResult, game.playerNames, game.atLatestTurn) @@ -23,11 +59,11 @@ class StatusBinding(private val game: GameModel): StringBinding() { if(game.gameStarted.value && game.atLatestTurn.value || game.gameResult.value != null) game.gameResult.takeIf { game.atLatestTurn.value }?.get()?.let { gameResult -> """ - ${gameResult.win?.winner?.let { "${it.displayName} hat gewonnen!" } ?: "Unentschieden"} - ${gameResult.win?.reason?.message?.replace(" brig", " übrig").orEmpty()} + ${gameResult.win?.winner?.let { "${decodeXmlEntities(it.displayName)} hat gewonnen!" } ?: "Unentschieden"} + ${decodeXmlEntities(gameResult.win?.reason?.message?.replace(" brig", " übrig").orEmpty())} """.trimIndent().trim('\n') - } ?: "${game.currentTeam.value.displayName} am Zug" - else game.playerNames.joinToString(" vs ") + } ?: "${decodeXmlEntities(game.currentTeam.value.displayName)} am Zug" + else game.playerNames.map { decodeXmlEntities(it) }.joinToString(" vs ") val ITeam.displayName get() = index.let { game.playerNames.getOrNull(it) ?: "Spieler ${it + 1}" } @@ -38,15 +74,22 @@ class ScoreBinding(private val game: GameModel): StringBinding() { bind(game.gameStarted, game.gameState) } - override fun computeValue(): String = - if(game.gameStarted.value) - "Runde ${game.getCurrentRound()} - " + - game.gameState.value?.run { - Team.values().sortedBy { it != startTeam }.joinToString(" : ") { - getPointsForTeam(it).first().toString() - } - } - else "Drücke auf Start".takeUnless { game.gameOver.value && game.atLatestTurn.value }.orEmpty() + /** + * A ScoreBinding should have the following computed String value: + * Runde X - Punkte Team 1 : Punkte Team 2 + * This point order is inverted if the startTeam is Team.TWO. + * This is only used for the finals. + */ + override fun computeValue(): String { + if(game.gameStarted.value) { + return "Runde ${(game.currentTurn.get() + 1) / 2} - " + + game.gameState.value?.getPointsForTeam(Team.ONE)?.first().toString() + + " : " + + game.gameState.value?.getPointsForTeam(Team.TWO)?.first().toString() + } else { + return "Drücke auf Start".takeUnless { game.gameOver.value && game.atLatestTurn.value }.orEmpty() + } + } } class StatusView: View() { @@ -55,13 +98,15 @@ class StatusView: View() { override val root = hbox { useMaxWidth = true alignment = Pos.CENTER - add(playerLabel(Team.ONE)) + // Moved to PlayerOne/TwoView +// add(playerLabel(Team.ONE)) vbox(alignment = Pos.CENTER) { this.spacing = AppStyle.fontSizeUnscaled.value runLater { prefWidthProperty().bind(scene.widthProperty().divide(2)) hgrow = Priority.ALWAYS - maxWidth = AppStyle.fontSizeRegular.value * 60 + maxWidth = AppStyle.fontSizeRegular.value * 90 + prefHeightProperty().bind(scene.heightProperty().divide(6)) } addClass(AppStyle.statusLabel) label(StatusBinding(game)) { @@ -70,7 +115,8 @@ class StatusView: View() { } label(ScoreBinding(game)) } - add(playerLabel(Team.TWO)) + // Moved to PlayerOne/TwoView +// add(playerLabel(Team.TWO)) //runLater { // scene.root.apply { diff --git a/src/main/kotlin/sc/gui/view/game/BlokusBoard.kt b/src/main/kotlin/sc/gui/view/game/BlokusBoard.kt index b4e1730..f2b4192 100644 --- a/src/main/kotlin/sc/gui/view/game/BlokusBoard.kt +++ b/src/main/kotlin/sc/gui/view/game/BlokusBoard.kt @@ -41,7 +41,7 @@ val Color.borderStyle } class BlokusBoard: GameBoard() { - private val gameController: BlokusController by inject() + val gameController: BlokusController by inject() val controller: BlokusBoardController by inject() private val undeployedPieces = EnumMap( @@ -56,11 +56,11 @@ class BlokusBoard: GameBoard() { private val leftPane = vbox { alignment = Pos.TOP_LEFT - replaceChildren(*undeployedPieces.filterKeys { it.team == Team.ONE }.values.toTypedArray()) + add(PlayerOneView(gameController, gridSize)) } private val rightPane = vbox { alignment = Pos.TOP_RIGHT - replaceChildren(*undeployedPieces.filterKeys { it.team == Team.TWO }.values.toTypedArray()) + add(PlayerTwoView(gameController, gridSize)) } /** @@ -78,7 +78,8 @@ class BlokusBoard: GameBoard() { */ val grid: GridPane = GridPane().addClass("grid").apply { squareSize.listenImmediately { size -> - val cellSize = (size.toDouble() / Constants.BOARD_LENGTH).toInt().toDouble() + // +2 is the magic number that makes everything fit. + val cellSize = (size.toDouble() / (Constants.BOARD_LENGTH + 2)).toInt().toDouble() columnConstraints.clear() rowConstraints.clear() repeat(Constants.BOARD_LENGTH) { diff --git a/src/main/kotlin/sc/gui/view/game/SkipMoveButton.kt b/src/main/kotlin/sc/gui/view/game/SkipMoveButton.kt new file mode 100644 index 0000000..f6178fb --- /dev/null +++ b/src/main/kotlin/sc/gui/view/game/SkipMoveButton.kt @@ -0,0 +1,32 @@ +package sc.gui.view.game + +import javafx.beans.binding.Bindings +import javafx.beans.binding.DoubleBinding +import javafx.scene.control.Button +import sc.api.plugins.Team +import sc.gui.controller.BlokusController +import sc.plugin2027.SkipMove +import tornadofx.onLeftClick + +class SkipMoveButton( + private val gameController: BlokusController, + gridSize: DoubleBinding, + team: Team +) : Button("Aussetzen") { + + init { + prefWidthProperty().bind(gridSize.multiply(5)) + // Disable button if the current turn is not from a human player, + // we cannot skip (i.e. the first round), + // or if it is not our turn. + disableProperty().bind(Bindings.createBooleanBinding( + { !(gameController.isHumanTurn.value && gameController.canSkip.value && gameController.currentColor.value.team == team) }, + gameController.isHumanTurn, + gameController.canSkip, + gameController.currentColor + )) + onLeftClick { + gameController.sendHumanMove(SkipMove(gameController.currentColor.value)) + } + } +} \ No newline at end of file