Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bbae970
First draft of automatic game end
tommy33790 Sep 10, 2026
508839e
Copied in-game-time (`LevelMenuPanel`) to automatic end timer (`GameT…
tommy33790 Sep 20, 2026
20f371b
Reduced desync between in-game-clock and automatic game end.
tommy33790 Sep 20, 2026
c7fdbab
Slight refactor after fixing delay timers.
tommy33790 Sep 20, 2026
5a888e2
Reset game end timers before reinstating them.
tommy33790 Sep 20, 2026
8fab4ec
Min and max for game end date-spinners.
tommy33790 Sep 22, 2026
84b8920
Refactoring for descriptive names.
tommy33790 Sep 22, 2026
c31c2a9
Removed duplicated line in `LevelMenuPanel`
tommy33790 Sep 22, 2026
c4f94f4
Fix formatting
tommy33790 Sep 27, 2026
f4ae70e
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Sep 28, 2026
d496e6c
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Sep 28, 2026
f840516
Merge branch 'dev' into 62-initiate-automatic-game-end-after-a-certai…
DivineThreepwood Oct 1, 2026
d147c1a
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 1, 2026
2ec566e
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 2, 2026
157ffda
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 2, 2026
b1a3886
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 2, 2026
bf248fd
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 13, 2026
7477972
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 14, 2026
a75ba69
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 15, 2026
b13811e
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
96ec235
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
1d53ae2
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
77b8aef
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
1cb68ca
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
5bd3fa9
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
d602ea6
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
31dc848
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
ce7e2c7
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
bddb9eb
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
20ec68b
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 16, 2026
7bf0009
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
3e1e95a
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
0cc0e75
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
e013190
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
15383e3
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
0aa888f
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
901a00b
Merge dev into 62-initiate-automatic-game-end-after-a-certain-amount-…
github-actions[bot] Oct 17, 2026
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
42 changes: 40 additions & 2 deletions src/main/kotlin/org/openbase/planetsudo/game/GameManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class GameManager : Runnable {
private var gameSpeedFactor: Double
var gameOverSoon: Boolean

private var gameEndDurationInMs: Long = 0
private var gameEndCalcDurationInMs: Long = 0
Comment on lines +53 to +54

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not using Duration for that?

private var gameEndTimeout: GameTimeout? = null
private var gameEndCalcTimeout: GameTimeout? = null
Comment on lines +55 to +56

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make non nullable and directly initialize it please.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculation is always at the end of the game, thus this names are quite confusing. Furthermore its not a timeout of the game end but a game timeout right ;)


init {
LOGGER.info("Create $this.")
this.gameThread = Thread(this, "GameThread")
Expand Down Expand Up @@ -116,7 +121,15 @@ class GameManager : Runnable {
LOGGER.info("Set $level as new level.")
}

fun startGame() {
fun setGameEndDuration(durationInMs: Long) {
gameEndDurationInMs = durationInMs
}

fun setGameEndCalcDuration(durationInMs: Long) {
gameEndCalcDurationInMs = durationInMs
}
Comment on lines +124 to +130

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats java not kotlin, just make the var public accessable.


fun startGame(gameEndFunction: () -> Unit) {
val gameStartThread: Thread = object : Thread("Gamestart Thread") {
override fun run() {
LOGGER.info("Init game start...")
Expand Down Expand Up @@ -146,11 +159,24 @@ class GameManager : Runnable {
}
}
level?.reset()

gameEndTimeout = null
gameEndCalcTimeout = null

if (gameEndDurationInMs > 0) {
gameEndTimeout = GameTimeout(level!!, gameEndDurationInMs) { setGameOverSoon() }
if (gameEndCalcDurationInMs > 0) {
gameEndCalcTimeout = GameTimeout(level!!, gameEndDurationInMs + gameEndCalcDurationInMs) { gameEndFunction() }
}
}

setGameState(GameState.Running)
Thread(level, "Levelrunner").start()
lock.withLock {
condition.signalAll()
}
gameEndTimeout?.startTimer()
gameEndCalcTimeout?.startTimer()
LOGGER.info("Game is Running.")
}
}
Expand All @@ -171,11 +197,23 @@ class GameManager : Runnable {
private fun setGameState(state: GameState) {
GUIController.setEvent(PropertyChangeEvent(this, GUIController.GAME_STATE_CHANGE, gameState, state))
this.gameState = state
isPause = state == GameState.Break

if (state == GameState.Break) {
isPause = true
gameEndCalcTimeout?.stopTimer()
gameEndTimeout?.stopTimer()
} else if (isPause && gameState == GameState.Running) {
isPause = false
gameEndCalcTimeout?.startTimer()
gameEndTimeout?.startTimer()
}

Comment on lines +201 to +210

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not letting the timeout consume the GameState? by that you could save lots of boilerplate code.

if (state == GameState.Running) {
isGameOver = false
} else if (state == GameState.Configuration) {
isGameOver = true
gameEndCalcTimeout?.stopTimer()
gameEndTimeout?.stopTimer()
}
}

Expand Down
57 changes: 57 additions & 0 deletions src/main/kotlin/org/openbase/planetsudo/game/GameTimeout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.openbase.planetsudo.game

import org.openbase.planetsudo.level.AbstractLevel
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.Timer

// This still de-syncs slightly from the timer in LevelMenuPanel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this an open todo / issue? Or what is the intent of this comment? :)

class GameTimeout(level: AbstractLevel, durationInMs: Long, val function: () -> Unit) : ActionListener {
private val timer: Timer = Timer(calcTimerDelay(level.getGameSpeedFactor()), this)
private var secondsRemaining = durationInMs / 1000
private var fired: Boolean = false

init { // This is a blatant copy of LevelMenuPanel, thus knowingly violating single source of truth

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

level.addPropertyChangeListener {
if (it.propertyName == AbstractLevel.GAME_SPEED_FACTOR_CHANGED) {
timer.isRunning.let { running ->
updateDelay(it.newValue as Double)
if (running) {
timer.restart()
}
}
}
}
}

private fun updateDelay(gameSpeed: Double) {
val delay = calcTimerDelay(gameSpeed)
timer.delay = delay
timer.initialDelay = delay
}

private fun calcTimerDelay(gameSpeed: Double): Int {
return (1000.0 / gameSpeed).toInt()
}

fun startTimer() {
if (!timer.isRunning && !fired) {
timer.start()
}
}

fun stopTimer() {
if (timer.isRunning) {
timer.stop()
}
}

override fun actionPerformed(ex: ActionEvent) {
secondsRemaining--
if (secondsRemaining <= 0 && !fired) {
function()
fired = true
stopTimer()
}
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/org/openbase/planetsudo/view/MainGUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ class MainGUI : JFrame, PropertyChangeListener {

private var finished = false

private fun finalizeGame() {
fun finalizeGame() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why public?

finished = true

gameManager.switchGameState(GameState.Break)
Expand Down Expand Up @@ -460,7 +460,7 @@ class MainGUI : JFrame, PropertyChangeListener {

private fun startPauseMenuItemActionPerformed(evt: ActionEvent) { // GEN-FIRST:event_startPauseMenuItemActionPerformed
when (gameManager.gameState) {
GameState.Configuration -> gameManager.startGame()
GameState.Configuration -> gameManager.startGame { finalizeGame() }
GameState.Running -> gameManager.switchGameState(GameState.Break)
GameState.Break -> gameManager.switchGameState(GameState.Running)
else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package org.openbase.planetsudo.view.configuration

import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.time.TimeZones
import org.openbase.jul.exception.CouldNotPerformException
import org.openbase.jul.exception.printer.ExceptionPrinter
import org.openbase.jul.visual.swing.image.ImageLoader
Expand All @@ -32,8 +33,10 @@ import java.awt.Color
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import javax.swing.*
import javax.swing.JSpinner.DateEditor
import javax.swing.border.BevelBorder
import javax.swing.border.SoftBevelBorder
import javax.swing.event.ListSelectionEvent
Expand Down Expand Up @@ -157,6 +160,11 @@ class ConfigurationPanel : JPanel() {
levelChooserPanel = JPanel()
levelChooserComboBox = JComboBox<String>()
startGameButton = JButton()
gameEndTimeSpinner = JSpinner()
gameEndCalcTimeSpinner = JSpinner()
gameEndTimeLabel = JLabel()
gameEndCalcTimeLabel = JLabel()
automaticEndPanel = JPanel()
networkPanel = JPanel()
networkTeamPanel = JPanel()
defaultTeamComboBox = JComboBox()
Expand Down Expand Up @@ -218,6 +226,8 @@ class ConfigurationPanel : JPanel() {

gameSettingsPanel!!.border = BorderFactory.createTitledBorder("Spiel Einstellungen")

automaticEndPanel!!.border = BorderFactory.createTitledBorder("Automatisches Ende")

teamsPanel!!.border = BorderFactory.createTitledBorder("Teams")

versusLabel!!.horizontalAlignment = SwingConstants.CENTER
Expand Down Expand Up @@ -418,6 +428,65 @@ class ConfigurationPanel : JPanel() {
startGameButton!!.text = "Spiel Starten"
startGameButton!!.addActionListener { _ -> startGameButtonActionPerformed() }

configureDateSpinner(gameEndTimeSpinner!!)
configureDateSpinner(gameEndCalcTimeSpinner!!)

gameEndTimeSpinner!!.addChangeListener { e -> gameManager.setGameEndDuration(((e.source as JSpinner).value as Date).time) }
gameEndCalcTimeSpinner!!.addChangeListener { e -> gameManager.setGameEndCalcDuration(((e.source as JSpinner).value as Date).time) }

gameEndTimeLabel!!.horizontalAlignment = SwingConstants.LEADING
gameEndTimeLabel!!.text = "Automatisches Spielende einleiten"

gameEndCalcTimeLabel!!.horizontalAlignment = SwingConstants.LEADING
gameEndCalcTimeLabel!!.text = "danach automatische Endabrechnung"

val automaticEndPanelLayout = GroupLayout(automaticEndPanel)
automaticEndPanel!!.layout = automaticEndPanelLayout
automaticEndPanelLayout.setHorizontalGroup(
automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(
automaticEndPanelLayout.createSequentialGroup()
.addContainerGap()
.addComponent(gameEndTimeLabel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(gameEndTimeSpinner)
.addContainerGap(),
)
.addGroup(
automaticEndPanelLayout.createSequentialGroup()
.addContainerGap()
.addComponent(gameEndCalcTimeLabel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(gameEndCalcTimeSpinner)
.addContainerGap(),
),
)

automaticEndPanelLayout.setVerticalGroup(
automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(
automaticEndPanelLayout.createSequentialGroup()
.addGroup(
automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(
gameEndTimeSpinner,
)
.addComponent(
gameEndTimeLabel,
),
)
.addGroup(
automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(
gameEndCalcTimeSpinner,
)
.addComponent(
gameEndCalcTimeLabel,
),
),
),
)

val gameSettingsPanelLayout = GroupLayout(gameSettingsPanel)
gameSettingsPanel!!.layout = gameSettingsPanelLayout
gameSettingsPanelLayout.setHorizontalGroup(
Expand All @@ -439,6 +508,12 @@ class ConfigurationPanel : JPanel() {
GroupLayout.DEFAULT_SIZE,
Short.MAX_VALUE.toInt(),
)
.addComponent(
automaticEndPanel,
GroupLayout.DEFAULT_SIZE,
GroupLayout.DEFAULT_SIZE,
Short.MAX_VALUE.toInt(),
)
.addComponent(
startGameButton,
GroupLayout.Alignment.TRAILING,
Expand All @@ -459,6 +534,8 @@ class ConfigurationPanel : JPanel() {
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(levelChooserPanel, GroupLayout.PREFERRED_SIZE, 62, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(automaticEndPanel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(startGameButton)
.addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE.toInt()),
),
Expand Down Expand Up @@ -668,6 +745,22 @@ class ConfigurationPanel : JPanel() {
)
} // </editor-fold>//GEN-END:initComponents

private fun configureDateSpinner(dateSpinner: JSpinner) {
val dateModel = SpinnerDateModel()
dateModel.calendarField = Calendar.MINUTE
dateSpinner.model = dateModel
dateModel.start = Date(0)
dateModel.end = Date(60 * 60 * 1000 - 1) // 59:59 in ms

dateSpinner.editor = DateEditor(dateSpinner)
val dateFormat: SimpleDateFormat = (dateSpinner.editor as DateEditor).format
dateFormat.applyLocalizedPattern("mm:ss")
dateFormat.timeZone = TimeZones.GMT // GMT because that's where a displayed value of "00:00" is 0 ms

// Update value here because of https://stackoverflow.com/a/37444974
dateModel.value = Date(0) // TODO keep last value as default?
}

private fun levelChooserComboBoxActionPerformed() { // GEN-FIRST:event_levelChooserComboBoxActionPerformed
object : SwingWorker<Any?, Any?>() {
@Throws(Exception::class)
Expand Down Expand Up @@ -716,7 +809,7 @@ class ConfigurationPanel : JPanel() {

private fun startGameButtonActionPerformed() { // GEN-FIRST:event_startGameButtonActionPerformed
MainGUI.instance!!.showLoadingPanel()
gameManager.startGame()
gameManager.startGame { MainGUI.instance!!.finalizeGame() }
} // GEN-LAST:event_startGameButtonActionPerformed

private fun teamAComboBoxActionPerformed() { // GEN-FIRST:event_teamAComboBoxActionPerformed
Expand Down Expand Up @@ -794,6 +887,11 @@ class ConfigurationPanel : JPanel() {
private var connectionStateLabel: JLabel? = null
private var defaultTeamComboBox: JComboBox<TeamData>? = null
private var startGameButton: JButton? = null
private var gameEndTimeSpinner: JSpinner? = null
private var gameEndCalcTimeSpinner: JSpinner? = null
private var gameEndTimeLabel: JLabel? = null
private var gameEndCalcTimeLabel: JLabel? = null
private var automaticEndPanel: JPanel? = null
private var teamALabel: JLabel? = null
private var teamBLabel: JLabel? = null
private var versusLabel: JLabel? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,30 @@ class LevelMenuPanel : JPanel(), ActionListener {

fun setLevel(level: AbstractLevel) {
levelName = level.name
updateDelay(level.getGameSpeedFactor())
level.addPropertyChangeListener {
if (it.propertyName == AbstractLevel.GAME_SPEED_FACTOR_CHANGED) {
timer.isRunning.let { running ->
timer.delay = (1000.0 * (1 / (it.newValue as Double))).toInt()
if (running) timer.restart()
updateDelay(it.newValue as Double)
if (running) {
timer.restart()
}
}
}
}
updateTitle()
}

private fun updateDelay(gameSpeed: Double) {
val delay = calcTimerDelay(gameSpeed)
timer.delay = delay
timer.initialDelay = delay
}

private fun calcTimerDelay(gameSpeed: Double): Int {
return (1000.0 / gameSpeed).toInt()
}

private var text: String? = null
private fun updateTitle() {
text = "$levelName ["
Expand All @@ -52,7 +65,9 @@ class LevelMenuPanel : JPanel(), ActionListener {
}

fun startTimer() {
if (!timer.isRunning) timer.start()
if (!timer.isRunning) {
timer.start()
}
}

fun stopTimer() {
Expand Down