diff --git a/.gitignore b/.gitignore index 33bfc3b..3a51581 100644 --- a/.gitignore +++ b/.gitignore @@ -19,52 +19,18 @@ gradle-app.setting .idea -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - ### Kotlin template # Compiled class file *.class @@ -89,192 +55,3 @@ fabric.properties # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* - -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Kotlin template -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -### Gradle template -.gradle -**/build/ -!client/src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Cache of project -.gradletasknamecache - -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - diff --git a/build.gradle.kts b/build.gradle.kts index d571af3..3e1c1df 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,32 +1,9 @@ -plugins { - application - kotlin("jvm") version "1.4.32" - kotlin("plugin.serialization") version "1.4.32" - id("io.gitlab.arturbosch.detekt") version "1.16.0-RC2" +allprojects { + repositories { + jcenter() + } } -group = "ru.ifmo.jb" -version = "1.0-SNAPSHOT" -val ktor_version = "1.5.3" - -application { - mainClass.set("ru.ifmo.sd.MainKt") -} - -repositories { - mavenCentral() -} - -dependencies { - implementation("io.ktor:ktor-websockets:$ktor_version") - implementation("io.ktor:ktor-client-cio:$ktor_version") - implementation("io.ktor:ktor-client-gson:$ktor_version") - implementation("io.ktor:ktor-gson:$ktor_version") - implementation("io.ktor:ktor-server-core:$ktor_version") - implementation("io.ktor:ktor-server-netty:$ktor_version") - testImplementation("io.ktor:ktor-server-tests:$ktor_version") -} - -tasks.test { - useJUnit() +subprojects { + version = "1.0" } diff --git a/game-client/build.gradle.kts b/game-client/build.gradle.kts new file mode 100644 index 0000000..34c6d13 --- /dev/null +++ b/game-client/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + java + application + kotlin("jvm") version "1.4.21" +} + +group = "ru.ifmo.jb" +version = "1.0-SNAPSHOT" +val ktor_version = "1.5.2" + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) + implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.32") + implementation("io.ktor:ktor-client-cio:$ktor_version") + implementation("io.ktor:ktor-client-gson:$ktor_version") + implementation("io.ktor:ktor-client-jackson:$ktor_version") + implementation("io.ktor:ktor-client-serialization:1.5.2") + implementation(project(":game-server")) +} + +application { + mainClass.set("ru.ifmo.sd.MainKt") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt new file mode 100644 index 0000000..c05b6d9 --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -0,0 +1,56 @@ +package ru.ifmo.sd + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import ru.ifmo.sd.httpapi.models.LevelConfiguration +import ru.ifmo.sd.httpapi.models.MoveEventData +import ru.ifmo.sd.stuff.GUI +import ru.ifmo.sd.world.configuration.GameConfiguration +import ru.ifmo.sd.world.configuration.GameConfigurationSerializable +import ru.ifmo.sd.world.representation.Position +import ru.ifmo.sd.world.representation.units.GameUnit +import ru.ifmo.sd.world.representation.units.Player +import java.awt.EventQueue + + +internal var client: HttpClient? = null + +suspend fun main() { + client = HttpClient(CIO) { + install(JsonFeature) { + serializer = KotlinxSerializer() + } + } + val response = makeNewGameConfiguration() + + println(response.deserializeBack()) + + EventQueue.invokeLater { createAndShowGUI(response.deserializeBack()) } + +// client.close() +} + +internal suspend fun makeNewGameConfiguration(): GameConfigurationSerializable { + return client!!.post("http://localhost:8080/start") { + contentType(ContentType.Application.Json) + body = LevelConfiguration(levelLength = 3, levelWidth = 3) + } +} + +internal suspend fun makeMove(newPos: Position, unit: GameUnit = Player(0)) { + client!!.post("http://localhost:8080/move") { + contentType(ContentType.Application.Json) + body = MoveEventData(unit, newPos) + } +} + +private fun createAndShowGUI(config: GameConfiguration) { + val frame = GUI("Roguelike", config) + frame.isVisible = true +} \ No newline at end of file diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt new file mode 100644 index 0000000..27fcaae --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -0,0 +1,243 @@ +package ru.ifmo.sd.stuff + +import kotlinx.coroutines.runBlocking +import ru.ifmo.sd.client +import ru.ifmo.sd.makeMove +import ru.ifmo.sd.makeNewGameConfiguration +import ru.ifmo.sd.stuff.ColoredSymbol.* +import ru.ifmo.sd.world.configuration.GameConfiguration +import ru.ifmo.sd.world.representation.Position +import java.awt.BorderLayout +import java.awt.Color.* +import java.awt.Container +import java.awt.event.KeyEvent +import java.awt.event.KeyEvent.* +import java.awt.event.KeyListener +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.* +import javax.swing.text.Style + +enum class MoveEvent { + UP, DOWN, LEFT, RIGHT, NONE +} + +class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyListener { + private var currPos = gameConfiguration.playerPos + private val mainPanel = JPanel() + private val headerPanel = JPanel() + private val mapPanel = JPanel() + private val infoPanel = JPanel() + private var map = SymbolMap(gameConfiguration) + private var mapTextPane = MapTextPane() + + init { + createUI(title) + start() + } + + private fun createUI(title: String) { + setTitle(title) + + createLayout() + makeNotFocusable() + + defaultCloseOperation = EXIT_ON_CLOSE + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + client!!.close() + e.window.dispose() + } + }) + setSize(800, 600) + isResizable = false + setLocationRelativeTo(null) + + // TODO should work regardless of component being focused + isFocusable = true + focusTraversalKeysEnabled = false + this.addKeyListener(this) + + requestFocus() + requestFocusInWindow() + } + + private fun start() { + Thread { + while (true) { + + try { + Thread.sleep(30) + } catch (e: InterruptedException) { + break + } + + mainPanel.repaint() + } + }.start() + } + + private fun createLayout() { + add(mainPanel) + mainPanel.layout = BorderLayout() + + headerPanel.setSize(800, 50) + mainPanel.add(headerPanel, BorderLayout.NORTH) +// val infoLabel = JLabel("loading...") +// infoLabel.isFocusable = false +// headerPanel.add(infoLabel) + + mapPanel.setSize(550, 550) + mainPanel.add(mapPanel, BorderLayout.WEST) + mapPanel.add(mapTextPane) + createMapTextPane() + + infoPanel.setSize(250, 550) + mainPanel.add(infoPanel, BorderLayout.EAST) + +// val hpLabel = JLabel("HP: ") +// hpLabel.isFocusable = false +// infoPanel.add(hpLabel, BorderLayout.WEST) + } + + private fun makeNotFocusable(container: Container = this) { + container.isFocusable = false + for (c in container.components) { + if (c is Container) makeNotFocusable(c) + } + } + + private fun createMapTextPane() { + mapTextPane.document.remove(0, mapTextPane.document.length) + for (i in 0 until map.rowSize) { + if (i != 0) { + insertText("\n", mapTextPane.colorMap[BLACK]!!) + } + for (j in 0 until map.columnSize) { + val symbol = map.rows[i][j] + val style = mapTextPane.colorMap[symbol.color]!! + insertText(symbol.char.toString(), style) + } + } + // TODO: make lineSpacing = 0 +// mapTextPane.margin = Insets(0, 0, 0, 0) +// mapTextPane.selectAll() +// val aSet = SimpleAttributeSet(mapTextPane.paragraphAttributes) +// val doc = mapTextPane.styledDocument +// mapTextPane.setParagraphAttributes(aSet, false) +// doc.setParagraphAttributes(0, doc.length, aSet, false) +// StyleConstants.setLineSpacing(aSet, 0F) +// mapTextPane.select(0, 0) + } + + private fun insertText(string: String, style: Style) { + try { + val doc = mapTextPane.document + doc.insertString(doc.length, string, style) + } catch (e: Exception) { + e.printStackTrace() // TODO + } + } + + ///////////////////////////////////// + + var moved = MoveEvent.NONE + var didMove = false + + override fun keyReleased(e: KeyEvent?) {} + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) { + if (e != null) { + println("Some key pressed, current=$moved, keyCode=${e.keyCode}, didMove=$didMove, currPos=$currPos") + } else { + println("e == null") + } + if (!didMove) { + when (e?.keyCode) { + null -> { + moved = MoveEvent.NONE + } + VK_UP, VK_W -> { + val newPos = Position(currPos.row - 1, currPos.column) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.UP + currPos = newPos + } + } + VK_DOWN, VK_S -> { + val newPos = Position(currPos.row + 1, currPos.column) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.DOWN + currPos = newPos + } + } + VK_LEFT, VK_A -> { + val newPos = Position(currPos.row, currPos.column - 1) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.LEFT + currPos = newPos + } + } + VK_RIGHT, VK_D -> { + val newPos = Position(currPos.row, currPos.column + 1) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.RIGHT + currPos = newPos + } + } + } + + if (didMove) { + println("Should move=$moved") + + didMove = false + this.validate() + this.mapPanel.validate() + this.mapTextPane.validate() + this.repaint() + this.mapPanel.repaint() + this.mapTextPane.repaint() + + runBlocking { + if (currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) { + val newConfig = makeNewGameConfiguration().deserializeBack() + map = SymbolMap(newConfig) + createMapTextPane() + currPos = newConfig.playerPos + } else { + makeMove(currPos) + } + } + } + } + } + + private fun replaceSymbol(oldPos: Position, newPos: Position, replaceSymb: ColoredSymbol = NONE): Boolean { + if (newPos.row < map.rowSize && newPos.row >= 0 && + newPos.column < map.columnSize && newPos.column >= 0 && + map.rows[newPos.row][newPos.column] == NONE + ) { + val oldSymb = map.rows[oldPos.row][oldPos.column] + map.rows[oldPos.row][oldPos.column] = replaceSymb + map.rows[newPos.row][newPos.column] = oldSymb + replacePaneSymbol(oldPos, replaceSymb) + replacePaneSymbol(newPos, oldSymb) + return true + } + return false + } + + private fun replacePaneSymbol(pos: Position, symbol: ColoredSymbol) { + mapTextPane.isEditable = true + val index = pos.row * (map.columnSize + 1) + pos.column + mapTextPane.select(index, index + 1) +// println("selected text='${mapTextPane.selectedText}'") + mapTextPane.selectionColor = symbol.color + mapTextPane.replaceSelection(symbol.char.toString()) + mapTextPane.isEditable = false + } +} \ No newline at end of file diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt new file mode 100644 index 0000000..288f4ad --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt @@ -0,0 +1,27 @@ +package ru.ifmo.sd.stuff + +import ru.ifmo.sd.world.representation.Position +import java.awt.Color +import java.awt.Color.* +import javax.swing.JTextPane +import javax.swing.text.Style +import javax.swing.text.StyleConstants +import java.awt.Graphics; + +class MapTextPane : JTextPane() { + private val FONT_FAMILY = "Monospaced" + private val FONT_SIZE = 24 + val colorMap: MutableMap = mutableMapOf() + private val colors = arrayOf(BLACK, RED, YELLOW, GREEN, MAGENTA, ORANGE, BLUE, GRAY) + + init { + isEditable = false + for (color in colors) { + val newStyle = addStyle(color.toString(), null) + StyleConstants.setFontFamily(newStyle, FONT_FAMILY) + StyleConstants.setFontSize(newStyle, FONT_SIZE) + colorMap[color] = newStyle + StyleConstants.setForeground(newStyle, color) + } + } +} \ No newline at end of file diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt new file mode 100644 index 0000000..72d0a48 --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -0,0 +1,28 @@ +package ru.ifmo.sd.stuff + +import ru.ifmo.sd.stuff.ColoredSymbol.* +import ru.ifmo.sd.world.configuration.GameConfiguration +import java.awt.Color +import java.awt.Color.* + + +enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { + WALL(0x2591.toChar(), GRAY), + PLAYER(0x267F.toChar()/*, RED*/), + NONE(' '), +} + +class SymbolMap(config: GameConfiguration) { + val rows: List> = config.level.map { arr -> + arr.map { i -> if (i == 0) NONE else WALL }.toMutableList() + } + + init { + rows[config.playerPos.row][config.playerPos.column] = PLAYER + } + + val rowSize: Int + get() = rows.size + val columnSize: Int + get() = rows[0].size +} \ No newline at end of file diff --git a/game-server/build.gradle.kts b/game-server/build.gradle.kts new file mode 100644 index 0000000..e7e646a --- /dev/null +++ b/game-server/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + java + application + kotlin("jvm") version "1.4.21" + kotlin("plugin.serialization") version "1.4.21" +} + +group = "ru.ifmo.jb" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) + + implementation("ch.qos.logback:logback-classic:1.2.3") + + implementation("io.ktor:ktor-server-core:1.5.2") + implementation("io.ktor:ktor-server-netty:1.5.2") + implementation("io.ktor:ktor-serialization:1.5.2") + testImplementation("io.ktor:ktor-server-tests:1.5.2") +} + +application { + mainClassName = "ru.ifmo.sd.httpapi.ApplicationKt" +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "1.8" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" + } +} + +tasks.test { + useJUnitPlatform() +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/Application.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/Application.kt new file mode 100644 index 0000000..bd00812 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/Application.kt @@ -0,0 +1,15 @@ +package ru.ifmo.sd.httpapi + +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.serialization.* +import ru.ifmo.sd.httpapi.routes.registerGameRoutes + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module(testing: Boolean = false) { + install(ContentNegotiation) { + json() + } + registerGameRoutes() +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/LevelConfiguration.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/LevelConfiguration.kt new file mode 100644 index 0000000..4e8ddcf --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/LevelConfiguration.kt @@ -0,0 +1,6 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class LevelConfiguration(val levelLength: Int, val levelWidth: Int) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt new file mode 100644 index 0000000..d7866c4 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt @@ -0,0 +1,8 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable +import ru.ifmo.sd.world.representation.Position +import ru.ifmo.sd.world.representation.units.GameUnit + +@Serializable +data class MoveEventData(val targetUnit: GameUnit, val newPos: Position) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/routes/GameRoutes.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/routes/GameRoutes.kt new file mode 100644 index 0000000..c91d1fa --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/routes/GameRoutes.kt @@ -0,0 +1,32 @@ +package ru.ifmo.sd.httpapi.routes + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import ru.ifmo.sd.httpapi.models.LevelConfiguration +import ru.ifmo.sd.httpapi.models.MoveEventData +import ru.ifmo.sd.world.events.EventsHandler + +fun Route.gameRouting() { + route("/") { + post("/start") { + val levelConfiguration = call.receive() + val startedGame = EventsHandler.startGame(levelConfiguration) + call.respond(message = startedGame.makeSerializable(), status = HttpStatusCode.Created) + } + + post("/move") { + val moveEventData = call.receive() + EventsHandler.move(moveEventData.targetUnit, moveEventData.newPos) + call.respondText("Position successfully updated", status = HttpStatusCode.Accepted) + } + } +} + +fun Application.registerGameRoutes() { + routing { + gameRouting() + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt new file mode 100644 index 0000000..911d1a3 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt @@ -0,0 +1,40 @@ +package ru.ifmo.sd.world.configuration + +import io.ktor.serialization.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import ru.ifmo.sd.world.representation.* +import ru.ifmo.sd.world.representation.units.GameUnit + +@Serializable +data class GameConfiguration( + val playerPos: Position, + val level: Array, + val unitsPositions: Map, + val unitsHealthStorage: Map +) { + fun makeSerializable(): GameConfigurationSerializable { + return GameConfigurationSerializable( + playerPos, level, + unitsPositions.mapKeys { (gameUnit, _) -> Json.encodeToString(GameUnit.serializer(), gameUnit) }, + unitsHealthStorage.mapKeys { (gameUnit, _) -> Json.encodeToString(GameUnit.serializer(), gameUnit) } + ) + } +} + +@Serializable +data class GameConfigurationSerializable( + val playerPos: Position, + val level: Array, + val unitsPositions: Map, + val unitsHealthStorage: Map +) { + fun deserializeBack(): GameConfiguration { + return GameConfiguration( + playerPos, level, + unitsPositions.mapKeys { (jsonString, _) -> Json.decodeFromString(GameUnit.serializer(), jsonString) }, + unitsHealthStorage.mapKeys { (jsonString, _) -> Json.decodeFromString(GameUnit.serializer(), jsonString) } + ) + } + +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt new file mode 100644 index 0000000..1c9db5f --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt @@ -0,0 +1,44 @@ +package ru.ifmo.sd.world.configuration + +import ru.ifmo.sd.world.generation.LevelGenerator +import ru.ifmo.sd.world.representation.* +import ru.ifmo.sd.world.representation.units.Player + +object GameConfigurator { + private lateinit var gameLevel: GameLevel + private lateinit var unitsPositions: UnitsPositionStorage + private lateinit var unitsHealths: UnitsHealthStorage + + fun getGameLevel(): GameLevel { + return gameLevel + } + + fun getUnitsPositions(): UnitsPositionStorage { + return unitsPositions + } + + fun getUnitsHealths(): UnitsHealthStorage { + return unitsHealths + } + + fun configure(levelLength: Int, levelWidth: Int): GameConfiguration { + gameLevel = GameLevel( + LevelGenerator.generateLevel( + levelLength, levelWidth + ) + ) + val playerId = 1 + val player = Player(playerId) + val playerPos = Position(1, 1) + unitsPositions = UnitsPositionStorage() + unitsHealths = UnitsHealthStorage() + unitsPositions.move(player, playerPos) + unitsHealths.addUnit(player) + return GameConfiguration( + playerPos, + gameLevel.gameLevel, + unitsPositions.getPositions(), + unitsHealths.getHealths() + ) + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt new file mode 100644 index 0000000..ef7865f --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt @@ -0,0 +1,19 @@ +package ru.ifmo.sd.world.events + +import ru.ifmo.sd.httpapi.models.LevelConfiguration +import ru.ifmo.sd.world.configuration.GameConfiguration +import ru.ifmo.sd.world.configuration.GameConfigurator +import ru.ifmo.sd.world.representation.Position +import ru.ifmo.sd.world.representation.units.GameUnit + +class EventsHandler { + companion object { + fun startGame(levelConf: LevelConfiguration): GameConfiguration { + return GameConfigurator.configure(levelConf.levelLength, levelConf.levelWidth) + } + + fun move(targetUnit: GameUnit, newPos: Position) { + GameConfigurator.getUnitsPositions().move(targetUnit, newPos) + } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/generation/LevelGenerator.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/generation/LevelGenerator.kt new file mode 100644 index 0000000..a547142 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/generation/LevelGenerator.kt @@ -0,0 +1,64 @@ +package ru.ifmo.sd.world.generation + +import kotlin.random.Random + +class LevelGenerator { + companion object { + private lateinit var level: Array + + fun generateLevel(length: Int, width: Int): Array { + val actualLength = if (length < 4) 9 else 2 * length + 1 + val actualWidth = if (width < 4) 9 else 2 * width + 1 + level = Array(actualLength) { IntArray(actualWidth) { 1 } } + + val enterPosRow = 0 + val enterPosColumn = 1 + level[enterPosRow][enterPosColumn] = 0 + + val exitPosRow = actualLength - 1 + val exitPosColumn = actualWidth - 2 + level[exitPosRow][exitPosColumn] = 0 + + val playerPosRow = 1 + val playerPosColumn = 1 + level[playerPosRow][playerPosColumn] = 0 + + generateRec(playerPosRow, playerPosColumn) + + return level + } + + private fun generateRec(curRow: Int, curColumn: Int) { + var i: Int + val dirs: Array = Array(3) { IntArray(2) { 0 } } + while (true) { + i = 0 + if (curRow > 1 && level[curRow - 2][curColumn] != 0) { + dirs[i][0] = curRow - 2 + dirs[i][1] = curColumn + i++ + } + if (curRow < level.size - 2 && level[curRow + 2][curColumn] != 0) { + dirs[i][0] = curRow + 2 + dirs[i][1] = curColumn + i++ + } + if (curColumn > 1 && level[curRow][curColumn - 2] != 0) { + dirs[i][0] = curRow + dirs[i][1] = curColumn - 2 + i++ + } + if (curColumn < level[0].size - 2 && level[curRow][curColumn + 2] != 0) { + dirs[i][0] = curRow + dirs[i][1] = curColumn + 2 + i++ + } + if (i == 0) break + i = (i * Random.nextDouble()).toInt() + level[(dirs[i][0] + curRow) / 2][(dirs[i][1] + curColumn) / 2] = 0 + level[dirs[i][0]][dirs[i][1]] = 0 + generateRec(dirs[i][0], dirs[i][1]) + } + } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/GameLevel.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/GameLevel.kt new file mode 100644 index 0000000..15b12d2 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/GameLevel.kt @@ -0,0 +1,20 @@ +package ru.ifmo.sd.world.representation + +import kotlin.math.abs + +class GameLevel(private val level: Array) { + val gameLevel: Array + get() = this.level + + fun isAvailable(oldPos: Position, newPos: Position): Boolean { + return isAdjacent(oldPos, newPos) && insideMaze(newPos) && level[newPos.row][newPos.column] == 0 + } + + private fun insideMaze(newPos: Position): Boolean { + return newPos.row < level.size && newPos.column < level[0].size + } + + private fun isAdjacent(oldPos: Position, newPos: Position): Boolean { + return abs(oldPos.row - newPos.row) <= 1 && abs(oldPos.column - newPos.column) <= 1 + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt new file mode 100644 index 0000000..ff9779d --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt @@ -0,0 +1,6 @@ +package ru.ifmo.sd.world.representation + +import kotlinx.serialization.Serializable + +@Serializable +data class Position(val row: Int, val column: Int) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsHealthStorage.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsHealthStorage.kt new file mode 100644 index 0000000..39defc9 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsHealthStorage.kt @@ -0,0 +1,25 @@ +package ru.ifmo.sd.world.representation + +import ru.ifmo.sd.world.representation.units.GameUnit + +class UnitsHealthStorage { + private val healths: MutableMap = HashMap() + + fun getHealths() = this.healths + + fun addUnit(unit: GameUnit, value: Int = 100) { + healths[unit] = value + } + + fun increase(gameUnit: GameUnit, value: Int) { + healths.merge(gameUnit, value, Int::plus) + } + + fun decrease(gameUnit: GameUnit, value: Int) { + healths.merge(gameUnit, value, Int::minus) + } + + fun isAlive(gameUnit: GameUnit): Boolean { + return healths.getOrDefault(gameUnit, 0) > 0 + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt new file mode 100644 index 0000000..c316da0 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt @@ -0,0 +1,17 @@ +package ru.ifmo.sd.world.representation + +import ru.ifmo.sd.world.representation.units.GameUnit + +class UnitsPositionStorage { + private val positions: MutableMap = HashMap() + + fun getPositions() = this.positions + + fun move(targetGameUnit: GameUnit, newPos: Position) { + positions[targetGameUnit] = newPos + } + + fun eliminateUnit(targetGameUnit: GameUnit) { + positions.remove(targetGameUnit) + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt new file mode 100644 index 0000000..3a05b26 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt @@ -0,0 +1,6 @@ +package ru.ifmo.sd.world.representation.units + +import kotlinx.serialization.Serializable + +@Serializable +open class GameUnit(val id: Int) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt new file mode 100644 index 0000000..2896dc5 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt @@ -0,0 +1,3 @@ +package ru.ifmo.sd.world.representation.units + +class Player(id: Int): GameUnit(id) diff --git a/game-server/src/main/resources/application.conf b/game-server/src/main/resources/application.conf new file mode 100644 index 0000000..b661627 --- /dev/null +++ b/game-server/src/main/resources/application.conf @@ -0,0 +1,9 @@ +ktor { + deployment { + port = 8080 + port = ${?PORT} + } + application { + modules = [ ru.ifmo.sd.httpapi.ApplicationKt.module ] + } +} diff --git a/game-server/src/main/resources/logback.xml b/game-server/src/main/resources/logback.xml new file mode 100644 index 0000000..bdbb64e --- /dev/null +++ b/game-server/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 25ad449..7f3ab9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ -rootProject.name = "roguelike" \ No newline at end of file +rootProject.name = "roguelike" +include("game-client") +include("game-server") diff --git a/src/main/kotlin/ru/ifmo/sd/Main.kt b/src/main/kotlin/ru/ifmo/sd/Main.kt deleted file mode 100644 index 18da3f5..0000000 --- a/src/main/kotlin/ru/ifmo/sd/Main.kt +++ /dev/null @@ -1,164 +0,0 @@ -package ru.ifmo.sd - -import ru.ifmo.sd.stuff.* -import java.awt.BorderLayout -import java.awt.EventQueue -import java.awt.event.KeyEvent -import javax.swing.* -import javax.swing.text.* - - -class GUI(title: String) : JFrame() { - val mainPanel = JPanel() - val headerPanel = JPanel() - val mapPanel = JPanel() - val infoPanel = JPanel() - var map = SymbolMap(mapPreviewSymbols) - - init { - createUI(title) - } - - private fun createUI(title: String) { - setTitle(title) - -// createMenuBar() - createLayout() - - defaultCloseOperation = EXIT_ON_CLOSE - setSize(800, 600) - isResizable = false - setLocationRelativeTo(null) - } - - private fun createMenuBar() { - val menubar = JMenuBar() - val icon = ImageIcon("src/main/resources/exit.png") - - val file = JMenu("File") - file.mnemonic = KeyEvent.VK_F - - val eMenuItem = JMenuItem("Exit", icon) - eMenuItem.mnemonic = KeyEvent.VK_E - eMenuItem.toolTipText = "Exit application" - eMenuItem.addActionListener { System.exit(0) } - - file.add(eMenuItem) - menubar.add(file) - - jMenuBar = menubar - } - - private fun createLayout() { - add(mainPanel) - mainPanel.layout = BorderLayout() - - headerPanel.setSize(800, 50) - mainPanel.add(headerPanel, BorderLayout.NORTH) - headerPanel.add(JButton("hey")) - - mapPanel.setSize(550, 550) - mainPanel.add(mapPanel, BorderLayout.WEST) - mapPanel.add(MapTextPane) - createMap() - - infoPanel.setSize(250, 550) - mainPanel.add(infoPanel, BorderLayout.EAST) - infoPanel.add(JButton("Okaey"), BorderLayout.WEST) - } - - private fun createMap() { - for (i in 0 until SymbolMap.rowSize) { - if (i != 0) { - insertText("\n", MapTextPane.colorMap[MapSymbolColor.BLACK]!!) - } - for (j in 0 until SymbolMap.columnSize) { - val symbol = map.rows[i][j] - val style = MapTextPane.colorMap[symbol.color]!! - insertText(symbol.content.toString(), style) - } - } - } - - private fun insertText(string: String, style: Style) { - try { - val doc = MapTextPane.document - doc.insertString(doc.length, string, style) - } catch (e: Exception) { - e.printStackTrace() // TODO - } - } -} - -private fun createAndShowGUI() { - val frame = GUI("Roguelike") - frame.isVisible = true -} - -fun main() { - EventQueue.invokeLater(::createAndShowGUI) -} - - - - - - - - - - - - - - - - - - - - - - - - - -private val mapPreviewText = listOf( - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", - "qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09qwert 09", -) - -val mapPreviewSymbols: List> = - mapPreviewText.map { s -> - s.map { c -> Symbol(c, MapSymbolColor.BLACK) } - } \ No newline at end of file diff --git a/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt b/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt deleted file mode 100644 index 8ee5813..0000000 --- a/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ru.ifmo.sd.stuff - -import java.awt.Color -import javax.swing.JTextPane -import javax.swing.text.Style -import javax.swing.text.StyleConstants - -object MapTextPane : JTextPane() { - private val FONT_FAMILY = "Monospaced" - private val FONT_SIZE = 12 - val colorMap: MutableMap = mutableMapOf() - - init { - isEditable = false - for (color in MapSymbolColor.values()) { - val newStyle = addStyle(color.toString(), null) - StyleConstants.setFontFamily(newStyle, FONT_FAMILY) - StyleConstants.setFontSize(newStyle, FONT_SIZE) - colorMap[color] = newStyle - } - StyleConstants.setForeground(colorMap[MapSymbolColor.BLACK], Color.BLACK) - StyleConstants.setForeground(colorMap[MapSymbolColor.GREEN], Color.GREEN) - StyleConstants.setForeground(colorMap[MapSymbolColor.RED], Color.RED) - StyleConstants.setForeground(colorMap[MapSymbolColor.YELLOW], Color.YELLOW) - StyleConstants.setForeground(colorMap[MapSymbolColor.MAGENTA], Color.MAGENTA) - StyleConstants.setForeground(colorMap[MapSymbolColor.ORANGE], Color.ORANGE) - StyleConstants.setForeground(colorMap[MapSymbolColor.BLUE], Color.BLUE) - } -} \ No newline at end of file diff --git a/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt b/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt deleted file mode 100644 index 12eca35..0000000 --- a/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ru.ifmo.sd.stuff - -enum class MapSymbolColor { - BLACK, GREEN, YELLOW, RED, BLUE, MAGENTA, ORANGE -} - -data class Symbol(val content: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) - -data class SymbolMap(val rows: List> = List(rowSize) { List(columnSize) { Symbol(' ') } } ) { - companion object { - const val rowSize = 32 - const val columnSize = 64 - } -} \ No newline at end of file