Skip to content

Commit

Permalink
Add support for mobile devices
Browse files Browse the repository at this point in the history
Other changes in this commit:
- Remove serialization support
- Remove Alert component and optimize imports
  • Loading branch information
haukesomm committed Sep 24, 2023
1 parent dc33020 commit 9dc2a3e
Show file tree
Hide file tree
Showing 29 changed files with 126 additions and 139 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ game published in 1984 by Spectrum Holobyte.

The latest snapshot version can be played [here](https://sokoban.haukesomm.de).

<img src="./assets/screenshot.png" width="50%">
## Screenshots

<img src="./assets/screenshot-desktop.png" width="50%">
<img src="./assets/screenshot-mobile.png" width="25%">

## Modules

Expand Down
Binary file added assets/screenshot-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshot-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/screenshot.png
Binary file not shown.
1 change: 0 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("multiplatform") version "1.7.20" apply false
kotlin("plugin.serialization") version "1.7.20" apply false
id("com.google.devtools.ksp") version "1.7.21-1.0.8" apply false
}

Expand Down
3 changes: 0 additions & 3 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
}

Expand All @@ -20,13 +19,11 @@ kotlin {
val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.extra["coroutinesVersion"]}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${rootProject.ext["serializationVersion"]}")
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test"))
//implementation(kotlin("test-annotations-common"))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.haukesomm.sokoban.core

import kotlinx.serialization.Serializable

/**
* Represents the type an [Entity] can have.
*/
Expand All @@ -10,7 +8,6 @@ enum class EntityType { Box, Player }
/**
* Represents an entity in the game that can be moved and placed on a [Tile].
*/
@Serializable
data class Entity(
val type: EntityType
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.haukesomm.sokoban.core

import kotlinx.serialization.Serializable

/**
* Represents the state of a game.
*
Expand Down Expand Up @@ -107,7 +105,6 @@ interface GameState {
* This implementation does not allow modifications to the game state.
* A mutable copy of the game state can be created via the [toMutableGameState] method.
*/
@Serializable
data class ImmutableGameState(
override val levelId: String,
override val width: Int,
Expand All @@ -132,7 +129,6 @@ fun GameState.toImmutableGameState(): ImmutableGameState =
* This implementation allows modifications to the game state.
* An immutable copy of the game state can be created via the [toImmutableGameState] method.
*/
@Serializable
data class MutableGameState(
override var levelId: String,
override var width: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package de.haukesomm.sokoban.core

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map

/**
* Represents a Sokoban game.
Expand Down
3 changes: 0 additions & 3 deletions core/src/commonMain/kotlin/de/haukesomm/sokoban/core/Tile.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.haukesomm.sokoban.core

import kotlinx.serialization.Serializable

enum class TileType { Empty, Wall, Target }

/**
Expand All @@ -20,7 +18,6 @@ data class TileProperties(
*
* A tile can hold an [Entity] and has a [TileType] that determines if it is a wall, a target or empty.
*/
@Serializable
data class Tile(
val type: TileType,
val entity: Entity? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.haukesomm.sokoban.core.levels

import de.haukesomm.sokoban.core.CharacterMap
import de.haukesomm.sokoban.core.Level
import de.haukesomm.sokoban.core.LevelDescription
import de.haukesomm.sokoban.core.LevelRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.haukesomm.sokoban.core.moving

import de.haukesomm.sokoban.core.*
import de.haukesomm.sokoban.core.Level
import de.haukesomm.sokoban.core.moving.rules.BoxDetectingMoveRule
import de.haukesomm.sokoban.core.moving.rules.MultipleBoxesPreventingMoveRule
import de.haukesomm.sokoban.core.moving.rules.WallCollisionPreventingMoveRule
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package de.haukesomm.sokoban.legacy;

import javax.swing.*;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;

import de.haukesomm.sokoban.core.GameState;
import de.haukesomm.sokoban.core.Position;
import de.haukesomm.sokoban.legacy.textures.JarResourceTextureRepository;
import de.haukesomm.sokoban.legacy.textures.TextureRepository;

import javax.swing.*;
import java.awt.*;

public class GameField extends JPanel {

private final TextureRepository textureRepository = new JarResourceTextureRepository();
Expand Down
11 changes: 5 additions & 6 deletions legacy/src/main/java/de/haukesomm/sokoban/legacy/GameFrame.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package de.haukesomm.sokoban.legacy;

import javax.swing.*;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;

import de.haukesomm.sokoban.core.GameStateChangeHandler;
import de.haukesomm.sokoban.core.Direction;
import de.haukesomm.sokoban.core.GameStateChangeHandler;
import de.haukesomm.sokoban.core.SokobanGame;
import de.haukesomm.sokoban.core.SokobanGameFactory;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;

public class GameFrame extends JFrame {

private class MovePlayerAction extends AbstractAction {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package de.haukesomm.sokoban.legacy;

import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.*;
import java.awt.*;

public class InfoWindow extends JFrame {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package de.haukesomm.sokoban.legacy;

import java.awt.*;
import de.haukesomm.sokoban.core.LevelDescription;
import de.haukesomm.sokoban.legacy.level.LevelDescriptionListCellRenderer;

import javax.swing.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.*;

import de.haukesomm.sokoban.core.LevelDescription;
import de.haukesomm.sokoban.legacy.level.LevelDescriptionListCellRenderer;

public class LevelInfoBar extends JPanel {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package de.haukesomm.sokoban.legacy;

import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.*;

public class MenuBar extends JMenuBar {

Expand Down
5 changes: 0 additions & 5 deletions web/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
}

Expand All @@ -19,7 +15,6 @@ kotlin {
val commonMain by getting {
dependencies {
implementation("dev.fritz2:headless:$fritz2version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${rootProject.ext["serializationVersion"]}")
implementation(project(":core"))
}
}
Expand Down
25 changes: 2 additions & 23 deletions web/src/jsMain/kotlin/de/haukesomm/sokoban/web/App.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
package de.haukesomm.sokoban.web

import de.haukesomm.sokoban.web.components.alert
import de.haukesomm.sokoban.web.components.icons.HeroIcons
import de.haukesomm.sokoban.web.theme.DarkModeStore
import dev.fritz2.core.RenderContext
import kotlinx.browser.document

fun main() {
renderWithPortalRoot {
initTheme()

div("hidden md:block") {
gameFrame()
}
div("block md:hidden") {
mobileDeviceNotSupportedBanner()
}
gameFrame()
}
}

Expand All @@ -31,16 +22,4 @@ private fun initTheme() {
document.querySelector("body")
?.classList
?.add("text-neutral-dark", "dark:text-neutral-light")
}

private fun RenderContext.mobileDeviceNotSupportedBanner() =
div("m-4") {
alert(
title = "Mobile devices are not yet supported",
message = """It appears you are accessing this app from a mobile device.
| Unfortunately, Sokoban is not playable on mobile devices yet.
| Please try again on a desktop computer.
""".trimMargin(),
icon = HeroIcons.device_mobile
)
}
}
66 changes: 39 additions & 27 deletions web/src/jsMain/kotlin/de/haukesomm/sokoban/web/GameFrame.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package de.haukesomm.sokoban.web

import de.haukesomm.sokoban.core.Direction
import de.haukesomm.sokoban.core.LevelDescription
import de.haukesomm.sokoban.core.SokobanGame
import de.haukesomm.sokoban.core.SokobanGameFactory
import de.haukesomm.sokoban.core.LevelDescription
import de.haukesomm.sokoban.web.components.*
import de.haukesomm.sokoban.web.components.game.MoveEvent
import de.haukesomm.sokoban.web.components.game.gameField
import de.haukesomm.sokoban.web.components.game.moveButtons
import de.haukesomm.sokoban.web.components.icons.*
import de.haukesomm.sokoban.web.theme.ThemePreference
import de.haukesomm.sokoban.web.theme.ThemePreferences
Expand All @@ -15,10 +17,6 @@ import kotlinx.coroutines.flow.*

class GameFrame {

companion object {
private const val MAX_TITLEBAR_WIDTH_CLASSES = "max-w-4xl"
}

private val enabledGameConfigurations = storeOf(SokobanGameFactory.configurationOptions)

private var gameFlow = MutableStateFlow(
Expand All @@ -28,20 +26,6 @@ class GameFrame {
)


private fun RenderContext.initializeKeyboardInput(game: SokobanGame) {
Window.keydowns.map { shortcutOf(it.key) } handledBy { shortcut ->
game.run {
when(shortcut) {
Keys.ArrowUp -> movePlayerIfPossible(Direction.Top)
Keys.ArrowDown -> movePlayerIfPossible(Direction.Bottom)
Keys.ArrowLeft -> movePlayerIfPossible(Direction.Left)
Keys.ArrowRight -> movePlayerIfPossible(Direction.Right)
}
}
}
}


private fun RenderContext.initializeLevelClearedAlert(game: SokobanGame) {
game.state
.filter { it.levelCleared }
Expand Down Expand Up @@ -71,15 +55,44 @@ class GameFrame {

fun RenderContext.render() {
gameFlow.render { game ->
initializeKeyboardInput(game)
initializeLevelClearedAlert(game)

div("mb-4 w-full flex flex-col gap-4 items-center") {
// Disable double-tap to zoom on mobile devices as this interferes with the
// virtual gamepad.
inlineStyle("touch-action: manipulation;")

titleBar(game)

div("max-w-min rounded-lg shadow overflow-hidden") {
gameField(game.state)
}

div("block md:hidden") {
moveButtons {
merge(
moveEvents,
Window.keydowns.mapNotNull {
when(shortcutOf(it.key)) {
Keys.ArrowUp -> MoveEvent.Up
Keys.ArrowDown -> MoveEvent.Down
Keys.ArrowLeft -> MoveEvent.Left
Keys.ArrowRight -> MoveEvent.Right
else -> null
}
},
) handledBy { moveEvent ->
game.run {
when(moveEvent) {
MoveEvent.Up -> movePlayerIfPossible(Direction.Top)
MoveEvent.Down -> movePlayerIfPossible(Direction.Bottom)
MoveEvent.Left -> movePlayerIfPossible(Direction.Left)
MoveEvent.Right -> movePlayerIfPossible(Direction.Right)
}
}
}
}
}
}
}
}
Expand All @@ -95,18 +108,18 @@ class GameFrame {
}

disclosure(
"""w-full py-2 px-4 flex flex-col items-center bg-background-lightest dark:bg-background-dark
"""w-full py-2 px-4 flex flex-col items-stretch md:items-center bg-background-lightest dark:bg-background-dark
| shadow-sm dark:shadow-md""".trimMargin()
) {
div("w-full flex flex-row justify-between items-center gap-4 text-sm") {
div("w-full flex flex-row flex-wrap justify-between items-center gap-x-4 gap-y-6 text-sm") {
div("flex flex-row items-center gap-2"){
icon("w-7 h-7", definition = SokobanAppIcons.logo)
span("text-xl font-semibold text-primary-500 dark:text-primary-600") {
+"Sokoban"
}
}
div("grow max-w-lg flex gap-6 items-center") {
div("grow") {
div("grow max-w-lg flex flex-wrap gap-x-6 gap-y-2 items-center") {
div("grow w-full md:w-auto") {
listBox {
entries = levelDescriptions
format = LevelDescription::name
Expand Down Expand Up @@ -169,9 +182,8 @@ class GameFrame {
disclosurePanel {
div(
classes(
"""mt-4 my-2 p-4 bg-background-light dark:bg-background-darkest rounded-md
| grid grid-cols-3 gap-6""".trimMargin(),
MAX_TITLEBAR_WIDTH_CLASSES
"""max-w-4xl mt-4 my-2 p-4 bg-background-light dark:bg-background-darkest rounded-md
| grid grid-cols-1 md:grid-cols-3 gap-6""".trimMargin(),
)
) {
withTitle("Configuration options") {
Expand Down
Loading

0 comments on commit 9dc2a3e

Please sign in to comment.