From 52aedd561a80e438e4d1bd99b0c72f2edab247eb Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 12:32:40 +0300 Subject: [PATCH 01/30] split into two modules --- .gitignore | 223 ------------------ build.gradle.kts | 35 +-- game-client/build.gradle.kts | 19 ++ .../src}/main/kotlin/ru/ifmo/sd/Main.kt | 0 .../kotlin/ru/ifmo/sd/stuff/MapTextPane.kt | 0 .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 0 game-server/build.gradle.kts | 39 +++ settings.gradle.kts | 4 +- 8 files changed, 67 insertions(+), 253 deletions(-) create mode 100644 game-client/build.gradle.kts rename {src => game-client/src}/main/kotlin/ru/ifmo/sd/Main.kt (100%) rename {src => game-client/src}/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt (100%) rename {src => game-client/src}/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt (100%) create mode 100644 game-server/build.gradle.kts 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..4984fce --- /dev/null +++ b/game-client/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + java + kotlin("jvm") version "1.4.21" +} + +group = "ru.ifmo.jb" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt similarity index 100% rename from src/main/kotlin/ru/ifmo/sd/Main.kt rename to game-client/src/main/kotlin/ru/ifmo/sd/Main.kt diff --git a/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt similarity index 100% rename from src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt rename to game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt diff --git a/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt similarity index 100% rename from src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt rename to game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt diff --git a/game-server/build.gradle.kts b/game-server/build.gradle.kts new file mode 100644 index 0000000..db86089 --- /dev/null +++ b/game-server/build.gradle.kts @@ -0,0 +1,39 @@ +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("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 { + +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "1.8" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" + } +} + +tasks.test { + useJUnitPlatform() +} 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") From 873430ee67592893ec16b3d8f0a5cdccd75f8e3a Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 12:34:01 +0300 Subject: [PATCH 02/30] add core functionality --- .../kotlin/ru/ifmo/sd/httpapi/Application.kt | 15 +++++++++ .../sd/httpapi/models/LevelConfiguration.kt | 6 ++++ .../ifmo/sd/httpapi/models/MoveEventData.kt | 9 ++++++ .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 32 +++++++++++++++++++ .../world/configuration/GameConfiguration.kt | 9 ++++++ .../world/configuration/GameConfigurator.kt | 32 +++++++++++++++++++ .../ru/ifmo/sd/world/events/EventsHandler.kt | 19 +++++++++++ .../sd/world/generation/LevelGenerator.kt | 11 +++++++ .../ifmo/sd/world/representation/GameLevel.kt | 20 ++++++++++++ .../ifmo/sd/world/representation/Position.kt | 3 ++ .../representation/UnitsHealthStorage.kt | 23 +++++++++++++ .../representation/UnitsPositionStorage.kt | 15 +++++++++ .../sd/world/representation/units/GameUnit.kt | 5 +++ .../sd/world/representation/units/Player.kt | 6 ++++ 14 files changed, 205 insertions(+) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/Application.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/LevelConfiguration.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/routes/GameRoutes.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/generation/LevelGenerator.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/GameLevel.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsHealthStorage.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt 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..2d45006 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt @@ -0,0 +1,9 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Contextual +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: @Contextual GameUnit, val newPos: @Contextual 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..b968d10 --- /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 { + val levelConfiguration = call.receive() + val startedGame = EventsHandler.startGame(levelConfiguration) + call.respond(message = startedGame,status = HttpStatusCode.Created) + } + + post { + 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..e35406c --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt @@ -0,0 +1,9 @@ +package ru.ifmo.sd.world.configuration + +import ru.ifmo.sd.world.representation.* + +data class GameConfiguration( + val level: GameLevel, + val unitsPositions: UnitsPositionStorage, + val unitsHealthStorage: UnitsHealthStorage +) 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..b22df10 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt @@ -0,0 +1,32 @@ +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 player = Player(1) + unitsPositions.move(player, Position(0, 0)) + unitsHealths.addUnit(player) + return GameConfiguration(gameLevel, unitsPositions, unitsHealths) + } +} 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..1d95933 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/generation/LevelGenerator.kt @@ -0,0 +1,11 @@ +package ru.ifmo.sd.world.generation + +class LevelGenerator { + companion object { + private lateinit var level: Array + + fun generateLevel(length: Int, width: Int): Array { + return Array(length) { IntArray(width){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..c758189 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt @@ -0,0 +1,3 @@ +package ru.ifmo.sd.world.representation + +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..2c63012 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsHealthStorage.kt @@ -0,0 +1,23 @@ +package ru.ifmo.sd.world.representation + +import ru.ifmo.sd.world.representation.units.GameUnit + +class UnitsHealthStorage { + private val healths: MutableMap = HashMap() + + 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..7c85d66 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt @@ -0,0 +1,15 @@ +package ru.ifmo.sd.world.representation + +import ru.ifmo.sd.world.representation.units.GameUnit + +class UnitsPositionStorage { + private val positionStorage: MutableMap = HashMap() + + fun move(targetGameUnit: GameUnit, newPos: Position) { + positionStorage[targetGameUnit] = newPos + } + + fun eliminateUnit(targetGameUnit: GameUnit) { + positionStorage.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..51280af --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt @@ -0,0 +1,5 @@ +package ru.ifmo.sd.world.representation.units + +abstract class GameUnit { + abstract 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..0ee8024 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt @@ -0,0 +1,6 @@ +package ru.ifmo.sd.world.representation.units + +class Player(private val identifier: Int) : GameUnit() { + override val id: Int + get() = this.identifier +} From cc280a77520d4c0b2a94304f1caeaff6321ebf50 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 13:37:50 +0300 Subject: [PATCH 03/30] add application configuration file --- game-server/build.gradle.kts | 4 +++- game-server/src/main/resources/application.conf | 9 +++++++++ game-server/src/main/resources/logback.xml | 12 ++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 game-server/src/main/resources/application.conf create mode 100644 game-server/src/main/resources/logback.xml diff --git a/game-server/build.gradle.kts b/game-server/build.gradle.kts index db86089..e7e646a 100644 --- a/game-server/build.gradle.kts +++ b/game-server/build.gradle.kts @@ -15,6 +15,8 @@ repositories { 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") @@ -22,7 +24,7 @@ dependencies { } application { - + mainClassName = "ru.ifmo.sd.httpapi.ApplicationKt" } tasks { 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 + + + + + + + + From 0b87f1f554e77aa93a50a3131f1dba86a9ee464f Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 13:38:06 +0300 Subject: [PATCH 04/30] fix problems with serialization --- .../kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt | 3 +-- .../kotlin/ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 6 +++--- .../ifmo/sd/world/configuration/GameConfiguration.kt | 10 +++++++--- .../ru/ifmo/sd/world/configuration/GameConfigurator.kt | 4 +++- .../kotlin/ru/ifmo/sd/world/representation/Position.kt | 3 +++ .../ifmo/sd/world/representation/UnitsHealthStorage.kt | 2 ++ .../sd/world/representation/UnitsPositionStorage.kt | 8 +++++--- .../ru/ifmo/sd/world/representation/units/GameUnit.kt | 7 ++++--- .../ru/ifmo/sd/world/representation/units/Player.kt | 5 +---- 9 files changed, 29 insertions(+), 19 deletions(-) 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 index 2d45006..d7866c4 100644 --- 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 @@ -1,9 +1,8 @@ package ru.ifmo.sd.httpapi.models -import kotlinx.serialization.Contextual 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: @Contextual GameUnit, val newPos: @Contextual Position) +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 index b968d10..6d27b51 100644 --- 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 @@ -11,13 +11,13 @@ import ru.ifmo.sd.world.events.EventsHandler fun Route.gameRouting() { route("/") { - post { + post("/start") { val levelConfiguration = call.receive() val startedGame = EventsHandler.startGame(levelConfiguration) - call.respond(message = startedGame,status = HttpStatusCode.Created) + call.respond(message = startedGame, status = HttpStatusCode.Created) } - post { + post("/move") { val moveEventData = call.receive() EventsHandler.move(moveEventData.targetUnit, moveEventData.newPos) call.respondText("Position successfully updated", status = HttpStatusCode.Accepted) 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 index e35406c..fadd18c 100644 --- 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 @@ -1,9 +1,13 @@ package ru.ifmo.sd.world.configuration +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable import ru.ifmo.sd.world.representation.* +import ru.ifmo.sd.world.representation.units.GameUnit +@Serializable data class GameConfiguration( - val level: GameLevel, - val unitsPositions: UnitsPositionStorage, - val unitsHealthStorage: UnitsHealthStorage + val level: Array, + val unitsPositions: Map<@Contextual GameUnit, @Contextual Position>, + val unitsHealthStorage: Map<@Contextual GameUnit, Int> ) 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 index b22df10..db82f0e 100644 --- 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 @@ -25,8 +25,10 @@ object GameConfigurator { gameLevel = GameLevel(LevelGenerator.generateLevel( levelLength, levelWidth)) val player = Player(1) + unitsPositions = UnitsPositionStorage() + unitsHealths = UnitsHealthStorage() unitsPositions.move(player, Position(0, 0)) unitsHealths.addUnit(player) - return GameConfiguration(gameLevel, unitsPositions, unitsHealths) + return GameConfiguration(gameLevel.gameLevel, unitsPositions.getPositions(), unitsHealths.getHealths()) } } 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 index c758189..ff9779d 100644 --- 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 @@ -1,3 +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 index 2c63012..39defc9 100644 --- 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 @@ -5,6 +5,8 @@ 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 } 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 index 7c85d66..c316da0 100644 --- 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 @@ -3,13 +3,15 @@ package ru.ifmo.sd.world.representation import ru.ifmo.sd.world.representation.units.GameUnit class UnitsPositionStorage { - private val positionStorage: MutableMap = HashMap() + private val positions: MutableMap = HashMap() + + fun getPositions() = this.positions fun move(targetGameUnit: GameUnit, newPos: Position) { - positionStorage[targetGameUnit] = newPos + positions[targetGameUnit] = newPos } fun eliminateUnit(targetGameUnit: GameUnit) { - positionStorage.remove(targetGameUnit) + 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 index 51280af..3a05b26 100644 --- 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 @@ -1,5 +1,6 @@ package ru.ifmo.sd.world.representation.units -abstract class GameUnit { - abstract val id: Int -} +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 index 0ee8024..2896dc5 100644 --- 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 @@ -1,6 +1,3 @@ package ru.ifmo.sd.world.representation.units -class Player(private val identifier: Int) : GameUnit() { - override val id: Int - get() = this.identifier -} +class Player(id: Int): GameUnit(id) From 7ce9fc02e025bf7c09a0f21e6900fbc4eedd4f7b Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 17:27:04 +0300 Subject: [PATCH 05/30] add level generation --- .../world/configuration/GameConfigurator.kt | 5 +- .../sd/world/generation/LevelGenerator.kt | 55 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) 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 index db82f0e..d9aab6d 100644 --- 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 @@ -24,10 +24,11 @@ object GameConfigurator { fun configure(levelLength: Int, levelWidth: Int): GameConfiguration { gameLevel = GameLevel(LevelGenerator.generateLevel( levelLength, levelWidth)) - val player = Player(1) + val playerId = 1 + val player = Player(playerId) unitsPositions = UnitsPositionStorage() unitsHealths = UnitsHealthStorage() - unitsPositions.move(player, Position(0, 0)) + unitsPositions.move(player, Position(1, 1)) unitsHealths.addUnit(player) return GameConfiguration(gameLevel.gameLevel, unitsPositions.getPositions(), unitsHealths.getHealths()) } 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 index 1d95933..a547142 100644 --- 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 @@ -1,11 +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 { - return Array(length) { IntArray(width){1} } + 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]) + } } } } From 2aba3f45b242260894132352c20b536ddda42b65 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Sun, 25 Apr 2021 17:30:24 +0300 Subject: [PATCH 06/30] extend configuration data with player position info --- .../sd/world/configuration/GameConfiguration.kt | 6 +++--- .../sd/world/configuration/GameConfigurator.kt | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) 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 index fadd18c..3f88854 100644 --- 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 @@ -1,13 +1,13 @@ package ru.ifmo.sd.world.configuration -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable 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<@Contextual GameUnit, @Contextual Position>, - val unitsHealthStorage: Map<@Contextual GameUnit, Int> + val unitsPositions: Map, + val unitsHealthStorage: Map ) 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 index d9aab6d..1c9db5f 100644 --- 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 @@ -22,14 +22,23 @@ object GameConfigurator { } fun configure(levelLength: Int, levelWidth: Int): GameConfiguration { - gameLevel = GameLevel(LevelGenerator.generateLevel( - levelLength, levelWidth)) + 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, Position(1, 1)) + unitsPositions.move(player, playerPos) unitsHealths.addUnit(player) - return GameConfiguration(gameLevel.gameLevel, unitsPositions.getPositions(), unitsHealths.getHealths()) + return GameConfiguration( + playerPos, + gameLevel.gameLevel, + unitsPositions.getPositions(), + unitsHealths.getHealths() + ) } } From 37bf9f85b13e777e8901fe097cb0da59101cd1f3 Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Mon, 26 Apr 2021 09:57:12 +0300 Subject: [PATCH 07/30] client movements --- game-client/build.gradle.kts | 6 + .../src/main/kotlin/ru/ifmo/sd/Main.kt | 178 ++-------- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 331 ++++++++++++++++++ .../kotlin/ru/ifmo/sd/stuff/MapTextPane.kt | 6 +- .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 26 +- .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 2 +- .../world/configuration/GameConfiguration.kt | 29 +- 7 files changed, 418 insertions(+), 160 deletions(-) create mode 100644 game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt diff --git a/game-client/build.gradle.kts b/game-client/build.gradle.kts index 4984fce..f693aa4 100644 --- a/game-client/build.gradle.kts +++ b/game-client/build.gradle.kts @@ -5,6 +5,7 @@ plugins { group = "ru.ifmo.jb" version = "1.0-SNAPSHOT" +val ktor_version = "1.5.2" repositories { mavenCentral() @@ -12,6 +13,11 @@ repositories { dependencies { implementation(kotlin("stdlib")) + 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")) } tasks.test { diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index 18da3f5..3caf8eb 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -1,164 +1,40 @@ package ru.ifmo.sd -import ru.ifmo.sd.stuff.* -import java.awt.BorderLayout +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.stuff.GUI +import ru.ifmo.sd.world.configuration.GameConfiguration +import ru.ifmo.sd.world.configuration.GameConfigurationSerializable +import ru.ifmo.sd.world.representation.units.GameUnit 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) +suspend fun main() { + val client = HttpClient(CIO) { + install(JsonFeature) { + serializer = KotlinxSerializer() + } } - - 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 + val response = client.post("http://localhost:8080/start") { + contentType(ContentType.Application.Json) + body = LevelConfiguration(levelLength = 15, levelWidth = 30) } - private fun createLayout() { - add(mainPanel) - mainPanel.layout = BorderLayout() - - headerPanel.setSize(800, 50) - mainPanel.add(headerPanel, BorderLayout.NORTH) - headerPanel.add(JButton("hey")) + println(response.deserializeBack()) - mapPanel.setSize(550, 550) - mainPanel.add(mapPanel, BorderLayout.WEST) - mapPanel.add(MapTextPane) - createMap() + EventQueue.invokeLater { createAndShowGUI(response.deserializeBack()) } - 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 - } - } +// client.close() } -private fun createAndShowGUI() { - val frame = GUI("Roguelike") +private fun createAndShowGUI(config: GameConfiguration) { + val frame = GUI("Roguelike", config) 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 +} \ 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..14a949b --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -0,0 +1,331 @@ +package ru.ifmo.sd.stuff + +import ru.ifmo.sd.stuff.SymbolMap.Symbol.* +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.event.KeyEvent +import java.awt.event.KeyListener +import javax.swing.* +import javax.swing.text.Style + +enum class MoveEvent { + UP, DOWN, LEFT, RIGHT, NONE +} + +class GUI : JFrame, KeyListener { + constructor(title: String, gameConfiguration: GameConfiguration) : super() { + this.mainPanel = JPanel() + this.headerPanel = JPanel() + this.mapPanel = JPanel() + this.infoPanel = JPanel() + this.map = SymbolMap(gameConfiguration.level) + this.mapTextPane = MapTextPane() + this.currPos = gameConfiguration.playerPos +// this.keyListener = MyKeyListener() + createUI(title) + } + +// private enum class State { +// STARTING, INPUT_WAITING, PENDING_REQUEST +// } + + + private var currPos: Position + private val mainPanel: JPanel + private val headerPanel: JPanel + private val mapPanel: JPanel + private val infoPanel: JPanel + private var map: SymbolMap + private var mapTextPane: MapTextPane +// private var keyListener: MyKeyListener + +// private var lock = Object() +// private var state = State.STARTING + + private fun createUI(title: String) { + setTitle(title) + +// createMenuBar() + createLayout() + makeNotFocusable() + + defaultCloseOperation = EXIT_ON_CLOSE + setSize(800, 600) + isResizable = false + setLocationRelativeTo(null) + + // TODO should work regardless of component being focused + isFocusable = true + focusTraversalKeysEnabled = false + this.addKeyListener(this) + + requestFocus() + requestFocusInWindow() + +// state = State.INPUT_WAITING + start() + } + + private fun start() { + Thread { + while (true) { +// if (state === GAME_OVERED) break +// if (state === GAME_PUSHED) { +// synchronized(lock) { +// try { +// lock.wait() +// } catch (e: InterruptedException) { +// break +// } +// } +// } + + try { + Thread.sleep(30) + } catch (e: InterruptedException) { + break + } +// when (state) { +// GAME_BARRIER_SELECT -> bselector.draw(bufferScreen) +// GAME_MAP_SELECT -> { +// } +// GAME_RUNING -> runningDraw() +// GAME_FAILED, GAME_SUCCEED -> if (gresult != null) gresult.draw(bufferScreen) +// } + + mainPanel.repaint() + } + }.start() + } + + + 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) +// val infoLabel = JLabel("loading...") +// infoLabel.isFocusable = false +// headerPanel.add(infoLabel) + + mapPanel.setSize(550, 550) + mainPanel.add(mapPanel, BorderLayout.WEST) + mapPanel.add(mapTextPane) + createMap() + + infoPanel.setSize(250, 550) + mainPanel.add(infoPanel, BorderLayout.EAST) + +// val hpLabel = JLabel("HP: ") +// hpLabel.isFocusable = false +// infoPanel.add(hpLabel, BorderLayout.WEST) + } + + private fun makeNotFocusable() { + mainPanel.isFocusable = false + headerPanel.isFocusable = false + mapPanel.isFocusable = false + infoPanel.isFocusable = false + mapTextPane.isFocusable = false + } + + private fun createMap() { + for (i in 0 until map.rowSize) { + if (i != 0) { + insertText("\n", mapTextPane.colorMap[MapSymbolColor.BLACK]!!) + } + for (j in 0 until map.columnSize) { + if (i == currPos.row && j == currPos.column) { + val style = mapTextPane.colorMap[MapSymbolColor.RED]!! + insertText(PLAYER.symbol.toString(), style) + } else { + 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 + } + } + + ///////////////////////////////////// + + var moved = MoveEvent.NONE + var didMove = false + + 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) { + if (e == null) { + moved = MoveEvent.NONE + return + } + + // TODO: rewrite to "when (e.keyCode)" + // note: may be not working when several keys pressed at the same time + if (e.keyCode == KeyEvent.VK_UP || e.keyCode == KeyEvent.VK_W) { + if (currPos.row > 0) { + val currRow = currPos.row - 1 + val currColumn = currPos.column + val currSymb = map.rows[currRow][currColumn] + if (currSymb.content == NONE.symbol) { + didMove = true + moved = MoveEvent.UP + map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) + map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) + replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) + replaceSymbolAt(currPos.row - 1, currPos.column, PLAYER.symbol) + currPos = Position(currPos.row - 1, currPos.column) + } + } + } + if (e.keyCode == KeyEvent.VK_DOWN || e.keyCode == KeyEvent.VK_S) { + val currRow = currPos.row + 1 + val currColumn = currPos.column + val currSymb = map.rows[currRow][currColumn] + if (currPos.row < map.rowSize - 1 && currSymb.content == NONE.symbol) { + didMove = true + moved = MoveEvent.DOWN + map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) + map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) + replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) + replaceSymbolAt(currPos.row + 1, currPos.column, PLAYER.symbol) + currPos = Position(currPos.row + 1, currPos.column) + } + } + if (e.keyCode == KeyEvent.VK_LEFT || e.keyCode == KeyEvent.VK_A) { + if (currPos.column > 0) { + val currRow = currPos.row + val currColumn = currPos.column - 1 + val currSymb = map.rows[currRow][currColumn] + if (currSymb.content == NONE.symbol) { + didMove = true + moved = MoveEvent.LEFT + map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) + map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) + replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) + replaceSymbolAt(currPos.row, currPos.column - 1, PLAYER.symbol) + currPos = Position(currPos.row, currPos.column - 1) + } + } + } + if (e.keyCode == KeyEvent.VK_RIGHT || e.keyCode == KeyEvent.VK_D) { + val currRow = currPos.row + val currColumn = currPos.column + 1 + val currSymb = map.rows[currRow][currColumn] + if (currPos.column < map.columnSize - 1 && currSymb.content == NONE.symbol) { + didMove = true + moved = MoveEvent.RIGHT + map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) + map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) + replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) + replaceSymbolAt(currPos.row, currPos.column + 1, PLAYER.symbol) + currPos = Position(currPos.row, currPos.column + 1) + } + } + + if (didMove) { + println("Should move=$moved") + + didMove = false + this.validate() + this.mapPanel.validate() + this.mapTextPane.validate() + this.repaint() + this.mapPanel.repaint() + this.mapTextPane.repaint() + } + } + } + + private fun replaceSymbolAt(row: Int, column: Int, symbol: Char) { + mapTextPane.isEditable = true + val index = row * (map.columnSize + 1) + column + mapTextPane.select(index, index + 1) + println("selected text='${mapTextPane.selectedText}'") + mapTextPane.replaceSelection(symbol.toString()) + if (symbol == PLAYER.symbol) { + mapTextPane.selectedTextColor = Color.RED + } else { + mapTextPane.selectedTextColor = Color.BLACK + } + mapTextPane.isEditable = false + } + + override fun keyReleased(e: KeyEvent?) {} + override fun keyTyped(e: KeyEvent?) {} +} + + +//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/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt index 8ee5813..76d47dc 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt @@ -1,13 +1,15 @@ package ru.ifmo.sd.stuff +import ru.ifmo.sd.world.representation.Position import java.awt.Color import javax.swing.JTextPane import javax.swing.text.Style import javax.swing.text.StyleConstants +import java.awt.Graphics; -object MapTextPane : JTextPane() { +class MapTextPane : JTextPane() { private val FONT_FAMILY = "Monospaced" - private val FONT_SIZE = 12 + private val FONT_SIZE = 14 val colorMap: MutableMap = mutableMapOf() init { 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 index 12eca35..cbede27 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -1,14 +1,30 @@ package ru.ifmo.sd.stuff +import ru.ifmo.sd.stuff.SymbolMap.Symbol.* + enum class MapSymbolColor { BLACK, GREEN, YELLOW, RED, BLUE, MAGENTA, ORANGE } -data class Symbol(val content: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) +data class ColoredSymbol(val content: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) + +class SymbolMap(level: Array) { + enum class Symbol(val symbol: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) { + WALL('#'), + PLAYER('@', MapSymbolColor.RED), + NONE(' '), + } -data class SymbolMap(val rows: List> = List(rowSize) { List(columnSize) { Symbol(' ') } } ) { - companion object { - const val rowSize = 32 - const val columnSize = 64 + val rows: List> = level.map { arr -> + arr.map { i -> + ColoredSymbol( + if (i == 0) NONE.symbol else WALL.symbol + ) + }.toMutableList() } + + val rowSize: Int + get() = rows.size + val columnSize: Int + get() = rows[0].size } \ No newline at end of file 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 index 6d27b51..c91d1fa 100644 --- 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 @@ -14,7 +14,7 @@ fun Route.gameRouting() { post("/start") { val levelConfiguration = call.receive() val startedGame = EventsHandler.startGame(levelConfiguration) - call.respond(message = startedGame, status = HttpStatusCode.Created) + call.respond(message = startedGame.makeSerializable(), status = HttpStatusCode.Created) } post("/move") { 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 index 3f88854..911d1a3 100644 --- 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 @@ -1,6 +1,8 @@ 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 @@ -10,4 +12,29 @@ data class GameConfiguration( 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) } + ) + } + +} From 07fddb9147c3b08a3e82a8acd9ba01ab1c9d265e Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Sat, 1 May 2021 17:29:04 +0300 Subject: [PATCH 08/30] client: map reloads when ended the level; bug fix, refactoring, polishing --- game-client/build.gradle.kts | 6 + .../src/main/kotlin/ru/ifmo/sd/Main.kt | 26 +- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 295 ++++++------------ .../kotlin/ru/ifmo/sd/stuff/MapTextPane.kt | 16 +- .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 30 +- 5 files changed, 146 insertions(+), 227 deletions(-) diff --git a/game-client/build.gradle.kts b/game-client/build.gradle.kts index f693aa4..2dfeba1 100644 --- a/game-client/build.gradle.kts +++ b/game-client/build.gradle.kts @@ -1,5 +1,6 @@ plugins { java + application kotlin("jvm") version "1.4.21" } @@ -13,6 +14,7 @@ repositories { dependencies { implementation(kotlin("stdlib")) + implementation("org.jetbrains.kotlin:kotlin-reflect:1.1.0") implementation("io.ktor:ktor-client-cio:$ktor_version") implementation("io.ktor:ktor-client-gson:$ktor_version") implementation("io.ktor:ktor-client-jackson:$ktor_version") @@ -20,6 +22,10 @@ dependencies { 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 index 3caf8eb..d424b4b 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -9,23 +9,25 @@ 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 +private var client: HttpClient? = null + suspend fun main() { - val client = HttpClient(CIO) { + client = HttpClient(CIO) { install(JsonFeature) { serializer = KotlinxSerializer() } } - val response = client.post("http://localhost:8080/start") { - contentType(ContentType.Application.Json) - body = LevelConfiguration(levelLength = 15, levelWidth = 30) - } + val response = makeNewGameConfiguration() println(response.deserializeBack()) @@ -34,6 +36,20 @@ suspend fun main() { // 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 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 index 14a949b..b429734 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -1,11 +1,16 @@ package ru.ifmo.sd.stuff -import ru.ifmo.sd.stuff.SymbolMap.Symbol.* +import kotlinx.coroutines.runBlocking +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.Color.* +import java.awt.Container import java.awt.event.KeyEvent +import java.awt.event.KeyEvent.* import java.awt.event.KeyListener import javax.swing.* import javax.swing.text.Style @@ -14,40 +19,23 @@ enum class MoveEvent { UP, DOWN, LEFT, RIGHT, NONE } -class GUI : JFrame, KeyListener { - constructor(title: String, gameConfiguration: GameConfiguration) : super() { - this.mainPanel = JPanel() - this.headerPanel = JPanel() - this.mapPanel = JPanel() - this.infoPanel = JPanel() - this.map = SymbolMap(gameConfiguration.level) - this.mapTextPane = MapTextPane() - this.currPos = gameConfiguration.playerPos -// this.keyListener = MyKeyListener() +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 enum class State { -// STARTING, INPUT_WAITING, PENDING_REQUEST -// } - - - private var currPos: Position - private val mainPanel: JPanel - private val headerPanel: JPanel - private val mapPanel: JPanel - private val infoPanel: JPanel - private var map: SymbolMap - private var mapTextPane: MapTextPane -// private var keyListener: MyKeyListener - -// private var lock = Object() -// private var state = State.STARTING - private fun createUI(title: String) { setTitle(title) -// createMenuBar() createLayout() makeNotFocusable() @@ -63,62 +51,23 @@ class GUI : JFrame, KeyListener { requestFocus() requestFocusInWindow() - -// state = State.INPUT_WAITING - start() } private fun start() { Thread { while (true) { -// if (state === GAME_OVERED) break -// if (state === GAME_PUSHED) { -// synchronized(lock) { -// try { -// lock.wait() -// } catch (e: InterruptedException) { -// break -// } -// } -// } try { Thread.sleep(30) } catch (e: InterruptedException) { break } -// when (state) { -// GAME_BARRIER_SELECT -> bselector.draw(bufferScreen) -// GAME_MAP_SELECT -> { -// } -// GAME_RUNING -> runningDraw() -// GAME_FAILED, GAME_SUCCEED -> if (gresult != null) gresult.draw(bufferScreen) -// } mainPanel.repaint() } }.start() } - - 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() @@ -132,7 +81,7 @@ class GUI : JFrame, KeyListener { mapPanel.setSize(550, 550) mainPanel.add(mapPanel, BorderLayout.WEST) mapPanel.add(mapTextPane) - createMap() + createMapTextPane() infoPanel.setSize(250, 550) mainPanel.add(infoPanel, BorderLayout.EAST) @@ -142,30 +91,34 @@ class GUI : JFrame, KeyListener { // infoPanel.add(hpLabel, BorderLayout.WEST) } - private fun makeNotFocusable() { - mainPanel.isFocusable = false - headerPanel.isFocusable = false - mapPanel.isFocusable = false - infoPanel.isFocusable = false - mapTextPane.isFocusable = false + private fun makeNotFocusable(container: Container = this) { + container.isFocusable = false + for (c in container.components) { + if (c is Container) makeNotFocusable(c) + } } - private fun createMap() { + private fun createMapTextPane() { + mapTextPane.document.remove(0, mapTextPane.document.length) for (i in 0 until map.rowSize) { if (i != 0) { - insertText("\n", mapTextPane.colorMap[MapSymbolColor.BLACK]!!) + insertText("\n", mapTextPane.colorMap[BLACK]!!) } for (j in 0 until map.columnSize) { - if (i == currPos.row && j == currPos.column) { - val style = mapTextPane.colorMap[MapSymbolColor.RED]!! - insertText(PLAYER.symbol.toString(), style) - } else { - val symbol = map.rows[i][j] - val style = mapTextPane.colorMap[symbol.color]!! - insertText(symbol.content.toString(), style) - } + 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) { @@ -182,6 +135,8 @@ class GUI : JFrame, KeyListener { 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") @@ -189,71 +144,41 @@ class GUI : JFrame, KeyListener { println("e == null") } if (!didMove) { - if (e == null) { - moved = MoveEvent.NONE - return - } - - // TODO: rewrite to "when (e.keyCode)" - // note: may be not working when several keys pressed at the same time - if (e.keyCode == KeyEvent.VK_UP || e.keyCode == KeyEvent.VK_W) { - if (currPos.row > 0) { - val currRow = currPos.row - 1 - val currColumn = currPos.column - val currSymb = map.rows[currRow][currColumn] - if (currSymb.content == NONE.symbol) { + 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 - map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) - map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) - replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) - replaceSymbolAt(currPos.row - 1, currPos.column, PLAYER.symbol) - currPos = Position(currPos.row - 1, currPos.column) + currPos = newPos } } - } - if (e.keyCode == KeyEvent.VK_DOWN || e.keyCode == KeyEvent.VK_S) { - val currRow = currPos.row + 1 - val currColumn = currPos.column - val currSymb = map.rows[currRow][currColumn] - if (currPos.row < map.rowSize - 1 && currSymb.content == NONE.symbol) { - didMove = true - moved = MoveEvent.DOWN - map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) - map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) - replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) - replaceSymbolAt(currPos.row + 1, currPos.column, PLAYER.symbol) - currPos = Position(currPos.row + 1, currPos.column) + VK_DOWN, VK_S -> { + val newPos = Position(currPos.row + 1, currPos.column) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.DOWN + currPos = newPos + } } - } - if (e.keyCode == KeyEvent.VK_LEFT || e.keyCode == KeyEvent.VK_A) { - if (currPos.column > 0) { - val currRow = currPos.row - val currColumn = currPos.column - 1 - val currSymb = map.rows[currRow][currColumn] - if (currSymb.content == NONE.symbol) { + VK_LEFT, VK_A -> { + val newPos = Position(currPos.row, currPos.column - 1) + if (replaceSymbol(currPos, newPos)) { didMove = true moved = MoveEvent.LEFT - map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) - map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) - replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) - replaceSymbolAt(currPos.row, currPos.column - 1, PLAYER.symbol) - currPos = Position(currPos.row, currPos.column - 1) + currPos = newPos } } - } - if (e.keyCode == KeyEvent.VK_RIGHT || e.keyCode == KeyEvent.VK_D) { - val currRow = currPos.row - val currColumn = currPos.column + 1 - val currSymb = map.rows[currRow][currColumn] - if (currPos.column < map.columnSize - 1 && currSymb.content == NONE.symbol) { - didMove = true - moved = MoveEvent.RIGHT - map.rows[currPos.row][currPos.column] = ColoredSymbol(NONE.symbol) - map.rows[currRow][currColumn] = ColoredSymbol(PLAYER.symbol, currSymb.color) - replaceSymbolAt(currPos.row, currPos.column, NONE.symbol) - replaceSymbolAt(currPos.row, currPos.column + 1, PLAYER.symbol) - currPos = Position(currPos.row, currPos.column + 1) + VK_RIGHT, VK_D -> { + val newPos = Position(currPos.row, currPos.column + 1) + if (replaceSymbol(currPos, newPos)) { + didMove = true + moved = MoveEvent.RIGHT + currPos = newPos + } } } @@ -267,65 +192,43 @@ class GUI : JFrame, KeyListener { 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 replaceSymbolAt(row: Int, column: Int, symbol: Char) { + 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 = row * (map.columnSize + 1) + column + val index = pos.row * (map.columnSize + 1) + pos.column mapTextPane.select(index, index + 1) - println("selected text='${mapTextPane.selectedText}'") - mapTextPane.replaceSelection(symbol.toString()) - if (symbol == PLAYER.symbol) { - mapTextPane.selectedTextColor = Color.RED - } else { - mapTextPane.selectedTextColor = Color.BLACK - } +// println("selected text='${mapTextPane.selectedText}'") + mapTextPane.selectionColor = symbol.color + mapTextPane.replaceSelection(symbol.char.toString()) mapTextPane.isEditable = false } - - override fun keyReleased(e: KeyEvent?) {} - override fun keyTyped(e: KeyEvent?) {} -} - - -//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 +} \ 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 index 76d47dc..288f4ad 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt @@ -2,6 +2,7 @@ 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 @@ -9,23 +10,18 @@ import java.awt.Graphics; class MapTextPane : JTextPane() { private val FONT_FAMILY = "Monospaced" - private val FONT_SIZE = 14 - val colorMap: MutableMap = mutableMapOf() + 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 MapSymbolColor.values()) { + 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) } - 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/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt index cbede27..72d0a48 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -1,26 +1,24 @@ package ru.ifmo.sd.stuff -import ru.ifmo.sd.stuff.SymbolMap.Symbol.* +import ru.ifmo.sd.stuff.ColoredSymbol.* +import ru.ifmo.sd.world.configuration.GameConfiguration +import java.awt.Color +import java.awt.Color.* -enum class MapSymbolColor { - BLACK, GREEN, YELLOW, RED, BLUE, MAGENTA, ORANGE -} -data class ColoredSymbol(val content: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) +enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { + WALL(0x2591.toChar(), GRAY), + PLAYER(0x267F.toChar()/*, RED*/), + NONE(' '), +} -class SymbolMap(level: Array) { - enum class Symbol(val symbol: Char, val color: MapSymbolColor = MapSymbolColor.BLACK) { - WALL('#'), - PLAYER('@', MapSymbolColor.RED), - NONE(' '), +class SymbolMap(config: GameConfiguration) { + val rows: List> = config.level.map { arr -> + arr.map { i -> if (i == 0) NONE else WALL }.toMutableList() } - val rows: List> = level.map { arr -> - arr.map { i -> - ColoredSymbol( - if (i == 0) NONE.symbol else WALL.symbol - ) - }.toMutableList() + init { + rows[config.playerPos.row][config.playerPos.column] = PLAYER } val rowSize: Int From c7dedc649279129bc09ac5a50a81d7db50a76398 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Mon, 3 May 2021 00:17:45 +0300 Subject: [PATCH 09/30] review fixes, add npc and combat interaction --- .../ru/ifmo/sd/httpapi/models/GameMove.kt | 15 ++++ .../ru/ifmo/sd/httpapi/models/JoinGameInfo.kt | 14 +++ .../sd/httpapi/models/LevelConfiguration.kt | 2 +- .../ifmo/sd/httpapi/models/MoveEventData.kt | 8 -- .../ru/ifmo/sd/httpapi/models/PlayerMove.kt | 6 ++ .../ru/ifmo/sd/httpapi/models/Position.kt | 28 ++++++ .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 12 +-- .../world/configuration/GameConfiguration.kt | 13 --- .../world/configuration/GameConfigurator.kt | 44 ---------- .../ru/ifmo/sd/world/events/EventsHandler.kt | 84 +++++++++++++++--- .../world/events/InteractionExecutorImpl.kt | 45 ++++++++++ .../ru/ifmo/sd/world/events/MazeEvent.kt | 22 +++++ .../sd/world/generation/LevelGenerator.kt | 88 +++++++++++++++++-- .../ru/ifmo/sd/world/npc/NpcEventProvider.kt | 27 ++++++ .../ifmo/sd/world/npc/strategy/Aggressive.kt | 49 +++++++++++ .../ru/ifmo/sd/world/npc/strategy/Coward.kt | 78 ++++++++++++++++ .../ru/ifmo/sd/world/npc/strategy/Passive.kt | 22 +++++ .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 28 ++++++ .../ifmo/sd/world/representation/GameLevel.kt | 23 ++--- .../ru/ifmo/sd/world/representation/Maze.kt | 19 ++++ .../ifmo/sd/world/representation/Position.kt | 6 -- .../representation/UnitsHealthStorage.kt | 24 ++--- .../representation/UnitsPositionStorage.kt | 17 ---- .../sd/world/representation/units/GameUnit.kt | 6 -- .../world/representation/units/MazeObject.kt | 10 +++ .../sd/world/representation/units/MazeUnit.kt | 26 ++++++ .../sd/world/representation/units/Player.kt | 3 - .../sd/world/representation/units/Wall.kt | 13 +++ 28 files changed, 580 insertions(+), 152 deletions(-) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameMove.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/Position.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameMove.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameMove.kt new file mode 100644 index 0000000..d3b8b2b --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameMove.kt @@ -0,0 +1,15 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class MazeEventData( + val position: Position, + val newMazeObj: Int +) + +@Serializable +data class GameMove( + val playerPosition: Position, + val events: List +) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt new file mode 100644 index 0000000..a66ad0e --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt @@ -0,0 +1,14 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class MazeData( + val levelMaze: Array> +) + +@Serializable +data class JoinGameInfo( + val playerPos: Position, + val maze: MazeData +) 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 index 4e8ddcf..3537bff 100644 --- 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 @@ -3,4 +3,4 @@ package ru.ifmo.sd.httpapi.models import kotlinx.serialization.Serializable @Serializable -data class LevelConfiguration(val levelLength: Int, val levelWidth: Int) +data class LevelConfiguration(val length: Int, val width: 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 deleted file mode 100644 index d7866c4..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/MoveEventData.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/models/PlayerMove.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt new file mode 100644 index 0000000..77a995a --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt @@ -0,0 +1,6 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class PlayerMove(val oldPosition: Position, val newPosition: Position) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/Position.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/Position.kt new file mode 100644 index 0000000..fab2ca2 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/Position.kt @@ -0,0 +1,28 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Position(val row: Int, val column: Int) { + operator fun plus(other: Position): Position { + return Position(row + other.row, column + other.column) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Position + + if (row != other.row) return false + if (column != other.column) return false + + return true + } + + override fun hashCode(): Int { + var result = row + result = 31 * result + column + return result + } +} 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 index 6d27b51..77c0954 100644 --- 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 @@ -6,21 +6,21 @@ 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.httpapi.models.PlayerMove import ru.ifmo.sd.world.events.EventsHandler fun Route.gameRouting() { route("/") { - post("/start") { + post("/join") { val levelConfiguration = call.receive() - val startedGame = EventsHandler.startGame(levelConfiguration) + val startedGame = EventsHandler.join(levelConfiguration) call.respond(message = startedGame, status = HttpStatusCode.Created) } post("/move") { - val moveEventData = call.receive() - EventsHandler.move(moveEventData.targetUnit, moveEventData.newPos) - call.respondText("Position successfully updated", status = HttpStatusCode.Accepted) + val playerMove = call.receive() + val move = EventsHandler.move(playerMove.oldPosition, playerMove.newPosition) + call.respond(message = move, status = HttpStatusCode.Accepted) } } } 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 deleted file mode 100644 index 3f88854..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.ifmo.sd.world.configuration - -import kotlinx.serialization.Serializable -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 -) 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 deleted file mode 100644 index 1c9db5f..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfigurator.kt +++ /dev/null @@ -1,44 +0,0 @@ -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 index ef7865f..98d1ae5 100644 --- 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 @@ -1,19 +1,79 @@ 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) +import ru.ifmo.sd.httpapi.models.* +import ru.ifmo.sd.world.generation.LevelGenerator +import ru.ifmo.sd.world.representation.GameLevel +import ru.ifmo.sd.world.representation.units.* + +object EventsHandler { + val interactionExecutor = InteractionExecutorImpl() + var gameLevel: GameLevel? = null + + fun join(levelConfiguration: LevelConfiguration): JoinGameInfo { + if (gameLevel == null) { + init(levelConfiguration.length, levelConfiguration.width) + } + val playerPos = gameLevel!!.maze.freePos.random() + gameLevel!!.unitsHealthStorage.addUnit(playerPos) + gameLevel!!.maze[playerPos] = Player() + + val length = gameLevel!!.maze.levelMaze.size + val width = gameLevel!!.maze.levelMaze[0].size + val mazeData: Array> = Array(length) { Array(width) { 0 } } + for (i in 0 until length) { + for (j in 0 until width) { + val mazeObj: MazeObject? = gameLevel!!.maze[Position(i, j)] + mazeData[i][j] = mazeObj?.getTypeIdentifier() ?: 0 + } } - fun move(targetUnit: GameUnit, newPos: Position) { - GameConfigurator.getUnitsPositions().move(targetUnit, newPos) + return JoinGameInfo(playerPos, MazeData(mazeData)) + } + + private fun init(length: Int, width: Int) { + gameLevel = LevelGenerator.generateLevel(length, width) + } + + fun move(playerPos: Position, targetPos: Position): GameMove { + val maze = gameLevel!!.maze + return if (maze[targetPos] != null) { + // ход игрока + maze[playerPos] = null + maze[targetPos] = Player() + + // результат хода игрового мира + val npcMove = gameLevel!!.npcEventProvider.move(targetPos) + npcMove.forEach { maze[it.position] = it.newMazeObj } + val newPlayerPos = + if (maze[targetPos] != null) targetPos + else Position(-1, -1) + + GameMove(newPlayerPos, npcMove.map { + MazeEventData( + it.position, + it.newMazeObj?.getTypeIdentifier() ?: 0 + ) + }) + } else { + // результат хода игрока + val playerMove = maze[targetPos]!!.interact(interactionExecutor, targetPos) + playerMove.forEach { maze[it.position] = it.newMazeObj } + + // результат хода игрового мира + val npcMove = gameLevel!!.npcEventProvider.move(playerPos) + npcMove.forEach { maze[it.position] = it.newMazeObj } + val newPlayerPos = + if (maze[playerPos] != null) playerPos + else Position(-1, -1) + + playerMove.forEach { npcMove.add(it) } + + GameMove(newPlayerPos, npcMove.map { + MazeEventData( + it.position, + it.newMazeObj?.getTypeIdentifier() ?: 0 + ) + }) } } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt new file mode 100644 index 0000000..3ba6cc0 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt @@ -0,0 +1,45 @@ +package ru.ifmo.sd.world.events + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.representation.units.* + + +interface InteractionExecutor { + fun doFor(obj: Wall, objPos: Position): MutableSet + fun doFor(obj: Enemy, objPos: Position): MutableSet + fun doFor(obj: Player, objPos: Position): MutableSet +} + +class InteractionExecutorImpl : InteractionExecutor { + companion object { + const val playerDamage = 25 + const val npcDamage = 10 + } + + override fun doFor(obj: Wall, objPos: Position): MutableSet { + return HashSet() + } + + override fun doFor(obj: Enemy, objPos: Position): MutableSet { + val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage + unitsHealthStorage.decrease(objPos, playerDamage) + return if (!unitsHealthStorage.isAlive(objPos)) { + unitsHealthStorage.eliminateUnit(objPos) + EventsHandler.gameLevel!!.npcEventProvider.eliminateNpc(objPos) + mutableSetOf(MazeEvent(objPos, null)) + } else { + HashSet() + } + } + + override fun doFor(obj: Player, objPos: Position): MutableSet { + val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage + unitsHealthStorage.decrease(objPos, npcDamage) + return if (!unitsHealthStorage.isAlive(objPos)) { + unitsHealthStorage.eliminateUnit(objPos) + mutableSetOf(MazeEvent(objPos, null)) + } else { + HashSet() + } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt new file mode 100644 index 0000000..233584b --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt @@ -0,0 +1,22 @@ +package ru.ifmo.sd.world.events + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.representation.units.MazeObject + + +data class MazeEvent(val position: Position, val newMazeObj: MazeObject?) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MazeEvent + + if (position != other.position) return false + + return true + } + + override fun hashCode(): Int { + return position.hashCode() + } +} 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 index a547142..cd3b206 100644 --- 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 @@ -1,38 +1,83 @@ package ru.ifmo.sd.world.generation +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.npc.NpcEventProvider +import ru.ifmo.sd.world.npc.strategy.* +import ru.ifmo.sd.world.representation.* +import ru.ifmo.sd.world.representation.units.Enemy import kotlin.random.Random +/** + * Класс, отвечающий за генерацию игрового лабиринта и + * размещения на нем противников. + */ class LevelGenerator { companion object { + private const val GAME_DIFFICULTY = 0.1 private lateinit var level: Array + private lateinit var freePos: MutableList - fun generateLevel(length: Int, width: Int): Array { + /** + * Метод для генерации случайного лабиринта. + * + * @param length -- длина лабиринта + * @param width -- ширина лабиринта + * @return двумерный массив соответствующий игровому лабиринту + */ + fun generateLevel(length: Int = 10, width: Int = 10): GameLevel { + /* + Шаг смещения позиции при создании лабиринта определим равным 2. + Создаем лабиринт с нечетными размерностями, чтобы поддерживать + инвариант нахождения в границах игрового поля. + */ 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 + // Стартовая позиция для генерации + val startPosRow = 1 + val startPosColumn = 1 + level[startPosRow][startPosColumn] = 0 - generateRec(playerPosRow, playerPosColumn) + freePos = ArrayList() + // Рекурсивно генерируем лабиринт + generateRec(startPosRow, startPosColumn) - return level + val maze = Maze(Array(actualLength) { + Array(actualWidth) { + null + } + }, HashSet()) + val unitsHealths = UnitsHealthStorage() + val npc = NpcEventProvider() + placeEnemies(maze, unitsHealths, npc) + return GameLevel(maze, unitsHealths, npc) } + // Идея в том, чтобы пробуривать соседние занятые клетки и избегать при этом циклов. 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 @@ -53,12 +98,39 @@ class LevelGenerator { 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 + // Пробуриваем две стенки в направлении движения + val firstPosToRemoveWall = Position( + (dirs[i][0] + curRow) / 2, + (dirs[i][1] + curColumn) / 2 + ) + val secondPosToRemoveWall = Position(dirs[i][0], dirs[i][1]) + level[firstPosToRemoveWall.row][firstPosToRemoveWall.column] = 0 + level[secondPosToRemoveWall.row][secondPosToRemoveWall.column] = 0 + + freePos.add(firstPosToRemoveWall) + freePos.add(secondPosToRemoveWall) + + // Рекурсивно запускаемся от позиции, в которой оказались после пробуривания стенок generateRec(dirs[i][0], dirs[i][1]) } } + + private fun placeEnemies(maze: Maze, healths: UnitsHealthStorage, npc: NpcEventProvider) { + val countOfEnemies = (maze.size() * GAME_DIFFICULTY).toInt() + freePos.forEach { maze.freePos.add(it) } + val enemiesPositions = freePos + .shuffled() + .take(countOfEnemies) + val strategies = listOf(Passive(), Aggressive(), Coward()) + enemiesPositions.forEach { + maze[it] = Enemy() + healths.addUnit(it, 100) + npc.addNpc(it, strategies.random()) + } + } } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt new file mode 100644 index 0000000..6702bfd --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt @@ -0,0 +1,27 @@ +package ru.ifmo.sd.world.npc + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.npc.strategy.Strategy + +class Npc(var position: Position, private val strategy: Strategy) { + fun move(playerPos: Position): MutableSet { + return strategy.execute(position, playerPos) + } +} + +class NpcEventProvider { + private val npc: MutableSet = HashSet() + + fun move(playerPos: Position): MutableSet { + return npc.random().move(playerPos) + } + + fun addNpc(pos: Position, strategy: Strategy) { + npc.add(Npc(pos, strategy)) + } + + fun eliminateNpc(pos: Position) { + npc.removeIf { it.position == pos } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt new file mode 100644 index 0000000..7707ed8 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt @@ -0,0 +1,49 @@ +package ru.ifmo.sd.world.npc.strategy + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.EventsHandler +import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.representation.units.Enemy +import kotlin.math.abs + +class Aggressive : Strategy { + private fun isNearby(npcPos: Position, playerPos: Position): Boolean { + return abs(npcPos.row - playerPos.row) == 1 && abs(npcPos.column - playerPos.column) == 0 + || abs(npcPos.row - playerPos.row) == 0 && abs(npcPos.column - playerPos.column) == 1 + } + + override fun execute(npcPos: Position, playerPos: Position): MutableSet { + return when (Strategy.findPlayer(npcPos, playerPos)) { + Strategy.Companion.PlayerDirection.North -> + if (isNearby(npcPos, playerPos)) { + EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + } else { + mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(-1, 0), Enemy())) + } + + Strategy.Companion.PlayerDirection.South -> + if (isNearby(npcPos, playerPos)) { + EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + } else { + mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(1, 0), Enemy())) + } + + Strategy.Companion.PlayerDirection.West -> + if (isNearby(npcPos, playerPos)) { + EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + } else { + mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(0, 1), Enemy())) + } + + Strategy.Companion.PlayerDirection.East -> + if (isNearby(npcPos, playerPos)) { + EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + } else { + mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(0, -1), Enemy())) + } + + Strategy.Companion.PlayerDirection.Failed -> + HashSet() + } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt new file mode 100644 index 0000000..a3ae3b5 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt @@ -0,0 +1,78 @@ +package ru.ifmo.sd.world.npc.strategy + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.representation.units.Enemy + + +class Coward : Strategy { + override fun execute(npcPos: Position, playerPos: Position): MutableSet { + return when (Strategy.findPlayer(npcPos, playerPos)) { + Strategy.Companion.PlayerDirection.North -> { + val positionsToMove = listOf( + Position(1, 0), + Position(0, 1), + Position(0, -1) + ) + val randomPos = Strategy.randomMove(positionsToMove) + if (randomPos != Position(0, 0)) { + mutableSetOf( + MazeEvent(npcPos, null), + MazeEvent(npcPos + randomPos, Enemy()) + ) + } else { + HashSet() + } + } + Strategy.Companion.PlayerDirection.South -> { + val positionsToMove = listOf( + Position(-1, 0), + Position(0, 1), + Position(0, -1) + ) + val randomPos = Strategy.randomMove(positionsToMove) + if (randomPos != Position(0, 0)) { + mutableSetOf( + MazeEvent(npcPos, null), + MazeEvent(npcPos + randomPos, Enemy()) + ) + } else { + HashSet() + } + } + Strategy.Companion.PlayerDirection.West -> { + val positionsToMove = listOf( + Position(1, 0), + Position(0, -1), + Position(-1, 0) + ) + val randomPos = Strategy.randomMove(positionsToMove) + if (randomPos != Position(0, 0)) { + mutableSetOf( + MazeEvent(npcPos, null), + MazeEvent(npcPos + randomPos, Enemy()) + ) + } else { + HashSet() + } + } + Strategy.Companion.PlayerDirection.East -> { + val positionsToMove = listOf( + Position(1, 0), + Position(0, 1), + Position(1, 0) + ) + val randomPos = Strategy.randomMove(positionsToMove) + if (randomPos != Position(0, 0)) { + mutableSetOf( + MazeEvent(npcPos, null), + MazeEvent(npcPos + randomPos, Enemy()) + ) + } else { + HashSet() + } + } + Strategy.Companion.PlayerDirection.Failed -> HashSet() + } + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt new file mode 100644 index 0000000..d043a4c --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt @@ -0,0 +1,22 @@ +package ru.ifmo.sd.world.npc.strategy + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.representation.units.Enemy + + +class Passive : Strategy { + override fun execute(npcPos: Position, playerPos: Position): MutableSet { + val positionsToMove = listOf( + Position(0, 1), Position(0, -1), + Position(1, 0), Position(-1, 0) + ) + val randomMove = Strategy.randomMove(positionsToMove) + return if (randomMove != Position(0, 0)) + mutableSetOf( + MazeEvent(npcPos, null), + MazeEvent(npcPos + randomMove, Enemy()) + ) + else HashSet() + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt new file mode 100644 index 0000000..c1ccc88 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -0,0 +1,28 @@ +package ru.ifmo.sd.world.npc.strategy + +import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.EventsHandler +import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.Failed + + +interface Strategy { + fun execute(npcPos: Position, playerPos: Position): MutableSet + + companion object { + enum class PlayerDirection { + North, South, West, East, Failed; + } + + // Bresenham's algorithm, see en.wikipedia.org/wiki/File:Bresenham.svg + fun findPlayer(npcPos: Position, playerPos: Position): PlayerDirection { + // TODO: implement Bresenham's algorithm + return Failed + } + + fun randomMove(positions: List): Position { + val availablePos = positions.filter { EventsHandler.gameLevel!!.maze[it] == null } + return if (availablePos.isNotEmpty()) availablePos.random() else Position(0, 0) + } + } +} 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 index 15b12d2..1030c2d 100644 --- 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 @@ -1,20 +1,9 @@ package ru.ifmo.sd.world.representation -import kotlin.math.abs +import ru.ifmo.sd.world.npc.NpcEventProvider -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 - } -} +data class GameLevel( + val maze: Maze, + val unitsHealthStorage: UnitsHealthStorage, + val npcEventProvider: NpcEventProvider +) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt new file mode 100644 index 0000000..4e193d2 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt @@ -0,0 +1,19 @@ +package ru.ifmo.sd.world.representation + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.representation.units.MazeObject + +data class Maze( + val levelMaze: Array>, + val freePos: MutableSet +) { + operator fun get(pos: Position): MazeObject? = levelMaze[pos.row][pos.column] + operator fun set(position: Position, mazeObject: MazeObject?) { + if (levelMaze[position.row][position.column] == null) { + freePos.remove(position) + } + levelMaze[position.row][position.column] = mazeObject + } + + fun size() = levelMaze.size * levelMaze[0].size +} 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 deleted file mode 100644 index ff9779d..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Position.kt +++ /dev/null @@ -1,6 +0,0 @@ -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 index 39defc9..ec44c30 100644 --- 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 @@ -1,25 +1,27 @@ package ru.ifmo.sd.world.representation -import ru.ifmo.sd.world.representation.units.GameUnit +import ru.ifmo.sd.httpapi.models.Position class UnitsHealthStorage { - private val healths: MutableMap = HashMap() + private val healths: MutableMap = HashMap() - fun getHealths() = this.healths + fun addUnit(pos: Position, value: Int = 100) { + healths[pos] = value + } - fun addUnit(unit: GameUnit, value: Int = 100) { - healths[unit] = value + fun eliminateUnit(pos: Position) { + healths.remove(pos) } - fun increase(gameUnit: GameUnit, value: Int) { - healths.merge(gameUnit, value, Int::plus) + fun increase(pos: Position, value: Int) { + healths.merge(pos, value, Int::plus) } - fun decrease(gameUnit: GameUnit, value: Int) { - healths.merge(gameUnit, value, Int::minus) + fun decrease(pos: Position, value: Int) { + healths.merge(pos, value, Int::minus) } - fun isAlive(gameUnit: GameUnit): Boolean { - return healths.getOrDefault(gameUnit, 0) > 0 + fun isAlive(pos: Position): Boolean { + return healths.getOrDefault(pos, 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 deleted file mode 100644 index c316da0..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/UnitsPositionStorage.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 3a05b26..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/GameUnit.kt +++ /dev/null @@ -1,6 +0,0 @@ -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/MazeObject.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt new file mode 100644 index 0000000..b11b04d --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt @@ -0,0 +1,10 @@ +package ru.ifmo.sd.world.representation.units + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.InteractionExecutor +import ru.ifmo.sd.world.events.MazeEvent + +interface MazeObject { + fun interact(executor: InteractionExecutor, pos: Position): MutableSet + fun getTypeIdentifier(): Int +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt new file mode 100644 index 0000000..31de81a --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt @@ -0,0 +1,26 @@ +package ru.ifmo.sd.world.representation.units + +import kotlinx.serialization.Serializable +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.InteractionExecutor +import ru.ifmo.sd.world.events.MazeEvent + +interface MazeUnit : MazeObject + +@Serializable +class Player : MazeUnit { + override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { + return executor.doFor(this, pos) + } + + override fun getTypeIdentifier() = 2 +} + +@Serializable +class Enemy : MazeUnit { + override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { + return executor.doFor(this, pos) + } + + override fun getTypeIdentifier() = 3 +} 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 deleted file mode 100644 index 2896dc5..0000000 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Player.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ru.ifmo.sd.world.representation.units - -class Player(id: Int): GameUnit(id) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt new file mode 100644 index 0000000..991a00a --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt @@ -0,0 +1,13 @@ +package ru.ifmo.sd.world.representation.units + +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.InteractionExecutor +import ru.ifmo.sd.world.events.MazeEvent + +class Wall : MazeObject { + override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { + return executor.doFor(this, pos) + } + + override fun getTypeIdentifier() = 1 +} From fb27b4e132b06d0bbcd288d6ac2b246c0dba994a Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Mon, 3 May 2021 00:45:37 +0300 Subject: [PATCH 10/30] client: small fixes --- game-client/build.gradle.kts | 2 +- game-client/src/main/kotlin/ru/ifmo/sd/Main.kt | 2 +- game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/game-client/build.gradle.kts b/game-client/build.gradle.kts index 2dfeba1..34c6d13 100644 --- a/game-client/build.gradle.kts +++ b/game-client/build.gradle.kts @@ -14,7 +14,7 @@ repositories { dependencies { implementation(kotlin("stdlib")) - implementation("org.jetbrains.kotlin:kotlin-reflect:1.1.0") + 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") diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index d424b4b..c05b6d9 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -19,7 +19,7 @@ import ru.ifmo.sd.world.representation.units.Player import java.awt.EventQueue -private var client: HttpClient? = null +internal var client: HttpClient? = null suspend fun main() { client = HttpClient(CIO) { 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 index b429734..27fcaae 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -1,6 +1,7 @@ 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.* @@ -12,6 +13,8 @@ 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 @@ -40,6 +43,12 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi 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) From d00cfdf29e57ea242c7c9efac69575fecda694a9 Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Mon, 3 May 2021 05:46:44 +0300 Subject: [PATCH 11/30] client: new api support --- .../src/main/kotlin/ru/ifmo/sd/Main.kt | 33 +++---- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 94 ++++++++++++------- .../kotlin/ru/ifmo/sd/stuff/MapTextPane.kt | 2 - .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 24 ++++- .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 2 +- 5 files changed, 91 insertions(+), 64 deletions(-) diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index c05b6d9..dfdf47f 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -1,21 +1,13 @@ 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.httpapi.models.* 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 @@ -27,30 +19,29 @@ suspend fun main() { serializer = KotlinxSerializer() } } - val response = makeNewGameConfiguration() + val response = apiJoin() - println(response.deserializeBack()) + println(response) - EventQueue.invokeLater { createAndShowGUI(response.deserializeBack()) } - -// client.close() + EventQueue.invokeLater { createAndShowGUI(response) } } -internal suspend fun makeNewGameConfiguration(): GameConfigurationSerializable { - return client!!.post("http://localhost:8080/start") { +internal suspend fun apiJoin(): JoinGameInfo { + return client!!.post("http://localhost:8080/join") { contentType(ContentType.Application.Json) - body = LevelConfiguration(levelLength = 3, levelWidth = 3) + body = LevelConfiguration(length = 3, width = 3) } } -internal suspend fun makeMove(newPos: Position, unit: GameUnit = Player(0)) { - client!!.post("http://localhost:8080/move") { +internal suspend fun apiMove(oldPos: Position, newPos: Position): GameMove { + println("apiMove oldPos=$oldPos, newPos=$newPos") + return client!!.post("http://localhost:8080/move") { contentType(ContentType.Application.Json) - body = MoveEventData(unit, newPos) + body = PlayerMove(oldPos, newPos) } } -private fun createAndShowGUI(config: GameConfiguration) { +private fun createAndShowGUI(config: JoinGameInfo) { 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 index 27fcaae..8dde7a2 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -2,11 +2,11 @@ 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.httpapi.models.JoinGameInfo +import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.apiMove +import ru.ifmo.sd.apiJoin 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 @@ -22,8 +22,13 @@ enum class MoveEvent { UP, DOWN, LEFT, RIGHT, NONE } -class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyListener { +class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListener { + private var prevPos: Position? = null // TODO: костыль private var currPos = gameConfiguration.playerPos + set(value) { + prevPos = currPos + field = value + } private val mainPanel = JPanel() private val headerPanel = JPanel() private val mapPanel = JPanel() @@ -90,7 +95,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi mapPanel.setSize(550, 550) mainPanel.add(mapPanel, BorderLayout.WEST) mapPanel.add(mapTextPane) - createMapTextPane() + reloadMapTextPane() infoPanel.setSize(250, 550) mainPanel.add(infoPanel, BorderLayout.EAST) @@ -107,7 +112,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi } } - private fun createMapTextPane() { + private fun reloadMapTextPane() { mapTextPane.document.remove(0, mapTextPane.document.length) for (i in 0 until map.rowSize) { if (i != 0) { @@ -159,7 +164,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi } VK_UP, VK_W -> { val newPos = Position(currPos.row - 1, currPos.column) - if (replaceSymbol(currPos, newPos)) { + if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.UP currPos = newPos @@ -167,7 +172,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi } VK_DOWN, VK_S -> { val newPos = Position(currPos.row + 1, currPos.column) - if (replaceSymbol(currPos, newPos)) { + if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.DOWN currPos = newPos @@ -175,7 +180,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi } VK_LEFT, VK_A -> { val newPos = Position(currPos.row, currPos.column - 1) - if (replaceSymbol(currPos, newPos)) { + if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.LEFT currPos = newPos @@ -183,7 +188,7 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi } VK_RIGHT, VK_D -> { val newPos = Position(currPos.row, currPos.column + 1) - if (replaceSymbol(currPos, newPos)) { + if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.RIGHT currPos = newPos @@ -204,40 +209,57 @@ class GUI(title: String, gameConfiguration: GameConfiguration) : JFrame(), KeyLi runBlocking { if (currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) { - val newConfig = makeNewGameConfiguration().deserializeBack() + // level ended + val newConfig = apiJoin() map = SymbolMap(newConfig) - createMapTextPane() + reloadMapTextPane() + val prevPosSaved = prevPos currPos = newConfig.playerPos + prevPos = prevPosSaved } else { - makeMove(currPos) + val gameMove = apiMove(prevPos!!, currPos) + if (gameMove.playerPosition == Position(-1, -1)) { + // player is dead + println("You are dead!") + } else { + // player moved + map.applyDiff(gameMove.events) + reloadMapTextPane() + val prevPosSaved = prevPos + currPos = gameMove.playerPosition + prevPos = prevPosSaved + } } } } } } - 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 checkMoveIsValid(oldPos: Position, newPos: Position): Boolean { + return newPos.row < map.rowSize && newPos.row >= 0 && + newPos.column < map.columnSize && newPos.column >= 0 && + (map.rows[newPos.row][newPos.column] == NONE || map.rows[newPos.row][newPos.column] == ENEMY) } - 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 - } +// private fun replaceSymbol(oldPos: Position, newPos: Position, replaceSymb: ColoredSymbol = NONE): Boolean { +// if (checkMoveIsValid(oldPos, newPos)) { +// 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 index 288f4ad..a09b2ea 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/MapTextPane.kt @@ -1,12 +1,10 @@ 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" 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 index 72d0a48..8c70f54 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -1,7 +1,8 @@ package ru.ifmo.sd.stuff +import ru.ifmo.sd.httpapi.models.JoinGameInfo +import ru.ifmo.sd.httpapi.models.MazeEventData import ru.ifmo.sd.stuff.ColoredSymbol.* -import ru.ifmo.sd.world.configuration.GameConfiguration import java.awt.Color import java.awt.Color.* @@ -9,12 +10,13 @@ import java.awt.Color.* enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { WALL(0x2591.toChar(), GRAY), PLAYER(0x267F.toChar()/*, RED*/), + ENEMY(0x2639.toChar()), NONE(' '), } -class SymbolMap(config: GameConfiguration) { - val rows: List> = config.level.map { arr -> - arr.map { i -> if (i == 0) NONE else WALL }.toMutableList() +class SymbolMap(config: JoinGameInfo) { + val rows: List> = config.maze.levelMaze.map { arr -> + arr.map { i -> mazeObjToSymbol(i) }.toMutableList() } init { @@ -25,4 +27,18 @@ class SymbolMap(config: GameConfiguration) { get() = rows.size val columnSize: Int get() = rows[0].size + + private fun mazeObjToSymbol(mazeObj: Int): ColoredSymbol = when (mazeObj) { + 1 -> WALL + 2 -> PLAYER + 3 -> ENEMY + else -> NONE + } + + internal fun applyDiff(events: List) { + events.forEach { e -> + val pos = e.position + rows[pos.row][pos.column] = mazeObjToSymbol(e.newMazeObj) + } + } } \ No newline at end of file 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 index 262cc51..77c0954 100644 --- 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 @@ -14,7 +14,7 @@ fun Route.gameRouting() { post("/join") { val levelConfiguration = call.receive() val startedGame = EventsHandler.join(levelConfiguration) - call.respond(message = startedGame.makeSerializable(), status = HttpStatusCode.Created) + call.respond(message = startedGame, status = HttpStatusCode.Created) } post("/move") { From 41132fda1193761b07ef0c8536c450e4d2495d93 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Mon, 3 May 2021 08:36:01 +0300 Subject: [PATCH 12/30] bug fix and add java docs --- .../world/configuration/GameConfiguration.kt | 0 ...azeEvent.kt => ChangeMazePositionEvent.kt} | 8 ++-- .../ru/ifmo/sd/world/events/EventsHandler.kt | 20 +++++++- .../world/events/InteractionExecutorImpl.kt | 42 +++++++++++++---- .../ru/ifmo/sd/world/npc/NpcEventProvider.kt | 6 +-- .../ifmo/sd/world/npc/strategy/Aggressive.kt | 47 +++++++++++++------ .../ru/ifmo/sd/world/npc/strategy/Coward.kt | 32 +++++++------ .../ru/ifmo/sd/world/npc/strategy/Passive.kt | 14 +++--- .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 45 +++++++++++++++--- .../ru/ifmo/sd/world/representation/Maze.kt | 6 +-- .../representation/UnitsHealthStorage.kt | 4 ++ .../world/representation/units/MazeObject.kt | 21 ++++++++- .../sd/world/representation/units/MazeUnit.kt | 10 ++-- .../sd/world/representation/units/Wall.kt | 6 +-- 14 files changed, 190 insertions(+), 71 deletions(-) delete mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/configuration/GameConfiguration.kt rename game-server/src/main/kotlin/ru/ifmo/sd/world/events/{MazeEvent.kt => ChangeMazePositionEvent.kt} (62%) 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 deleted file mode 100644 index e69de29..0000000 diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/ChangeMazePositionEvent.kt similarity index 62% rename from game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt rename to game-server/src/main/kotlin/ru/ifmo/sd/world/events/ChangeMazePositionEvent.kt index 233584b..d0747ea 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/MazeEvent.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/ChangeMazePositionEvent.kt @@ -3,13 +3,15 @@ package ru.ifmo.sd.world.events import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.representation.units.MazeObject - -data class MazeEvent(val position: Position, val newMazeObj: MazeObject?) { +/** + * Класс события изменения игрового объекта на заданной позиции. + */ +data class ChangeMazePositionEvent(val position: Position, val newMazeObj: MazeObject?) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as MazeEvent + other as ChangeMazePositionEvent if (position != other.position) return false 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 index 98d1ae5..d0c3aff 100644 --- 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 @@ -5,10 +5,20 @@ import ru.ifmo.sd.world.generation.LevelGenerator import ru.ifmo.sd.world.representation.GameLevel import ru.ifmo.sd.world.representation.units.* + +/** + * Класс, отвечающий за обработку игровых событий, приходящих с клиента. + */ object EventsHandler { val interactionExecutor = InteractionExecutorImpl() var gameLevel: GameLevel? = null + /** + * Присоединяет нового игрока к текущей игровой сессии, если таковая есть, или создает новую. + * + * @param levelConfiguration -- конфигурация игрового уровня + * @return начальную информацию об игровом уровне + */ fun join(levelConfiguration: LevelConfiguration): JoinGameInfo { if (gameLevel == null) { init(levelConfiguration.length, levelConfiguration.width) @@ -34,9 +44,17 @@ object EventsHandler { gameLevel = LevelGenerator.generateLevel(length, width) } + /** + * Выполняет игровое действие игрока на заданной позиции + * по отношению к игровому объекту на заданной позиции. + * + * @param playerPos -- позиция игрока + * @param targetPos -- позиция игрового объекта + * @return данные об изменениях после игрового хода + */ fun move(playerPos: Position, targetPos: Position): GameMove { val maze = gameLevel!!.maze - return if (maze[targetPos] != null) { + return if (maze[targetPos] == null) { // ход игрока maze[playerPos] = null maze[targetPos] = Player() diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt index 3ba6cc0..8bd6fdd 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt @@ -4,10 +4,36 @@ import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.representation.units.* +/** + * Интерфейс классов, отвечающих за выполнение взаимодействия с объектом на заданной позиции. + */ interface InteractionExecutor { - fun doFor(obj: Wall, objPos: Position): MutableSet - fun doFor(obj: Enemy, objPos: Position): MutableSet - fun doFor(obj: Player, objPos: Position): MutableSet + /** + * Выполняет взаимодействие со стеной. + * + * @param obj -- тип объекта + * @param objPos -- позиция объекта + * @return множество событий изменения лабиринта + */ + fun doFor(obj: Wall, objPos: Position): MutableSet + + /** + * Выполняет взаимодействие с противником. + * + * @param obj -- тип объекта + * @param objPos -- позиция объекта + * @return множество событий изменения лабиринта + */ + fun doFor(obj: Enemy, objPos: Position): MutableSet + + /** + * Выполнеяет взаимодействие с игроком. + * + * @param obj -- тип объекта + * @param objPos -- позиция объекта + * @return множество событий изменения лабиринта + */ + fun doFor(obj: Player, objPos: Position): MutableSet } class InteractionExecutorImpl : InteractionExecutor { @@ -16,28 +42,28 @@ class InteractionExecutorImpl : InteractionExecutor { const val npcDamage = 10 } - override fun doFor(obj: Wall, objPos: Position): MutableSet { + override fun doFor(obj: Wall, objPos: Position): MutableSet { return HashSet() } - override fun doFor(obj: Enemy, objPos: Position): MutableSet { + override fun doFor(obj: Enemy, objPos: Position): MutableSet { val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage unitsHealthStorage.decrease(objPos, playerDamage) return if (!unitsHealthStorage.isAlive(objPos)) { unitsHealthStorage.eliminateUnit(objPos) EventsHandler.gameLevel!!.npcEventProvider.eliminateNpc(objPos) - mutableSetOf(MazeEvent(objPos, null)) + mutableSetOf(ChangeMazePositionEvent(objPos, null)) } else { HashSet() } } - override fun doFor(obj: Player, objPos: Position): MutableSet { + override fun doFor(obj: Player, objPos: Position): MutableSet { val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage unitsHealthStorage.decrease(objPos, npcDamage) return if (!unitsHealthStorage.isAlive(objPos)) { unitsHealthStorage.eliminateUnit(objPos) - mutableSetOf(MazeEvent(objPos, null)) + mutableSetOf(ChangeMazePositionEvent(objPos, null)) } else { HashSet() } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt index 6702bfd..5f38161 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt @@ -1,11 +1,11 @@ package ru.ifmo.sd.world.npc import ru.ifmo.sd.httpapi.models.Position -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.npc.strategy.Strategy class Npc(var position: Position, private val strategy: Strategy) { - fun move(playerPos: Position): MutableSet { + fun move(playerPos: Position): MutableSet { return strategy.execute(position, playerPos) } } @@ -13,7 +13,7 @@ class Npc(var position: Position, private val strategy: Strategy) { class NpcEventProvider { private val npc: MutableSet = HashSet() - fun move(playerPos: Position): MutableSet { + fun move(playerPos: Position): MutableSet { return npc.random().move(playerPos) } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt index 7707ed8..869f2ef 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt @@ -2,44 +2,61 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.EventsHandler -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.representation.units.Enemy import kotlin.math.abs + +/** + * Класс, отвечащий агрессивной стратегии поведения NPC. + */ class Aggressive : Strategy { - private fun isNearby(npcPos: Position, playerPos: Position): Boolean { + private fun isAdjacent(npcPos: Position, playerPos: Position): Boolean { return abs(npcPos.row - playerPos.row) == 1 && abs(npcPos.column - playerPos.column) == 0 || abs(npcPos.row - playerPos.row) == 0 && abs(npcPos.column - playerPos.column) == 1 } - override fun execute(npcPos: Position, playerPos: Position): MutableSet { + override fun execute(npcPos: Position, playerPos: Position): MutableSet { + val player = EventsHandler.gameLevel!!.maze[playerPos]!! return when (Strategy.findPlayer(npcPos, playerPos)) { Strategy.Companion.PlayerDirection.North -> - if (isNearby(npcPos, playerPos)) { - EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + if (isAdjacent(npcPos, playerPos)) { + player.interact(EventsHandler.interactionExecutor, playerPos) } else { - mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(-1, 0), Enemy())) + mutableSetOf( + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + Position(-1, 0), Enemy()) + ) } Strategy.Companion.PlayerDirection.South -> - if (isNearby(npcPos, playerPos)) { - EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + if (isAdjacent(npcPos, playerPos)) { + player.interact(EventsHandler.interactionExecutor, playerPos) } else { - mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(1, 0), Enemy())) + mutableSetOf( + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + Position(1, 0), Enemy()) + ) } Strategy.Companion.PlayerDirection.West -> - if (isNearby(npcPos, playerPos)) { - EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + if (isAdjacent(npcPos, playerPos)) { + player.interact(EventsHandler.interactionExecutor, playerPos) } else { - mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(0, 1), Enemy())) + mutableSetOf( + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + Position(0, 1), Enemy()) + ) } Strategy.Companion.PlayerDirection.East -> - if (isNearby(npcPos, playerPos)) { - EventsHandler.gameLevel!!.maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) + if (isAdjacent(npcPos, playerPos)) { + player.interact(EventsHandler.interactionExecutor, playerPos) } else { - mutableSetOf(MazeEvent(npcPos, null), MazeEvent(npcPos + Position(0, -1), Enemy())) + mutableSetOf( + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + Position(0, -1), Enemy()) + ) } Strategy.Companion.PlayerDirection.Failed -> diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt index a3ae3b5..375a0d1 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt @@ -1,12 +1,14 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.representation.units.Enemy - +/** + * Класс, отвечащий трусливой стратегии поведения NPC. + */ class Coward : Strategy { - override fun execute(npcPos: Position, playerPos: Position): MutableSet { + override fun execute(npcPos: Position, playerPos: Position): MutableSet { return when (Strategy.findPlayer(npcPos, playerPos)) { Strategy.Companion.PlayerDirection.North -> { val positionsToMove = listOf( @@ -14,11 +16,11 @@ class Coward : Strategy { Position(0, 1), Position(0, -1) ) - val randomPos = Strategy.randomMove(positionsToMove) + val randomPos = Strategy.randomMove(npcPos, positionsToMove) if (randomPos != Position(0, 0)) { mutableSetOf( - MazeEvent(npcPos, null), - MazeEvent(npcPos + randomPos, Enemy()) + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + randomPos, Enemy()) ) } else { HashSet() @@ -30,11 +32,11 @@ class Coward : Strategy { Position(0, 1), Position(0, -1) ) - val randomPos = Strategy.randomMove(positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove) if (randomPos != Position(0, 0)) { mutableSetOf( - MazeEvent(npcPos, null), - MazeEvent(npcPos + randomPos, Enemy()) + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + randomPos, Enemy()) ) } else { HashSet() @@ -46,11 +48,11 @@ class Coward : Strategy { Position(0, -1), Position(-1, 0) ) - val randomPos = Strategy.randomMove(positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove) if (randomPos != Position(0, 0)) { mutableSetOf( - MazeEvent(npcPos, null), - MazeEvent(npcPos + randomPos, Enemy()) + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + randomPos, Enemy()) ) } else { HashSet() @@ -62,11 +64,11 @@ class Coward : Strategy { Position(0, 1), Position(1, 0) ) - val randomPos = Strategy.randomMove(positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove) if (randomPos != Position(0, 0)) { mutableSetOf( - MazeEvent(npcPos, null), - MazeEvent(npcPos + randomPos, Enemy()) + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + randomPos, Enemy()) ) } else { HashSet() diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt index d043a4c..652c3b3 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt @@ -1,21 +1,23 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.representation.units.Enemy - +/** + * Класс, отвечающий пассивной стратегии поведения NPC. + */ class Passive : Strategy { - override fun execute(npcPos: Position, playerPos: Position): MutableSet { + override fun execute(npcPos: Position, playerPos: Position): MutableSet { val positionsToMove = listOf( Position(0, 1), Position(0, -1), Position(1, 0), Position(-1, 0) ) - val randomMove = Strategy.randomMove(positionsToMove) + val randomMove = Strategy.randomMove(npcPos, positionsToMove) return if (randomMove != Position(0, 0)) mutableSetOf( - MazeEvent(npcPos, null), - MazeEvent(npcPos + randomMove, Enemy()) + ChangeMazePositionEvent(npcPos, null), + ChangeMazePositionEvent(npcPos + randomMove, Enemy()) ) else HashSet() } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt index c1ccc88..9653ee7 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -1,27 +1,60 @@ package ru.ifmo.sd.world.npc.strategy -import ru.ifmo.sd.world.events.MazeEvent import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.Failed - +/** + * Интерфейс стратегии поведения NPC. + */ interface Strategy { - fun execute(npcPos: Position, playerPos: Position): MutableSet + /** + * Выполняет действие NPC на заданной позиции по отношению к игроку на заданной позиции. + * + * @param npcPos -- позиция NPC + * @param playerPos -- позиция игрока + * @return множество событий изменения лабиринта + */ + fun execute(npcPos: Position, playerPos: Position): MutableSet companion object { enum class PlayerDirection { North, South, West, East, Failed; } - // Bresenham's algorithm, see en.wikipedia.org/wiki/File:Bresenham.svg + /** + * Проверяет нахождение игрового персонажа на заданной позиции + * в области видимости NPC на заданной позиции. Возвращает направление, + * в котором находится игровой персонаж относительно заданного NPC. + * Bresenham's algorithm, see en.wikipedia.org/wiki/File:Bresenham.svg + * + * @param npcPos -- позиция NPC + * @param playerPos -- позиция игрового персонажа + * @return направление или неудачу, если персонаж невидим для NPC + */ fun findPlayer(npcPos: Position, playerPos: Position): PlayerDirection { // TODO: implement Bresenham's algorithm return Failed } - fun randomMove(positions: List): Position { - val availablePos = positions.filter { EventsHandler.gameLevel!!.maze[it] == null } + /** + * Выбирает случайное перемещении NPC на заданной позиции. + * + * @param npcPos -- позиция NPC + * @param positionsToChoose -- список позиций для выбора + * @return выбранное перемещение NPC или нулевое перемещение, если доступных перемещений нет + */ + fun randomMove(npcPos: Position, positionsToChoose: List): Position { + val mazeLength = EventsHandler.gameLevel!!.maze.levelMaze.size + val mazeWidth = EventsHandler.gameLevel!!.maze.levelMaze[0].size + val availablePos = positionsToChoose.filter { + val newNpcPos = npcPos + it + (newNpcPos.row in 1..mazeLength - 2 + && newNpcPos.column in 1..mazeWidth - 2 + && EventsHandler.gameLevel!!.maze[newNpcPos] == null) + + } return if (availablePos.isNotEmpty()) availablePos.random() else Position(0, 0) } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt index 4e193d2..a811e50 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/Maze.kt @@ -3,11 +3,9 @@ package ru.ifmo.sd.world.representation import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.representation.units.MazeObject -data class Maze( - val levelMaze: Array>, - val freePos: MutableSet -) { +data class Maze(val levelMaze: Array>, val freePos: MutableSet) { operator fun get(pos: Position): MazeObject? = levelMaze[pos.row][pos.column] + operator fun set(position: Position, mazeObject: MazeObject?) { if (levelMaze[position.row][position.column] == null) { freePos.remove(position) 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 index ec44c30..a45e03c 100644 --- 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 @@ -2,6 +2,10 @@ package ru.ifmo.sd.world.representation import ru.ifmo.sd.httpapi.models.Position + +/** + * Хранилище информации о жизненных силах персонажа и NPC. + */ class UnitsHealthStorage { private val healths: MutableMap = HashMap() diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt index b11b04d..d677a00 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeObject.kt @@ -2,9 +2,26 @@ package ru.ifmo.sd.world.representation.units import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.InteractionExecutor -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent + +/** + * Интерфейс объекта игрового лабиринта. + */ interface MazeObject { - fun interact(executor: InteractionExecutor, pos: Position): MutableSet + /** + * Запускает взаимодействие с текущим объектом с помощью исполнителя взаимодействия. + * + * @param executor -- исполнитель взаимодействия + * @param mazeObjPos -- позиция текущего объекта + * @return множество событий изменения лабиринта + */ + fun interact(executor: InteractionExecutor, mazeObjPos: Position): MutableSet + + /** + * Возвращает идентификатор типа текущего объекта. + * + * @return идентификатор типа + */ fun getTypeIdentifier(): Int } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt index 31de81a..04fdf57 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt @@ -3,14 +3,14 @@ package ru.ifmo.sd.world.representation.units import kotlinx.serialization.Serializable import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.InteractionExecutor -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent interface MazeUnit : MazeObject @Serializable class Player : MazeUnit { - override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { - return executor.doFor(this, pos) + override fun interact(executor: InteractionExecutor, mazeObjPos: Position): MutableSet { + return executor.doFor(this, mazeObjPos) } override fun getTypeIdentifier() = 2 @@ -18,8 +18,8 @@ class Player : MazeUnit { @Serializable class Enemy : MazeUnit { - override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { - return executor.doFor(this, pos) + override fun interact(executor: InteractionExecutor, mazeObjPos: Position): MutableSet { + return executor.doFor(this, mazeObjPos) } override fun getTypeIdentifier() = 3 diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt index 991a00a..a0452c8 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/Wall.kt @@ -2,11 +2,11 @@ package ru.ifmo.sd.world.representation.units import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.InteractionExecutor -import ru.ifmo.sd.world.events.MazeEvent +import ru.ifmo.sd.world.events.ChangeMazePositionEvent class Wall : MazeObject { - override fun interact(executor: InteractionExecutor, pos: Position): MutableSet { - return executor.doFor(this, pos) + override fun interact(executor: InteractionExecutor, mazeObjPos: Position): MutableSet { + return executor.doFor(this, mazeObjPos) } override fun getTypeIdentifier() = 1 From bbcd9f61240e81a507d0a7f2f6904397b56de9cc Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Mon, 3 May 2021 10:01:36 +0300 Subject: [PATCH 13/30] update generator, add algorithm for determining visibility of player --- .../sd/world/generation/LevelGenerator.kt | 10 +++ .../ru/ifmo/sd/world/npc/NpcEventProvider.kt | 34 +++++++++- .../ifmo/sd/world/npc/strategy/Aggressive.kt | 5 +- .../ru/ifmo/sd/world/npc/strategy/Coward.kt | 13 ++-- .../ru/ifmo/sd/world/npc/strategy/Passive.kt | 5 +- .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 65 ++++++++++++++++--- 6 files changed, 112 insertions(+), 20 deletions(-) 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 index cd3b206..72904c9 100644 --- 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 @@ -5,6 +5,7 @@ import ru.ifmo.sd.world.npc.NpcEventProvider import ru.ifmo.sd.world.npc.strategy.* import ru.ifmo.sd.world.representation.* import ru.ifmo.sd.world.representation.units.Enemy +import ru.ifmo.sd.world.representation.units.Wall import kotlin.random.Random /** @@ -59,12 +60,21 @@ class LevelGenerator { null } }, HashSet()) + fillMaze(maze) val unitsHealths = UnitsHealthStorage() val npc = NpcEventProvider() placeEnemies(maze, unitsHealths, npc) return GameLevel(maze, unitsHealths, npc) } + private fun fillMaze(maze: Maze) { + for (i in level.indices) { + for (j in level[0].indices) { + if (level[i][j] == 1) maze[Position(i, j)] = Wall() + } + } + } + // Идея в том, чтобы пробуривать соседние занятые клетки и избегать при этом циклов. private fun generateRec(curRow: Int, curColumn: Int) { // Идентификатор текущего направления движения при генерации лабиринта diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt index 5f38161..72d5dfa 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt @@ -2,25 +2,55 @@ package ru.ifmo.sd.world.npc import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.npc.strategy.Strategy +/** + * Класс NPC. + */ class Npc(var position: Position, private val strategy: Strategy) { + /** + * Выполняет игровой ход текущего NPC по отношению к игровому персонажу на заданной позиции. + * + * @param playerPos -- позиция игрового персонажа + * @return множество событий игрового лабиринта + */ fun move(playerPos: Position): MutableSet { - return strategy.execute(position, playerPos) + return strategy.execute(position, playerPos, EventsHandler.gameLevel!!.maze) } } +/** + * Класс, отвечающий за игровые действия NPC. + */ class NpcEventProvider { private val npc: MutableSet = HashSet() + /** + * Выполняет игровое действие случайного NPC по отношению к игровому персонажу на заданной позиции. + * + * @param playerPos -- позиция игрового персонажа + * @return множество событий игрового лабиринта + */ fun move(playerPos: Position): MutableSet { - return npc.random().move(playerPos) + return if (npc.isNotEmpty()) npc.random().move(playerPos) else HashSet() } + /** + * Добавляет нового NPC с заданной стратегией на заданную позицию. + * + * @param pos -- позиция нового NPC + * @param strategy -- стратегия поведения нового NPC + */ fun addNpc(pos: Position, strategy: Strategy) { npc.add(Npc(pos, strategy)) } + /** + * Уничтожает NPC на заданной позиции. + * + * @param pos -- позиция NPC + */ fun eliminateNpc(pos: Position) { npc.removeIf { it.position == pos } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt index 869f2ef..5f2edc0 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt @@ -3,6 +3,7 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.representation.Maze import ru.ifmo.sd.world.representation.units.Enemy import kotlin.math.abs @@ -16,9 +17,9 @@ class Aggressive : Strategy { || abs(npcPos.row - playerPos.row) == 0 && abs(npcPos.column - playerPos.column) == 1 } - override fun execute(npcPos: Position, playerPos: Position): MutableSet { + override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { val player = EventsHandler.gameLevel!!.maze[playerPos]!! - return when (Strategy.findPlayer(npcPos, playerPos)) { + return when (Strategy.findPlayer(npcPos, playerPos, maze)) { Strategy.Companion.PlayerDirection.North -> if (isAdjacent(npcPos, playerPos)) { player.interact(EventsHandler.interactionExecutor, playerPos) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt index 375a0d1..f289997 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt @@ -2,21 +2,22 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.representation.Maze import ru.ifmo.sd.world.representation.units.Enemy /** * Класс, отвечащий трусливой стратегии поведения NPC. */ class Coward : Strategy { - override fun execute(npcPos: Position, playerPos: Position): MutableSet { - return when (Strategy.findPlayer(npcPos, playerPos)) { + override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { + return when (Strategy.findPlayer(npcPos, playerPos, maze)) { Strategy.Companion.PlayerDirection.North -> { val positionsToMove = listOf( Position(1, 0), Position(0, 1), Position(0, -1) ) - val randomPos = Strategy.randomMove(npcPos, positionsToMove) + val randomPos = Strategy.randomMove(npcPos, positionsToMove, maze) if (randomPos != Position(0, 0)) { mutableSetOf( ChangeMazePositionEvent(npcPos, null), @@ -32,7 +33,7 @@ class Coward : Strategy { Position(0, 1), Position(0, -1) ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) if (randomPos != Position(0, 0)) { mutableSetOf( ChangeMazePositionEvent(npcPos, null), @@ -48,7 +49,7 @@ class Coward : Strategy { Position(0, -1), Position(-1, 0) ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) if (randomPos != Position(0, 0)) { mutableSetOf( ChangeMazePositionEvent(npcPos, null), @@ -64,7 +65,7 @@ class Coward : Strategy { Position(0, 1), Position(1, 0) ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove) + val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) if (randomPos != Position(0, 0)) { mutableSetOf( ChangeMazePositionEvent(npcPos, null), diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt index 652c3b3..9a93868 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt @@ -2,18 +2,19 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.representation.Maze import ru.ifmo.sd.world.representation.units.Enemy /** * Класс, отвечающий пассивной стратегии поведения NPC. */ class Passive : Strategy { - override fun execute(npcPos: Position, playerPos: Position): MutableSet { + override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { val positionsToMove = listOf( Position(0, 1), Position(0, -1), Position(1, 0), Position(-1, 0) ) - val randomMove = Strategy.randomMove(npcPos, positionsToMove) + val randomMove = Strategy.randomMove(npcPos, positionsToMove, maze) return if (randomMove != Position(0, 0)) mutableSetOf( ChangeMazePositionEvent(npcPos, null), diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt index 9653ee7..29b5f6c 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -2,8 +2,9 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent -import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.Failed +import ru.ifmo.sd.world.representation.Maze +import kotlin.math.abs /** * Интерфейс стратегии поведения NPC. @@ -14,9 +15,10 @@ interface Strategy { * * @param npcPos -- позиция NPC * @param playerPos -- позиция игрока + * @param maze -- игровой лабиринт * @return множество событий изменения лабиринта */ - fun execute(npcPos: Position, playerPos: Position): MutableSet + fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet companion object { enum class PlayerDirection { @@ -31,28 +33,75 @@ interface Strategy { * * @param npcPos -- позиция NPC * @param playerPos -- позиция игрового персонажа + * @param maze -- игровой лабиринт * @return направление или неудачу, если персонаж невидим для NPC */ - fun findPlayer(npcPos: Position, playerPos: Position): PlayerDirection { - // TODO: implement Bresenham's algorithm + fun findPlayer(npcPos: Position, playerPos: Position, maze: Maze): PlayerDirection { + var d = 0 + + val dx: Int = abs(playerPos.row - npcPos.row) + val dy: Int = abs(playerPos.column - npcPos.column) + + val dx2 = 2 * dx + val dy2 = 2 * dy + + val ix = if (npcPos.row < playerPos.row) 1 else -1 + val iy = if (npcPos.column < playerPos.column) 1 else -1 + + var x: Int = npcPos.row + var y: Int = npcPos.column + + if (dx >= dy) { + while (true) { + if (!checkVisibility(Position(x, y), maze)) { + return Failed + } + if (x == playerPos.row) break + x += ix + d += dy2 + if (d > dx) { + y += iy + d -= dx2 + } + } + } else { + while (true) { + if (!checkVisibility(Position(x, y), maze)) { + return Failed + } + if (y == playerPos.column) break + y += iy + d += dx2 + if (d > dy) { + x += ix + d -= dy2 + } + } + } + return Failed } + private fun checkVisibility(pos: Position, maze: Maze): Boolean { + return maze[pos] == null + } + /** * Выбирает случайное перемещении NPC на заданной позиции. * * @param npcPos -- позиция NPC * @param positionsToChoose -- список позиций для выбора + * @param maze -- игровой лабиринт * @return выбранное перемещение NPC или нулевое перемещение, если доступных перемещений нет */ - fun randomMove(npcPos: Position, positionsToChoose: List): Position { - val mazeLength = EventsHandler.gameLevel!!.maze.levelMaze.size - val mazeWidth = EventsHandler.gameLevel!!.maze.levelMaze[0].size + fun randomMove(npcPos: Position, positionsToChoose: List, maze: Maze): Position { + val mazeLength = maze.levelMaze.size + val mazeWidth = maze.levelMaze[0].size val availablePos = positionsToChoose.filter { val newNpcPos = npcPos + it (newNpcPos.row in 1..mazeLength - 2 && newNpcPos.column in 1..mazeWidth - 2 - && EventsHandler.gameLevel!!.maze[newNpcPos] == null) + && maze[newNpcPos] == null) } return if (availablePos.isNotEmpty()) availablePos.random() else Position(0, 0) From c592241973f7c2027b8a43d9ad6a6b8931c0937d Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Mon, 3 May 2021 13:11:04 +0500 Subject: [PATCH 14/30] client: new api support --- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 90 ++++++++++--------- .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 2 +- 2 files changed, 50 insertions(+), 42 deletions(-) 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 index 8dde7a2..b2e1411 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -10,6 +10,7 @@ import ru.ifmo.sd.stuff.ColoredSymbol.* import java.awt.BorderLayout import java.awt.Color.* import java.awt.Container +import java.awt.SystemColor.infoText import java.awt.event.KeyEvent import java.awt.event.KeyEvent.* import java.awt.event.KeyListener @@ -29,10 +30,12 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene prevPos = currPos field = value } + private var isDead = false private val mainPanel = JPanel() private val headerPanel = JPanel() private val mapPanel = JPanel() private val infoPanel = JPanel() + private val infoLabel = JLabel("") private var map = SymbolMap(gameConfiguration) private var mapTextPane = MapTextPane() @@ -88,9 +91,8 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene headerPanel.setSize(800, 50) mainPanel.add(headerPanel, BorderLayout.NORTH) -// val infoLabel = JLabel("loading...") -// infoLabel.isFocusable = false -// headerPanel.add(infoLabel) + headerPanel.add(infoLabel) + mapPanel.setSize(550, 550) mainPanel.add(mapPanel, BorderLayout.WEST) @@ -101,7 +103,6 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene mainPanel.add(infoPanel, BorderLayout.EAST) // val hpLabel = JLabel("HP: ") -// hpLabel.isFocusable = false // infoPanel.add(hpLabel, BorderLayout.WEST) } @@ -152,18 +153,20 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene override fun keyReleased(e: KeyEvent?) {} override fun keyTyped(e: KeyEvent?) {} override fun keyPressed(e: KeyEvent?) { + if (isDead) return if (e != null) { println("Some key pressed, current=$moved, keyCode=${e.keyCode}, didMove=$didMove, currPos=$currPos") } else { println("e == null") } if (!didMove) { + var newPos = currPos when (e?.keyCode) { null -> { moved = MoveEvent.NONE } VK_UP, VK_W -> { - val newPos = Position(currPos.row - 1, currPos.column) + newPos = Position(currPos.row - 1, currPos.column) if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.UP @@ -171,7 +174,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } } VK_DOWN, VK_S -> { - val newPos = Position(currPos.row + 1, currPos.column) + newPos = Position(currPos.row + 1, currPos.column) if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.DOWN @@ -179,7 +182,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } } VK_LEFT, VK_A -> { - val newPos = Position(currPos.row, currPos.column - 1) + newPos = Position(currPos.row, currPos.column - 1) if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.LEFT @@ -187,7 +190,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } } VK_RIGHT, VK_D -> { - val newPos = Position(currPos.row, currPos.column + 1) + newPos = Position(currPos.row, currPos.column + 1) if (checkMoveIsValid(currPos, newPos)) { didMove = true moved = MoveEvent.RIGHT @@ -208,28 +211,33 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene this.mapTextPane.repaint() runBlocking { - if (currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) { - // level ended - val newConfig = apiJoin() - map = SymbolMap(newConfig) - reloadMapTextPane() - val prevPosSaved = prevPos - currPos = newConfig.playerPos - prevPos = prevPosSaved - } else { +// if (currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) { +// // level ended +// val newConfig = apiJoin() +// map = SymbolMap(newConfig) +//// replaceSymbol(currPos, newPos) +// val prevPosSaved = prevPos +// currPos = newConfig.playerPos +// prevPos = prevPosSaved +// reloadMapTextPane() +// } else { val gameMove = apiMove(prevPos!!, currPos) + println("gameMove=$gameMove") if (gameMove.playerPosition == Position(-1, -1)) { // player is dead - println("You are dead!") + isDead = true + infoLabel.text = "You are dead!" + replacePaneSymbol(prevPos!!, NONE) } else { // player moved + replaceSymbol(prevPos!!, gameMove.playerPosition) map.applyDiff(gameMove.events) - reloadMapTextPane() val prevPosSaved = prevPos currPos = gameMove.playerPosition prevPos = prevPosSaved + reloadMapTextPane() } - } +// } } } } @@ -241,25 +249,25 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene (map.rows[newPos.row][newPos.column] == NONE || map.rows[newPos.row][newPos.column] == ENEMY) } -// private fun replaceSymbol(oldPos: Position, newPos: Position, replaceSymb: ColoredSymbol = NONE): Boolean { -// if (checkMoveIsValid(oldPos, newPos)) { -// 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 -// } + private fun replaceSymbol(oldPos: Position, newPos: Position, replaceSymb: ColoredSymbol = NONE): Boolean { + if (checkMoveIsValid(oldPos, newPos)) { + 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/SymbolMap.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt index 8c70f54..d079c5d 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -36,7 +36,7 @@ class SymbolMap(config: JoinGameInfo) { } internal fun applyDiff(events: List) { - events.forEach { e -> + for (e in events) { val pos = e.position rows[pos.row][pos.column] = mazeObjToSymbol(e.newMazeObj) } From 6fbbe2dcc3a20e8503c81cb3826ac9d6fe67d395 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Mon, 3 May 2021 15:36:10 +0300 Subject: [PATCH 15/30] refactoring and bug fixes --- .../ru/ifmo/sd/httpapi/models/JoinGameInfo.kt | 3 +- .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 2 +- .../ru/ifmo/sd/world/events/EventsHandler.kt | 38 ++++--- .../sd/world/generation/LevelGenerator.kt | 13 ++- .../ru/ifmo/sd/world/npc/NpcEventProvider.kt | 12 +- .../ifmo/sd/world/npc/strategy/Aggressive.kt | 70 +++++------- .../ru/ifmo/sd/world/npc/strategy/Coward.kt | 104 +++++++----------- .../ru/ifmo/sd/world/npc/strategy/Passive.kt | 25 +++-- .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 20 +++- .../representation/UnitsHealthStorage.kt | 41 +++++++ .../representation/units/EnemyFactory.kt | 29 +++++ .../sd/world/representation/units/MazeUnit.kt | 15 ++- 12 files changed, 227 insertions(+), 145 deletions(-) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/EnemyFactory.kt diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt index a66ad0e..0ae498c 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt @@ -10,5 +10,6 @@ data class MazeData( @Serializable data class JoinGameInfo( val playerPos: Position, - val maze: MazeData + val maze: MazeData, + val unitsHealth: Map ) 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 index 77c0954..43c03c4 100644 --- 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 @@ -19,7 +19,7 @@ fun Route.gameRouting() { post("/move") { val playerMove = call.receive() - val move = EventsHandler.move(playerMove.oldPosition, playerMove.newPosition) + val move = EventsHandler.move(playerMove.oldPosition, playerMove.newPosition, EventsHandler.gameLevel!!) call.respond(message = move, status = HttpStatusCode.Accepted) } } 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 index d0c3aff..73bd36a 100644 --- 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 @@ -3,7 +3,8 @@ package ru.ifmo.sd.world.events import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.world.generation.LevelGenerator import ru.ifmo.sd.world.representation.GameLevel -import ru.ifmo.sd.world.representation.units.* +import ru.ifmo.sd.world.representation.units.MazeObject +import ru.ifmo.sd.world.representation.units.Player /** @@ -23,21 +24,28 @@ object EventsHandler { if (gameLevel == null) { init(levelConfiguration.length, levelConfiguration.width) } - val playerPos = gameLevel!!.maze.freePos.random() + + val maze = gameLevel!!.maze + val playerPos = maze.freePos.random() gameLevel!!.unitsHealthStorage.addUnit(playerPos) - gameLevel!!.maze[playerPos] = Player() + maze[playerPos] = Player() + + return JoinGameInfo( + playerPos, MazeData(getMazeData()), + gameLevel!!.unitsHealthStorage.getHealths() + ) + } - val length = gameLevel!!.maze.levelMaze.size - val width = gameLevel!!.maze.levelMaze[0].size - val mazeData: Array> = Array(length) { Array(width) { 0 } } - for (i in 0 until length) { - for (j in 0 until width) { - val mazeObj: MazeObject? = gameLevel!!.maze[Position(i, j)] + private fun getMazeData(): Array> { + val maze = gameLevel!!.maze + val mazeData: Array> = Array(maze.levelMaze.size) { Array(maze.levelMaze[0].size) { 0 } } + for (i in mazeData.indices) { + for (j in mazeData[0].indices) { + val mazeObj: MazeObject? = maze[Position(i, j)] mazeData[i][j] = mazeObj?.getTypeIdentifier() ?: 0 } } - - return JoinGameInfo(playerPos, MazeData(mazeData)) + return mazeData } private fun init(length: Int, width: Int) { @@ -52,15 +60,15 @@ object EventsHandler { * @param targetPos -- позиция игрового объекта * @return данные об изменениях после игрового хода */ - fun move(playerPos: Position, targetPos: Position): GameMove { - val maze = gameLevel!!.maze + fun move(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + val maze = gameLevel.maze return if (maze[targetPos] == null) { // ход игрока maze[playerPos] = null maze[targetPos] = Player() // результат хода игрового мира - val npcMove = gameLevel!!.npcEventProvider.move(targetPos) + val npcMove = gameLevel.npcEventProvider.move(targetPos, maze) npcMove.forEach { maze[it.position] = it.newMazeObj } val newPlayerPos = if (maze[targetPos] != null) targetPos @@ -78,7 +86,7 @@ object EventsHandler { playerMove.forEach { maze[it.position] = it.newMazeObj } // результат хода игрового мира - val npcMove = gameLevel!!.npcEventProvider.move(playerPos) + val npcMove = gameLevel.npcEventProvider.move(playerPos, maze) npcMove.forEach { maze[it.position] = it.newMazeObj } val newPlayerPos = if (maze[playerPos] != null) playerPos 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 index 72904c9..78fa577 100644 --- 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 @@ -6,6 +6,7 @@ import ru.ifmo.sd.world.npc.strategy.* import ru.ifmo.sd.world.representation.* import ru.ifmo.sd.world.representation.units.Enemy import ru.ifmo.sd.world.representation.units.Wall +import kotlin.properties.Delegates import kotlin.random.Random /** @@ -14,7 +15,7 @@ import kotlin.random.Random */ class LevelGenerator { companion object { - private const val GAME_DIFFICULTY = 0.1 + private var gameDifficulty by Delegates.notNull() private lateinit var level: Array private lateinit var freePos: MutableList @@ -25,7 +26,8 @@ class LevelGenerator { * @param width -- ширина лабиринта * @return двумерный массив соответствующий игровому лабиринту */ - fun generateLevel(length: Int = 10, width: Int = 10): GameLevel { + fun generateLevel(length: Int = 10, width: Int = 10, difficulty: Float = 0.1F): GameLevel { + gameDifficulty = difficulty /* Шаг смещения позиции при создании лабиринта определим равным 2. Создаем лабиринт с нечетными размерностями, чтобы поддерживать @@ -130,16 +132,17 @@ class LevelGenerator { } private fun placeEnemies(maze: Maze, healths: UnitsHealthStorage, npc: NpcEventProvider) { - val countOfEnemies = (maze.size() * GAME_DIFFICULTY).toInt() + val countOfEnemies = (maze.size() * gameDifficulty).toInt() freePos.forEach { maze.freePos.add(it) } val enemiesPositions = freePos .shuffled() .take(countOfEnemies) val strategies = listOf(Passive(), Aggressive(), Coward()) enemiesPositions.forEach { - maze[it] = Enemy() + val chosenStrategy = strategies.random() + maze[it] = chosenStrategy.getEnemyFactory().getEnemy() healths.addUnit(it, 100) - npc.addNpc(it, strategies.random()) + npc.addNpc(it, chosenStrategy) } } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt index 72d5dfa..3a16a12 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt @@ -2,8 +2,8 @@ package ru.ifmo.sd.world.npc import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent -import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.npc.strategy.Strategy +import ru.ifmo.sd.world.representation.Maze /** * Класс NPC. @@ -13,10 +13,11 @@ class Npc(var position: Position, private val strategy: Strategy) { * Выполняет игровой ход текущего NPC по отношению к игровому персонажу на заданной позиции. * * @param playerPos -- позиция игрового персонажа + * @param maze -- игровой лабиринт * @return множество событий игрового лабиринта */ - fun move(playerPos: Position): MutableSet { - return strategy.execute(position, playerPos, EventsHandler.gameLevel!!.maze) + fun move(playerPos: Position, maze: Maze): MutableSet { + return strategy.execute(this, playerPos, maze) } } @@ -30,10 +31,11 @@ class NpcEventProvider { * Выполняет игровое действие случайного NPC по отношению к игровому персонажу на заданной позиции. * * @param playerPos -- позиция игрового персонажа + * @param maze -- игровой лабиринт * @return множество событий игрового лабиринта */ - fun move(playerPos: Position): MutableSet { - return if (npc.isNotEmpty()) npc.random().move(playerPos) else HashSet() + fun move(playerPos: Position, maze: Maze): MutableSet { + return if (npc.isNotEmpty()) npc.random().move(playerPos, maze) else HashSet() } /** diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt index 5f2edc0..0cc6e80 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt @@ -1,10 +1,12 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position -import ru.ifmo.sd.world.events.EventsHandler import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.events.EventsHandler +import ru.ifmo.sd.world.npc.Npc +import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection import ru.ifmo.sd.world.representation.Maze -import ru.ifmo.sd.world.representation.units.Enemy +import ru.ifmo.sd.world.representation.units.* import kotlin.math.abs @@ -17,51 +19,37 @@ class Aggressive : Strategy { || abs(npcPos.row - playerPos.row) == 0 && abs(npcPos.column - playerPos.column) == 1 } - override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { - val player = EventsHandler.gameLevel!!.maze[playerPos]!! - return when (Strategy.findPlayer(npcPos, playerPos, maze)) { - Strategy.Companion.PlayerDirection.North -> - if (isAdjacent(npcPos, playerPos)) { - player.interact(EventsHandler.interactionExecutor, playerPos) - } else { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + Position(-1, 0), Enemy()) - ) - } - - Strategy.Companion.PlayerDirection.South -> - if (isAdjacent(npcPos, playerPos)) { - player.interact(EventsHandler.interactionExecutor, playerPos) - } else { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + Position(1, 0), Enemy()) - ) - } + override fun getEnemyFactory(): EnemyFactory { + return AggressiveEnemyFactory() + } - Strategy.Companion.PlayerDirection.West -> - if (isAdjacent(npcPos, playerPos)) { - player.interact(EventsHandler.interactionExecutor, playerPos) + override fun execute(npc: Npc, playerPos: Position, maze: Maze): MutableSet = + when (val direction = Strategy.findPlayer(npc.position, playerPos, maze)) { + PlayerDirection.Failed -> + HashSet() + else -> + if (isAdjacent(npc.position, playerPos)) { + EventsHandler + .gameLevel!! + .maze[playerPos]!! + .interact(EventsHandler.interactionExecutor, playerPos) } else { + val oldNpcPos = npc.position + npc.position = npc.position + getDirection(direction) mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + Position(0, 1), Enemy()) + ChangeMazePositionEvent(oldNpcPos, null), + ChangeMazePositionEvent(npc.position, getEnemyFactory().getEnemy()) ) } + } - Strategy.Companion.PlayerDirection.East -> - if (isAdjacent(npcPos, playerPos)) { - player.interact(EventsHandler.interactionExecutor, playerPos) - } else { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + Position(0, -1), Enemy()) - ) - } - Strategy.Companion.PlayerDirection.Failed -> - HashSet() + private fun getDirection(direction: PlayerDirection): Position = + when (direction) { + PlayerDirection.North -> Position(-1, 0) + PlayerDirection.South -> Position(1, 0) + PlayerDirection.West -> Position(0, 1) + PlayerDirection.East -> Position(0, -1) + PlayerDirection.Failed -> Position(0, 0) } - } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt index f289997..49b4776 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt @@ -2,80 +2,60 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.npc.Npc +import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection import ru.ifmo.sd.world.representation.Maze -import ru.ifmo.sd.world.representation.units.Enemy +import ru.ifmo.sd.world.representation.units.* /** * Класс, отвечащий трусливой стратегии поведения NPC. */ class Coward : Strategy { - override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { - return when (Strategy.findPlayer(npcPos, playerPos, maze)) { - Strategy.Companion.PlayerDirection.North -> { - val positionsToMove = listOf( - Position(1, 0), - Position(0, 1), - Position(0, -1) - ) - val randomPos = Strategy.randomMove(npcPos, positionsToMove, maze) - if (randomPos != Position(0, 0)) { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + randomPos, Enemy()) - ) - } else { - HashSet() - } - } - Strategy.Companion.PlayerDirection.South -> { - val positionsToMove = listOf( - Position(-1, 0), - Position(0, 1), - Position(0, -1) - ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) - if (randomPos != Position(0, 0)) { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + randomPos, Enemy()) - ) - } else { - HashSet() - } - } - Strategy.Companion.PlayerDirection.West -> { - val positionsToMove = listOf( - Position(1, 0), - Position(0, -1), - Position(-1, 0) - ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) - if (randomPos != Position(0, 0)) { - mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + randomPos, Enemy()) - ) - } else { - HashSet() - } - } - Strategy.Companion.PlayerDirection.East -> { - val positionsToMove = listOf( - Position(1, 0), - Position(0, 1), - Position(1, 0) - ) - val randomPos = Strategy.randomMove(npcPos,positionsToMove, maze) + override fun getEnemyFactory(): EnemyFactory { + return CowardEnemyFactory() + } + + override fun execute(npc: Npc, playerPos: Position, maze: Maze): MutableSet = + when (val direction = Strategy.findPlayer(npc.position, playerPos, maze)) { + PlayerDirection.Failed -> HashSet() + else -> { + val directionsToMove = getDirections(direction) + val randomPos = Strategy.randomDirection(npc.position, directionsToMove, maze) if (randomPos != Position(0, 0)) { + val oldNpcPos = npc.position + npc.position = npc.position + randomPos mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + randomPos, Enemy()) + ChangeMazePositionEvent(oldNpcPos, null), + ChangeMazePositionEvent(npc.position, getEnemyFactory().getEnemy()) ) } else { HashSet() } } - Strategy.Companion.PlayerDirection.Failed -> HashSet() } - } + + private fun getDirections(direction: PlayerDirection): List = + when (direction) { + PlayerDirection.North -> listOf( + Position(1, 0), + Position(0, 1), + Position(0, -1) + ) + PlayerDirection.South -> listOf( + Position(-1, 0), + Position(0, 1), + Position(0, -1) + ) + PlayerDirection.West -> listOf( + Position(1, 0), + Position(0, -1), + Position(-1, 0) + ) + PlayerDirection.East -> listOf( + Position(1, 0), + Position(0, 1), + Position(1, 0) + ) + PlayerDirection.Failed -> emptyList() + } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt index 9a93868..fb01522 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt @@ -2,24 +2,33 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.npc.Npc import ru.ifmo.sd.world.representation.Maze -import ru.ifmo.sd.world.representation.units.Enemy +import ru.ifmo.sd.world.representation.units.EnemyFactory +import ru.ifmo.sd.world.representation.units.PassiveEnemyFactory /** * Класс, отвечающий пассивной стратегии поведения NPC. */ class Passive : Strategy { - override fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet { - val positionsToMove = listOf( + override fun getEnemyFactory(): EnemyFactory { + return PassiveEnemyFactory() + } + + override fun execute(npc: Npc, playerPos: Position, maze: Maze): MutableSet { + val directionsToMove = listOf( Position(0, 1), Position(0, -1), Position(1, 0), Position(-1, 0) ) - val randomMove = Strategy.randomMove(npcPos, positionsToMove, maze) - return if (randomMove != Position(0, 0)) + val randomMove = Strategy.randomDirection(npc.position, directionsToMove, maze) + + return if (randomMove != Position(0, 0)) { + val oldNpcPos = npc.position + npc.position = npc.position + randomMove mutableSetOf( - ChangeMazePositionEvent(npcPos, null), - ChangeMazePositionEvent(npcPos + randomMove, Enemy()) + ChangeMazePositionEvent(oldNpcPos, null), + ChangeMazePositionEvent(npc.position, getEnemyFactory().getEnemy()) ) - else HashSet() + } else HashSet() } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt index 29b5f6c..2e563d5 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -2,23 +2,32 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent +import ru.ifmo.sd.world.npc.Npc import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.Failed import ru.ifmo.sd.world.representation.Maze +import ru.ifmo.sd.world.representation.units.EnemyFactory import kotlin.math.abs /** * Интерфейс стратегии поведения NPC. */ interface Strategy { + /** + * Возвращает фабрику объектов лабиринта, соответствующих противникам с данной стратегией поведения. + * + * @return фабрика противников + */ + fun getEnemyFactory(): EnemyFactory + /** * Выполняет действие NPC на заданной позиции по отношению к игроку на заданной позиции. * - * @param npcPos -- позиция NPC + * @param npc-- NPC, выполняющий действие * @param playerPos -- позиция игрока * @param maze -- игровой лабиринт * @return множество событий изменения лабиринта */ - fun execute(npcPos: Position, playerPos: Position, maze: Maze): MutableSet + fun execute(npc: Npc, playerPos: Position, maze: Maze): MutableSet companion object { enum class PlayerDirection { @@ -90,19 +99,18 @@ interface Strategy { * Выбирает случайное перемещении NPC на заданной позиции. * * @param npcPos -- позиция NPC - * @param positionsToChoose -- список позиций для выбора + * @param directions -- список позиций для выбора * @param maze -- игровой лабиринт * @return выбранное перемещение NPC или нулевое перемещение, если доступных перемещений нет */ - fun randomMove(npcPos: Position, positionsToChoose: List, maze: Maze): Position { + fun randomDirection(npcPos: Position, directions: List, maze: Maze): Position { val mazeLength = maze.levelMaze.size val mazeWidth = maze.levelMaze[0].size - val availablePos = positionsToChoose.filter { + val availablePos = directions.filter { val newNpcPos = npcPos + it (newNpcPos.row in 1..mazeLength - 2 && newNpcPos.column in 1..mazeWidth - 2 && maze[newNpcPos] == null) - } return if (availablePos.isNotEmpty()) availablePos.random() else Position(0, 0) } 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 index a45e03c..2d7ec00 100644 --- 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 @@ -9,22 +9,63 @@ import ru.ifmo.sd.httpapi.models.Position class UnitsHealthStorage { private val healths: MutableMap = HashMap() + /** + * Возвращает словарь жизненных сил юнитов. + * + * @return словарь с жизненными силами юнитов + */ + fun getHealths(): Map { + return healths + } + + /** + * Добавляет в хранилище нового юнита на заданную + * позицию с заданным количеством жизненных сил. + * + * @param pos -- позиция нового юнита + * @param value -- количество жизненных сил нового юнита + */ fun addUnit(pos: Position, value: Int = 100) { healths[pos] = value } + /** + * Уничтожает юнита на заданной позиции. + * + * @param pos -- позиция юнита + */ fun eliminateUnit(pos: Position) { healths.remove(pos) } + /** + * Увеличивает количество жизненных сил юнита на + * заданной позиции на заданное количество единиц. + * + * @param pos -- позиция юнита + * @param value -- количество жизненных сил + */ fun increase(pos: Position, value: Int) { healths.merge(pos, value, Int::plus) } + /** + * Уменьшает количество жизненных сил юнита на + * заданной позиции на заданное количество единиц. + * + * @param pos -- позиция юнита + * @param value -- количество жизненных сил + */ fun decrease(pos: Position, value: Int) { healths.merge(pos, value, Int::minus) } + /** + * Проверяет, что юнит на заданной позиции жив. + * + * @param pos -- позиция юнита + * @return флаг, соответствующий состоянию юнита + */ fun isAlive(pos: Position): Boolean { return healths.getOrDefault(pos, 0) > 0 } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/EnemyFactory.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/EnemyFactory.kt new file mode 100644 index 0000000..07f3d15 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/EnemyFactory.kt @@ -0,0 +1,29 @@ +package ru.ifmo.sd.world.representation.units + +/** + * Интерфейс фабрики противников. + */ +interface EnemyFactory { + /** + * Возвращает противника. + */ + fun getEnemy(): Enemy +} + +class AggressiveEnemyFactory : EnemyFactory { + override fun getEnemy(): Enemy { + return AggressiveEnemy() + } +} + +class PassiveEnemyFactory : EnemyFactory { + override fun getEnemy(): Enemy { + return PassiveEnemy() + } +} + +class CowardEnemyFactory : EnemyFactory { + override fun getEnemy(): Enemy { + return CowardEnemy() + } +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt index 04fdf57..f2fa359 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/representation/units/MazeUnit.kt @@ -17,10 +17,23 @@ class Player : MazeUnit { } @Serializable -class Enemy : MazeUnit { +abstract class Enemy : MazeUnit { override fun interact(executor: InteractionExecutor, mazeObjPos: Position): MutableSet { return executor.doFor(this, mazeObjPos) } +} +@Serializable +class PassiveEnemy : Enemy() { override fun getTypeIdentifier() = 3 } + +@Serializable +class AggressiveEnemy : Enemy() { + override fun getTypeIdentifier() = 4 +} + +@Serializable +class CowardEnemy : Enemy() { + override fun getTypeIdentifier() = 5 +} From cdced7a70c6857d135aaa01dc9754a949adc71a0 Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Mon, 3 May 2021 18:24:50 +0500 Subject: [PATCH 16/30] server: serialization fix --- .../src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt | 2 +- .../src/main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt index 0ae498c..3be1636 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt @@ -11,5 +11,5 @@ data class MazeData( data class JoinGameInfo( val playerPos: Position, val maze: MazeData, - val unitsHealth: Map + val unitsHealth: List> ) 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 index 73bd36a..a4e2ce7 100644 --- 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 @@ -5,6 +5,7 @@ import ru.ifmo.sd.world.generation.LevelGenerator import ru.ifmo.sd.world.representation.GameLevel import ru.ifmo.sd.world.representation.units.MazeObject import ru.ifmo.sd.world.representation.units.Player +import java.util.stream.Collectors /** @@ -32,7 +33,8 @@ object EventsHandler { return JoinGameInfo( playerPos, MazeData(getMazeData()), - gameLevel!!.unitsHealthStorage.getHealths() + gameLevel!!.unitsHealthStorage.getHealths().entries.stream().map { e -> Pair(e.key, e.value) } + .collect(Collectors.toList()) ) } From 741f62fd9cbb9911b039ca944793573dfbdd1267 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Mon, 3 May 2021 16:26:27 +0300 Subject: [PATCH 17/30] add restart game method --- .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 5 ++ .../ru/ifmo/sd/world/events/EventsHandler.kt | 89 +++++++++++-------- 2 files changed, 56 insertions(+), 38 deletions(-) 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 index 43c03c4..737fd27 100644 --- 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 @@ -22,6 +22,11 @@ fun Route.gameRouting() { val move = EventsHandler.move(playerMove.oldPosition, playerMove.newPosition, EventsHandler.gameLevel!!) call.respond(message = move, status = HttpStatusCode.Accepted) } + + get("/restart") { + EventsHandler.restartGame() + call.respond( HttpStatusCode.Accepted, "Game was successfully restarted") + } } } 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 index a4e2ce7..b68c1c7 100644 --- 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 @@ -54,6 +54,10 @@ object EventsHandler { gameLevel = LevelGenerator.generateLevel(length, width) } + fun restartGame() { + gameLevel = null + } + /** * Выполняет игровое действие игрока на заданной позиции * по отношению к игровому объекту на заданной позиции. @@ -63,45 +67,54 @@ object EventsHandler { * @return данные об изменениях после игрового хода */ fun move(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { - val maze = gameLevel.maze - return if (maze[targetPos] == null) { - // ход игрока - maze[playerPos] = null - maze[targetPos] = Player() - - // результат хода игрового мира - val npcMove = gameLevel.npcEventProvider.move(targetPos, maze) - npcMove.forEach { maze[it.position] = it.newMazeObj } - val newPlayerPos = - if (maze[targetPos] != null) targetPos - else Position(-1, -1) - - GameMove(newPlayerPos, npcMove.map { - MazeEventData( - it.position, - it.newMazeObj?.getTypeIdentifier() ?: 0 - ) - }) + return if (gameLevel.maze[targetPos] == null) { + moveToFreePos(playerPos, targetPos, gameLevel) } else { - // результат хода игрока - val playerMove = maze[targetPos]!!.interact(interactionExecutor, targetPos) - playerMove.forEach { maze[it.position] = it.newMazeObj } - - // результат хода игрового мира - val npcMove = gameLevel.npcEventProvider.move(playerPos, maze) - npcMove.forEach { maze[it.position] = it.newMazeObj } - val newPlayerPos = - if (maze[playerPos] != null) playerPos - else Position(-1, -1) - - playerMove.forEach { npcMove.add(it) } - - GameMove(newPlayerPos, npcMove.map { - MazeEventData( - it.position, - it.newMazeObj?.getTypeIdentifier() ?: 0 - ) - }) + moveToOccupied(playerPos, targetPos, gameLevel) } } + + private fun moveToFreePos(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + val maze = gameLevel.maze + // ход игрока + maze[playerPos] = null + maze[targetPos] = Player() + + // результат хода игрового мира + val npcMove = gameLevel.npcEventProvider.move(targetPos, maze) + npcMove.forEach { maze[it.position] = it.newMazeObj } + val newPlayerPos = + if (maze[targetPos] != null) targetPos + else Position(-1, -1) + + return GameMove(newPlayerPos, npcMove.map { + MazeEventData( + it.position, + it.newMazeObj?.getTypeIdentifier() ?: 0 + ) + }) + } + + private fun moveToOccupied(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + val maze = gameLevel.maze + // результат хода игрока + val playerMove = maze[targetPos]!!.interact(interactionExecutor, targetPos) + playerMove.forEach { maze[it.position] = it.newMazeObj } + + // результат хода игрового мира + val npcMove = gameLevel.npcEventProvider.move(playerPos, maze) + npcMove.forEach { maze[it.position] = it.newMazeObj } + val newPlayerPos = + if (maze[playerPos] != null) playerPos + else Position(-1, -1) + + playerMove.forEach { npcMove.add(it) } + + return GameMove(newPlayerPos, npcMove.map { + MazeEventData( + it.position, + it.newMazeObj?.getTypeIdentifier() ?: 0 + ) + }) + } } From 7ea04c8fbd027d152c4afb5d1620dfd26ac09a7c Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Tue, 4 May 2021 17:11:01 +0500 Subject: [PATCH 18/30] client: stable version --- .../src/main/kotlin/ru/ifmo/sd/Main.kt | 21 +------ .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 57 ++++++++++--------- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 38 +++++++++++++ .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 33 ++++++++--- 4 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index dfdf47f..f127675 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -4,10 +4,9 @@ import io.ktor.client.* 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.http.* import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.stuff.GUI +import ru.ifmo.sd.stuff.ServerAPI import java.awt.EventQueue @@ -19,28 +18,14 @@ suspend fun main() { serializer = KotlinxSerializer() } } - val response = apiJoin() + ServerAPI.restart() // to clear all previous data + val response = ServerAPI.join() println(response) EventQueue.invokeLater { createAndShowGUI(response) } } -internal suspend fun apiJoin(): JoinGameInfo { - return client!!.post("http://localhost:8080/join") { - contentType(ContentType.Application.Json) - body = LevelConfiguration(length = 3, width = 3) - } -} - -internal suspend fun apiMove(oldPos: Position, newPos: Position): GameMove { - println("apiMove oldPos=$oldPos, newPos=$newPos") - return client!!.post("http://localhost:8080/move") { - contentType(ContentType.Application.Json) - body = PlayerMove(oldPos, newPos) - } -} - private fun createAndShowGUI(config: JoinGameInfo) { val frame = GUI("Roguelike", config) frame.isVisible = true 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 index b2e1411..4ce8d28 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -4,13 +4,10 @@ import kotlinx.coroutines.runBlocking import ru.ifmo.sd.client import ru.ifmo.sd.httpapi.models.JoinGameInfo import ru.ifmo.sd.httpapi.models.Position -import ru.ifmo.sd.apiMove -import ru.ifmo.sd.apiJoin import ru.ifmo.sd.stuff.ColoredSymbol.* import java.awt.BorderLayout import java.awt.Color.* import java.awt.Container -import java.awt.SystemColor.infoText import java.awt.event.KeyEvent import java.awt.event.KeyEvent.* import java.awt.event.KeyListener @@ -210,43 +207,51 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene this.mapPanel.repaint() this.mapTextPane.repaint() - runBlocking { -// if (currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) { -// // level ended -// val newConfig = apiJoin() -// map = SymbolMap(newConfig) -//// replaceSymbol(currPos, newPos) -// val prevPosSaved = prevPos -// currPos = newConfig.playerPos -// prevPos = prevPosSaved -// reloadMapTextPane() -// } else { - val gameMove = apiMove(prevPos!!, currPos) - println("gameMove=$gameMove") - if (gameMove.playerPosition == Position(-1, -1)) { - // player is dead - isDead = true - infoLabel.text = "You are dead!" - replacePaneSymbol(prevPos!!, NONE) + if ((currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) || + (currPos.row == 0 && currPos.column == 1)) { + // stepped away from the level + remakeMap() + } else { + val gameMove = ServerAPI.move(prevPos!!, currPos) + println("gameMove=$gameMove") + if (gameMove.playerPosition == Position(-1, -1)) { + // player is dead + isDead = true + infoLabel.text = "You are dead!" + replacePaneSymbol(prevPos!!, NONE) + } else { + // player moved + replaceSymbol(prevPos!!, gameMove.playerPosition) + map.applyDiff(gameMove.events) + if (map.enemyAmount == 0) { + remakeMap() } else { - // player moved - replaceSymbol(prevPos!!, gameMove.playerPosition) - map.applyDiff(gameMove.events) + // reload pane val prevPosSaved = prevPos currPos = gameMove.playerPosition prevPos = prevPosSaved reloadMapTextPane() } -// } + } } } } } + private fun remakeMap() { + ServerAPI.restart() // may break previous player's progress + val newConfig = ServerAPI.join() + map = SymbolMap(newConfig) + val prevPosSaved = prevPos + currPos = newConfig.playerPos + prevPos = prevPosSaved + reloadMapTextPane() + } + private fun checkMoveIsValid(oldPos: Position, newPos: Position): Boolean { return newPos.row < map.rowSize && newPos.row >= 0 && newPos.column < map.columnSize && newPos.column >= 0 && - (map.rows[newPos.row][newPos.column] == NONE || map.rows[newPos.row][newPos.column] == ENEMY) + map.rows[newPos.row][newPos.column] != WALL && map.rows[newPos.row][newPos.column] != PLAYER } private fun replaceSymbol(oldPos: Position, newPos: Position, replaceSymb: ColoredSymbol = NONE): Boolean { diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt new file mode 100644 index 0000000..a2936d8 --- /dev/null +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -0,0 +1,38 @@ +package ru.ifmo.sd.stuff + +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import ru.ifmo.sd.client +import ru.ifmo.sd.httpapi.models.* + +object ServerAPI { + const val address = "http://localhost:8080" + + internal fun join(): JoinGameInfo { + return runBlocking { + println("apiJoin") + return@runBlocking client!!.post("$address/join") { + contentType(ContentType.Application.Json) + body = LevelConfiguration(length = 4, width = 4) + } + } + } + + internal fun move(oldPos: Position, newPos: Position): GameMove { + return runBlocking { + println("apiMove oldPos=$oldPos, newPos=$newPos") + return@runBlocking client!!.post("$address/move") { + contentType(ContentType.Application.Json) + body = PlayerMove(oldPos, newPos) + } + } + } + + internal fun restart() { + runBlocking { + println("apiRestart") + client!!.get("$address/restart") + } + } +} \ 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 index d079c5d..94253f6 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -9,8 +9,10 @@ import java.awt.Color.* enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { WALL(0x2591.toChar(), GRAY), - PLAYER(0x267F.toChar()/*, RED*/), - ENEMY(0x2639.toChar()), + PLAYER(0x2689.toChar()/*, RED*/), + PASSIVE_ENEMY(0x2640.toChar()), + AGGRESSIVE_ENEMY(0x263F.toChar()), + COWARD_ENEMY(0x26B2.toChar()), NONE(' '), } @@ -18,27 +20,40 @@ class SymbolMap(config: JoinGameInfo) { val rows: List> = config.maze.levelMaze.map { arr -> arr.map { i -> mazeObjToSymbol(i) }.toMutableList() } - - init { - rows[config.playerPos.row][config.playerPos.column] = PLAYER - } - val rowSize: Int get() = rows.size val columnSize: Int get() = rows[0].size + var enemyAmount = config.maze.levelMaze.sumBy { arr -> arr.sumBy { i -> if (isEnemy(i)) 1 else 0 } } + private set + + init { + rows[config.playerPos.row][config.playerPos.column] = PLAYER + } private fun mazeObjToSymbol(mazeObj: Int): ColoredSymbol = when (mazeObj) { + 0 -> NONE 1 -> WALL 2 -> PLAYER - 3 -> ENEMY - else -> NONE + 3 -> PASSIVE_ENEMY + 4 -> AGGRESSIVE_ENEMY + 5 -> COWARD_ENEMY + else -> throw IllegalArgumentException("No such index in maze: $mazeObj") } internal fun applyDiff(events: List) { for (e in events) { val pos = e.position + + val wasEnemy = isEnemy(rows[pos.row][pos.column]) + val willBeEnemy = isEnemy(e.newMazeObj) + if (wasEnemy && !willBeEnemy) enemyAmount -= 1 + if (!wasEnemy && willBeEnemy) enemyAmount += 1 + rows[pos.row][pos.column] = mazeObjToSymbol(e.newMazeObj) } } + + private fun isEnemy(i: Int): Boolean = i == 3 || i == 4 || i == 5 + private fun isEnemy(i: ColoredSymbol): Boolean = i == PASSIVE_ENEMY || i == AGGRESSIVE_ENEMY || i == COWARD_ENEMY } \ No newline at end of file From a4d7fe779cc2bc0e01e24ec27849a7075bec7fbe Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Tue, 4 May 2021 17:51:51 +0500 Subject: [PATCH 19/30] client: added menu with restart; map resizes --- .../src/main/kotlin/ru/ifmo/sd/Main.kt | 2 +- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 24 ++++++++++++++++++- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 13 +++++++++- .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index f127675..cf9567d 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -12,7 +12,7 @@ import java.awt.EventQueue internal var client: HttpClient? = null -suspend fun main() { +fun main() { client = HttpClient(CIO) { install(JsonFeature) { serializer = KotlinxSerializer() 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 index 4ce8d28..af14cd0 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -45,6 +45,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene setTitle(title) createLayout() + createMenuBar() makeNotFocusable() defaultCloseOperation = EXIT_ON_CLOSE @@ -103,6 +104,23 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene // infoPanel.add(hpLabel, BorderLayout.WEST) } + private fun createMenuBar() { + val menubar = JMenuBar() + + val file = JMenu("Menu") +// file.mnemonic = KeyEvent.VK_ESCAPE + + val eMenuItem = JMenuItem("Restart") +// eMenuItem.mnemonic = KeyEvent.VK_R + eMenuItem.toolTipText = "Restart game" + eMenuItem.addActionListener { remakeMap(restart = true) } + + file.add(eMenuItem) + menubar.add(file) + + jMenuBar = menubar + } + private fun makeNotFocusable(container: Container = this) { container.isFocusable = false for (c in container.components) { @@ -224,6 +242,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene replaceSymbol(prevPos!!, gameMove.playerPosition) map.applyDiff(gameMove.events) if (map.enemyAmount == 0) { + ServerAPI.increaseMapSize() remakeMap() } else { // reload pane @@ -238,8 +257,11 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } } - private fun remakeMap() { + private fun remakeMap(restart: Boolean = false) { ServerAPI.restart() // may break previous player's progress + if (restart) { + ServerAPI.resetMapSize() + } val newConfig = ServerAPI.join() map = SymbolMap(newConfig) val prevPosSaved = prevPos diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt index a2936d8..a0c0781 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -8,13 +8,24 @@ import ru.ifmo.sd.httpapi.models.* object ServerAPI { const val address = "http://localhost:8080" + private var mapSize = Pair(4, 4) + + internal fun increaseMapSize() { + if (mapSize.first < 9) { + mapSize = Pair(mapSize.first + 1, mapSize.second + 2) + } + } + + internal fun resetMapSize() { + mapSize = Pair(4, 4) + } internal fun join(): JoinGameInfo { return runBlocking { println("apiJoin") return@runBlocking client!!.post("$address/join") { contentType(ContentType.Application.Json) - body = LevelConfiguration(length = 4, width = 4) + body = LevelConfiguration(length = mapSize.first, width = mapSize.second) } } } 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 index 94253f6..716f439 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -12,7 +12,7 @@ enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { PLAYER(0x2689.toChar()/*, RED*/), PASSIVE_ENEMY(0x2640.toChar()), AGGRESSIVE_ENEMY(0x263F.toChar()), - COWARD_ENEMY(0x26B2.toChar()), + COWARD_ENEMY(0x2649.toChar()), NONE(' '), } From b0a3da5e63a4a1fc1bc10d39eec898feeec4af0d Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Thu, 6 May 2021 10:33:58 +0300 Subject: [PATCH 20/30] improve npc movement algorithm --- .../sd/world/generation/LevelGenerator.kt | 2 +- .../ifmo/sd/world/npc/strategy/Aggressive.kt | 32 +++++++++--------- .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 33 ++++++++++++++++--- 3 files changed, 47 insertions(+), 20 deletions(-) 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 index 78fa577..409f074 100644 --- 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 @@ -87,7 +87,7 @@ class LevelGenerator { while (true) { i = 0 /* - Если текущаяя позиция в границах игрового поля, и через клетку не свободно, + Если текущая позиция в границах игрового поля, и через клетку не свободно, иначе цикл, то пробуриваемся в текущем направлении. */ if (curRow > 1 && level[curRow - 2][curColumn] != 0) { diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt index 0cc6e80..a84f924 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Aggressive.kt @@ -11,7 +11,7 @@ import kotlin.math.abs /** - * Класс, отвечащий агрессивной стратегии поведения NPC. + * Класс, отвечающий агрессивной стратегии поведения NPC. */ class Aggressive : Strategy { private fun isAdjacent(npcPos: Position, playerPos: Position): Boolean { @@ -29,27 +29,29 @@ class Aggressive : Strategy { HashSet() else -> if (isAdjacent(npc.position, playerPos)) { - EventsHandler - .gameLevel!! - .maze[playerPos]!! - .interact(EventsHandler.interactionExecutor, playerPos) + maze[playerPos]!!.interact(EventsHandler.interactionExecutor, playerPos) } else { - val oldNpcPos = npc.position - npc.position = npc.position + getDirection(direction) - mutableSetOf( - ChangeMazePositionEvent(oldNpcPos, null), - ChangeMazePositionEvent(npc.position, getEnemyFactory().getEnemy()) - ) + val directionsToMove = getDirection(direction) + val randomPos = Strategy.randomDirection(npc.position, directionsToMove, maze) + if (randomPos != Position(0, 0)) { + val oldNpcPos = npc.position + npc.position = npc.position + randomPos + mutableSetOf( + ChangeMazePositionEvent(oldNpcPos, null), + ChangeMazePositionEvent(npc.position, getEnemyFactory().getEnemy()) + ) + } else { + HashSet() + } } } - - private fun getDirection(direction: PlayerDirection): Position = - when (direction) { + private fun getDirection(direction: PlayerDirection): List = + listOf(when (direction) { PlayerDirection.North -> Position(-1, 0) PlayerDirection.South -> Position(1, 0) PlayerDirection.West -> Position(0, 1) PlayerDirection.East -> Position(0, -1) PlayerDirection.Failed -> Position(0, 0) - } + }) } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt index 2e563d5..9cac293 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -3,10 +3,11 @@ package ru.ifmo.sd.world.npc.strategy import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.events.ChangeMazePositionEvent import ru.ifmo.sd.world.npc.Npc -import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.Failed +import ru.ifmo.sd.world.npc.strategy.Strategy.Companion.PlayerDirection.* import ru.ifmo.sd.world.representation.Maze import ru.ifmo.sd.world.representation.units.EnemyFactory import kotlin.math.abs +import kotlin.math.atan2 /** * Интерфейс стратегии поведения NPC. @@ -62,7 +63,9 @@ interface Strategy { if (dx >= dy) { while (true) { - if (!checkVisibility(Position(x, y), maze)) { + if ((x != npcPos.row || y != npcPos.column) + && (x != playerPos.row || y != playerPos.column) + && !checkVisibility(Position(x, y), maze)) { return Failed } if (x == playerPos.row) break @@ -75,7 +78,9 @@ interface Strategy { } } else { while (true) { - if (!checkVisibility(Position(x, y), maze)) { + if ((x != npcPos.row || y != npcPos.column) + && (x != playerPos.row || y != playerPos.column) + && !checkVisibility(Position(x, y), maze)) { return Failed } if (y == playerPos.column) break @@ -88,7 +93,27 @@ interface Strategy { } } - return Failed + return getDirection(npcPos, playerPos) + } + + private fun getDirection(npcPos: Position, playerPos: Position): PlayerDirection { + var rad = atan2((playerPos.row - npcPos.row).toDouble(), (playerPos.column - npcPos.column).toDouble()); + if (rad < 0) rad += (2 * Math.PI) + val deg = rad * (180 / Math.PI) + return when (deg) { + in 45.0..135.0 -> { + South + } + in 135.0..225.0 -> { + East + } + in 225.0..315.0 -> { + North + } + else -> { + West + } + } } private fun checkVisibility(pos: Position, maze: Maze): Boolean { From dce81501b5f517af50eabe40a5a36ef9f2271a3c Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Fri, 7 May 2021 09:57:07 +0300 Subject: [PATCH 21/30] fix comments --- .../ru/ifmo/sd/world/events/EventsHandler.kt | 30 +++++++++++++------ .../world/events/InteractionExecutorImpl.kt | 2 +- .../sd/world/generation/LevelGenerator.kt | 3 +- .../representation/UnitsHealthStorage.kt | 6 ++-- 4 files changed, 26 insertions(+), 15 deletions(-) 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 index b68c1c7..17da8cd 100644 --- 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 @@ -3,7 +3,6 @@ package ru.ifmo.sd.world.events import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.world.generation.LevelGenerator import ru.ifmo.sd.world.representation.GameLevel -import ru.ifmo.sd.world.representation.units.MazeObject import ru.ifmo.sd.world.representation.units.Player import java.util.stream.Collectors @@ -33,17 +32,23 @@ object EventsHandler { return JoinGameInfo( playerPos, MazeData(getMazeData()), - gameLevel!!.unitsHealthStorage.getHealths().entries.stream().map { e -> Pair(e.key, e.value) } + gameLevel!!.unitsHealthStorage + .getHealthsDictionary() + .entries + .stream() + .map { e -> Pair(e.key, e.value) } .collect(Collectors.toList()) ) } private fun getMazeData(): Array> { val maze = gameLevel!!.maze - val mazeData: Array> = Array(maze.levelMaze.size) { Array(maze.levelMaze[0].size) { 0 } } + val mazeData = Array(maze.levelMaze.size) { + Array(maze.levelMaze[0].size) { 0 } + } for (i in mazeData.indices) { for (j in mazeData[0].indices) { - val mazeObj: MazeObject? = maze[Position(i, j)] + val mazeObj = maze[Position(i, j)] mazeData[i][j] = mazeObj?.getTypeIdentifier() ?: 0 } } @@ -70,23 +75,30 @@ object EventsHandler { return if (gameLevel.maze[targetPos] == null) { moveToFreePos(playerPos, targetPos, gameLevel) } else { - moveToOccupied(playerPos, targetPos, gameLevel) + moveToOccupiedPos(playerPos, targetPos, gameLevel) } } private fun moveToFreePos(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { val maze = gameLevel.maze + val healths = gameLevel.unitsHealthStorage // ход игрока maze[playerPos] = null + val playerHealth = healths.eliminateUnit(playerPos)!! maze[targetPos] = Player() + healths.addUnit(targetPos, playerHealth) // результат хода игрового мира val npcMove = gameLevel.npcEventProvider.move(targetPos, maze) npcMove.forEach { maze[it.position] = it.newMazeObj } val newPlayerPos = - if (maze[targetPos] != null) targetPos - else Position(-1, -1) - + if (maze[targetPos] != null){ + targetPos + } + else { + healths.eliminateUnit(targetPos) + Position(-1, -1) + } return GameMove(newPlayerPos, npcMove.map { MazeEventData( it.position, @@ -95,7 +107,7 @@ object EventsHandler { }) } - private fun moveToOccupied(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + private fun moveToOccupiedPos(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { val maze = gameLevel.maze // результат хода игрока val playerMove = maze[targetPos]!!.interact(interactionExecutor, targetPos) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt index 8bd6fdd..695c374 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt @@ -27,7 +27,7 @@ interface InteractionExecutor { fun doFor(obj: Enemy, objPos: Position): MutableSet /** - * Выполнеяет взаимодействие с игроком. + * Выполняет взаимодействие с игроком. * * @param obj -- тип объекта * @param objPos -- позиция объекта 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 index 409f074..1d596ba 100644 --- 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 @@ -4,7 +4,6 @@ import ru.ifmo.sd.httpapi.models.Position import ru.ifmo.sd.world.npc.NpcEventProvider import ru.ifmo.sd.world.npc.strategy.* import ru.ifmo.sd.world.representation.* -import ru.ifmo.sd.world.representation.units.Enemy import ru.ifmo.sd.world.representation.units.Wall import kotlin.properties.Delegates import kotlin.random.Random @@ -35,7 +34,7 @@ class LevelGenerator { */ 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 } } // Позиция входа в лабиринт 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 index 2d7ec00..1ff69cd 100644 --- 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 @@ -14,7 +14,7 @@ class UnitsHealthStorage { * * @return словарь с жизненными силами юнитов */ - fun getHealths(): Map { + fun getHealthsDictionary(): Map { return healths } @@ -34,8 +34,8 @@ class UnitsHealthStorage { * * @param pos -- позиция юнита */ - fun eliminateUnit(pos: Position) { - healths.remove(pos) + fun eliminateUnit(pos: Position): Int? { + return healths.remove(pos) } /** From 764dc8fe943cabb1d22fcdfbcf321fb0fe58bdbf Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Tue, 25 May 2021 12:23:39 +0500 Subject: [PATCH 22/30] some fixes and changes --- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 36 ++++++++++++++----- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 14 ++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) 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 index af14cd0..175c345 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -1,6 +1,5 @@ package ru.ifmo.sd.stuff -import kotlinx.coroutines.runBlocking import ru.ifmo.sd.client import ru.ifmo.sd.httpapi.models.JoinGameInfo import ru.ifmo.sd.httpapi.models.Position @@ -35,6 +34,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene private val infoLabel = JLabel("") private var map = SymbolMap(gameConfiguration) private var mapTextPane = MapTextPane() + private var isMultiplayer = false init { createUI(title) @@ -106,17 +106,37 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene private fun createMenuBar() { val menubar = JMenuBar() - - val file = JMenu("Menu") + val menu = JMenu("Menu") // file.mnemonic = KeyEvent.VK_ESCAPE - val eMenuItem = JMenuItem("Restart") + val multiplayerItem = if (isMultiplayer) { + val item = JMenuItem("Exit multiplayer") + item.addActionListener { + ServerAPI.exitMultiplayer() + isMultiplayer = false + } + item + } else { + val item = JMenuItem("Join multiplayer") + item.addActionListener { + ServerAPI.joinMultiplayer() + isMultiplayer = true + } + item + } + + val restartItem = JMenuItem("Restart") // eMenuItem.mnemonic = KeyEvent.VK_R - eMenuItem.toolTipText = "Restart game" - eMenuItem.addActionListener { remakeMap(restart = true) } + restartItem.toolTipText = "Restart game" + restartItem.addActionListener { + isDead = false + infoLabel.text = "" + remakeMap(restart = true) + } - file.add(eMenuItem) - menubar.add(file) + menu.add(multiplayerItem) + menu.add(restartItem) + menubar.add(menu) jMenuBar = menubar } diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt index a0c0781..b206db8 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -46,4 +46,18 @@ object ServerAPI { client!!.get("$address/restart") } } + + internal fun joinMultiplayer() { + runBlocking { + println("apiJoinMultiplayer") + client!!.get("$address/join_multiplayer") + } + } + + internal fun exitMultiplayer() { + runBlocking { + println("apiExitMultiplayer") + client!!.get("$address/exit_multiplayer") + } + } } \ No newline at end of file From aeae5070a17c9b75911697d3f9e01f80e972f43a Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Tue, 25 May 2021 14:17:12 +0300 Subject: [PATCH 23/30] review fixes --- .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 11 +++-- .../sd/world/errors/GameServerException.kt | 3 ++ .../ru/ifmo/sd/world/events/EventsHandler.kt | 2 +- .../world/events/InteractionExecutorImpl.kt | 7 ++- .../ru/ifmo/sd/world/npc/strategy/Coward.kt | 49 +++++++++++-------- .../ru/ifmo/sd/world/npc/strategy/Passive.kt | 12 +++-- .../ru/ifmo/sd/world/npc/strategy/Strategy.kt | 3 +- 7 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/world/errors/GameServerException.kt 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 index 737fd27..93e89c9 100644 --- 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 @@ -7,6 +7,7 @@ import io.ktor.response.* import io.ktor.routing.* import ru.ifmo.sd.httpapi.models.LevelConfiguration import ru.ifmo.sd.httpapi.models.PlayerMove +import ru.ifmo.sd.world.errors.GameServerException import ru.ifmo.sd.world.events.EventsHandler fun Route.gameRouting() { @@ -19,13 +20,17 @@ fun Route.gameRouting() { post("/move") { val playerMove = call.receive() - val move = EventsHandler.move(playerMove.oldPosition, playerMove.newPosition, EventsHandler.gameLevel!!) + val move = try { + EventsHandler.move(playerMove.oldPosition, playerMove.newPosition, EventsHandler.gameLevel!!) + } catch (e: GameServerException) { + call.respond(message = e.localizedMessage, status = HttpStatusCode.BadRequest) + } call.respond(message = move, status = HttpStatusCode.Accepted) } get("/restart") { - EventsHandler.restartGame() - call.respond( HttpStatusCode.Accepted, "Game was successfully restarted") + EventsHandler.closeGame() + call.respond(HttpStatusCode.Accepted, "Game was successfully restarted") } } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/errors/GameServerException.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/errors/GameServerException.kt new file mode 100644 index 0000000..7a5c201 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/errors/GameServerException.kt @@ -0,0 +1,3 @@ +package ru.ifmo.sd.world.errors + +class GameServerException(message: String): Exception(message) 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 index 17da8cd..43e2d32 100644 --- 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 @@ -59,7 +59,7 @@ object EventsHandler { gameLevel = LevelGenerator.generateLevel(length, width) } - fun restartGame() { + fun closeGame() { gameLevel = null } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt index 695c374..a35f900 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/events/InteractionExecutorImpl.kt @@ -1,6 +1,7 @@ package ru.ifmo.sd.world.events import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.world.errors.GameServerException import ru.ifmo.sd.world.representation.units.* @@ -47,7 +48,8 @@ class InteractionExecutorImpl : InteractionExecutor { } override fun doFor(obj: Enemy, objPos: Position): MutableSet { - val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage + val unitsHealthStorage = EventsHandler.gameLevel?.unitsHealthStorage + ?: throw GameServerException("Game level is not initialized!") unitsHealthStorage.decrease(objPos, playerDamage) return if (!unitsHealthStorage.isAlive(objPos)) { unitsHealthStorage.eliminateUnit(objPos) @@ -59,7 +61,8 @@ class InteractionExecutorImpl : InteractionExecutor { } override fun doFor(obj: Player, objPos: Position): MutableSet { - val unitsHealthStorage = EventsHandler.gameLevel!!.unitsHealthStorage + val unitsHealthStorage = EventsHandler.gameLevel?.unitsHealthStorage + ?: throw GameServerException("Game level is not initialized!") unitsHealthStorage.decrease(objPos, npcDamage) return if (!unitsHealthStorage.isAlive(objPos)) { unitsHealthStorage.eliminateUnit(objPos) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt index 49b4776..562ac2e 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Coward.kt @@ -8,9 +8,32 @@ import ru.ifmo.sd.world.representation.Maze import ru.ifmo.sd.world.representation.units.* /** - * Класс, отвечащий трусливой стратегии поведения NPC. + * Класс, отвечающий трусливой стратегии поведения NPC. */ class Coward : Strategy { + companion object { + private val north = listOf( + Position(1, 0), + Position(0, 1), + Position(0, -1) + ) + private val south = listOf( + Position(-1, 0), + Position(0, 1), + Position(0, -1) + ) + private val west = listOf( + Position(1, 0), + Position(0, -1), + Position(-1, 0) + ) + private val east = listOf( + Position(1, 0), + Position(0, 1), + Position(1, 0) + ) + } + override fun getEnemyFactory(): EnemyFactory { return CowardEnemyFactory() } @@ -36,26 +59,10 @@ class Coward : Strategy { private fun getDirections(direction: PlayerDirection): List = when (direction) { - PlayerDirection.North -> listOf( - Position(1, 0), - Position(0, 1), - Position(0, -1) - ) - PlayerDirection.South -> listOf( - Position(-1, 0), - Position(0, 1), - Position(0, -1) - ) - PlayerDirection.West -> listOf( - Position(1, 0), - Position(0, -1), - Position(-1, 0) - ) - PlayerDirection.East -> listOf( - Position(1, 0), - Position(0, 1), - Position(1, 0) - ) + PlayerDirection.North -> north + PlayerDirection.South -> south + PlayerDirection.West -> west + PlayerDirection.East -> east PlayerDirection.Failed -> emptyList() } } diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt index fb01522..30eaa70 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Passive.kt @@ -11,17 +11,19 @@ import ru.ifmo.sd.world.representation.units.PassiveEnemyFactory * Класс, отвечающий пассивной стратегии поведения NPC. */ class Passive : Strategy { + companion object { + private val directionsToMove = listOf( + Position(0, 1), Position(0, -1), + Position(1, 0), Position(-1, 0) + ) + } + override fun getEnemyFactory(): EnemyFactory { return PassiveEnemyFactory() } override fun execute(npc: Npc, playerPos: Position, maze: Maze): MutableSet { - val directionsToMove = listOf( - Position(0, 1), Position(0, -1), - Position(1, 0), Position(-1, 0) - ) val randomMove = Strategy.randomDirection(npc.position, directionsToMove, maze) - return if (randomMove != Position(0, 0)) { val oldNpcPos = npc.position npc.position = npc.position + randomMove diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt index 9cac293..7824321 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/strategy/Strategy.kt @@ -99,8 +99,7 @@ interface Strategy { private fun getDirection(npcPos: Position, playerPos: Position): PlayerDirection { var rad = atan2((playerPos.row - npcPos.row).toDouble(), (playerPos.column - npcPos.column).toDouble()); if (rad < 0) rad += (2 * Math.PI) - val deg = rad * (180 / Math.PI) - return when (deg) { + return when (rad * (180 / Math.PI)) { in 45.0..135.0 -> { South } From 21cad626e84ec516df07fbe3238d9b7d0183bfc2 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Tue, 25 May 2021 21:31:33 +0300 Subject: [PATCH 24/30] add multiplayer supporting --- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 4 +- .../ru/ifmo/sd/httpapi/models/GameState.kt | 10 ++ .../models/{PlayerMove.kt => PlayerInfo.kt} | 5 +- .../httpapi/models/PlayerPositionChanging.kt | 10 ++ .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 50 ++++++-- .../ru/ifmo/sd/world/events/EventsHandler.kt | 115 ++++++++++++++---- 6 files changed, 156 insertions(+), 38 deletions(-) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt rename game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/{PlayerMove.kt => PlayerInfo.kt} (54%) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerPositionChanging.kt diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt index a0c0781..0a1ae4a 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -35,7 +35,7 @@ object ServerAPI { println("apiMove oldPos=$oldPos, newPos=$newPos") return@runBlocking client!!.post("$address/move") { contentType(ContentType.Application.Json) - body = PlayerMove(oldPos, newPos) + body = PlayerPositionChanging(oldPos, newPos) } } } @@ -46,4 +46,4 @@ object ServerAPI { client!!.get("$address/restart") } } -} \ No newline at end of file +} diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt new file mode 100644 index 0000000..ff89c11 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt @@ -0,0 +1,10 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class GameState( + val currentMovePlayerName: String, + val maze: MazeData, + val unitsHealth: List> +) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerInfo.kt similarity index 54% rename from game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt rename to game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerInfo.kt index 77a995a..80409a9 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerMove.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerInfo.kt @@ -3,4 +3,7 @@ package ru.ifmo.sd.httpapi.models import kotlinx.serialization.Serializable @Serializable -data class PlayerMove(val oldPosition: Position, val newPosition: Position) +data class PlayerInfo( + val name: String, + val position: Position +) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerPositionChanging.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerPositionChanging.kt new file mode 100644 index 0000000..fbc6bc8 --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/PlayerPositionChanging.kt @@ -0,0 +1,10 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class PlayerPositionChanging( + val name: String, + val oldPosition: Position, + val newPosition: 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 index 93e89c9..887b337 100644 --- 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 @@ -5,33 +5,61 @@ 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.PlayerMove +import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.world.errors.GameServerException import ru.ifmo.sd.world.events.EventsHandler fun Route.gameRouting() { route("/") { - post("/join") { + post("/start") { val levelConfiguration = call.receive() - val startedGame = EventsHandler.join(levelConfiguration) + EventsHandler.startGame(levelConfiguration) + } + + get("/gameState") { + val gameState = try { + EventsHandler.getActualGameState() + } catch (e: GameServerException) { + call.respond(HttpStatusCode.BadRequest, e.localizedMessage) + } + call.respond(HttpStatusCode.Accepted, gameState) + } + + get("/join") { + val newPlayerName = call.request.queryParameters["playerName"] + if (newPlayerName == null) { + call.respond(HttpStatusCode.BadRequest, "Could not get player name.") + } + val startedGame = try { + EventsHandler.join(newPlayerName!!) + } catch (e: GameServerException) { + call.respond(message = e.localizedMessage, status = HttpStatusCode.BadRequest) + } + call.respond(message = startedGame, status = HttpStatusCode.Created) + } + + post("/disconnect") { + val playerInfo = call.receive() + val startedGame = try { + EventsHandler.disconnect(playerInfo.name, playerInfo.position) + } catch (e: GameServerException) { + call.respond(message = e.localizedMessage, status = HttpStatusCode.BadRequest) + } call.respond(message = startedGame, status = HttpStatusCode.Created) } post("/move") { - val playerMove = call.receive() + val playerMove = call.receive() val move = try { - EventsHandler.move(playerMove.oldPosition, playerMove.newPosition, EventsHandler.gameLevel!!) + EventsHandler.move( + playerMove.name, playerMove.oldPosition, + playerMove.newPosition, EventsHandler.gameLevel!! + ) } catch (e: GameServerException) { call.respond(message = e.localizedMessage, status = HttpStatusCode.BadRequest) } call.respond(message = move, status = HttpStatusCode.Accepted) } - - get("/restart") { - EventsHandler.closeGame() - call.respond(HttpStatusCode.Accepted, "Game was successfully restarted") - } } } 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 index 43e2d32..eba509d 100644 --- 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 @@ -1,9 +1,11 @@ package ru.ifmo.sd.world.events import ru.ifmo.sd.httpapi.models.* +import ru.ifmo.sd.world.errors.GameServerException import ru.ifmo.sd.world.generation.LevelGenerator -import ru.ifmo.sd.world.representation.GameLevel +import ru.ifmo.sd.world.representation.* import ru.ifmo.sd.world.representation.units.Player +import java.util.* import java.util.stream.Collectors @@ -13,36 +15,34 @@ import java.util.stream.Collectors object EventsHandler { val interactionExecutor = InteractionExecutorImpl() var gameLevel: GameLevel? = null + var playersQueue: Queue = LinkedList() /** * Присоединяет нового игрока к текущей игровой сессии, если таковая есть, или создает новую. * - * @param levelConfiguration -- конфигурация игрового уровня + * @param playerName -- имя нового игрока * @return начальную информацию об игровом уровне */ - fun join(levelConfiguration: LevelConfiguration): JoinGameInfo { + fun join(playerName: String): JoinGameInfo { + if (playersQueue.contains(playerName)) { + throw GameServerException("Player with $playerName name already exists.") + } if (gameLevel == null) { - init(levelConfiguration.length, levelConfiguration.width) + throw GameServerException("Game level has not been initialized yet.") } - val maze = gameLevel!!.maze - val playerPos = maze.freePos.random() - gameLevel!!.unitsHealthStorage.addUnit(playerPos) - maze[playerPos] = Player() + val newPlayerPos = maze.freePos.random() + gameLevel!!.unitsHealthStorage.addUnit(newPlayerPos) + maze[newPlayerPos] = Player() + playersQueue.add(playerName) return JoinGameInfo( - playerPos, MazeData(getMazeData()), - gameLevel!!.unitsHealthStorage - .getHealthsDictionary() - .entries - .stream() - .map { e -> Pair(e.key, e.value) } - .collect(Collectors.toList()) + newPlayerPos, MazeData(getMazeData(gameLevel!!.maze)), + getHealthsData(gameLevel!!.unitsHealthStorage) ) } - private fun getMazeData(): Array> { - val maze = gameLevel!!.maze + private fun getMazeData(maze: Maze): Array> { val mazeData = Array(maze.levelMaze.size) { Array(maze.levelMaze[0].size) { 0 } } @@ -55,23 +55,91 @@ object EventsHandler { return mazeData } - private fun init(length: Int, width: Int) { - gameLevel = LevelGenerator.generateLevel(length, width) + private fun getHealthsData(unitsHealthStorage: UnitsHealthStorage): List> { + return unitsHealthStorage + .getHealthsDictionary() + .entries + .stream() + .map { e -> Pair(e.key, e.value) } + .collect(Collectors.toList()) + } + + /** + * Отсоединяет указанного игрока от игры. + * @param playerName -- имя игрока для отсоединения + * @param playerPos -- позиция игрока для отсоединения + */ + fun disconnect(playerName: String, playerPos: Position) { + playersQueue.remove(playerName) + if (playersQueue.isEmpty()) { + closeGame() + } + if (gameLevel == null) { + throw GameServerException("Game already finished.") + } + val healths = gameLevel!!.unitsHealthStorage + healths.eliminateUnit(playerPos) + val maze = gameLevel!!.maze + maze[playerPos] = null + } + + /** + * Возвращает актуальное состояние игры. + * + * @return актуальное состояние игры + */ + fun getActualGameState(): GameState { + if (gameLevel == null) { + throw GameServerException("Game level has not been initialized yet.") + } + if (playersQueue.isEmpty()) { + throw GameServerException("No player connected on level.") + } + return GameState( + playersQueue.peek(), MazeData(getMazeData(gameLevel!!.maze)), + getHealthsData(gameLevel!!.unitsHealthStorage) + ) + } + + /** + * Создает новую игровую сессию. + * + * @param levelConfiguration -- конфигурация игрового уровня + */ + fun startGame(levelConfiguration: LevelConfiguration) { + if (gameLevel == null) { + gameLevel = LevelGenerator.generateLevel( + levelConfiguration.length, + levelConfiguration.width + ) + } } - fun closeGame() { + private fun closeGame() { gameLevel = null + playersQueue.clear() } /** * Выполняет игровое действие игрока на заданной позиции * по отношению к игровому объекту на заданной позиции. * + * @param playerName -- имя игрока * @param playerPos -- позиция игрока * @param targetPos -- позиция игрового объекта * @return данные об изменениях после игрового хода */ - fun move(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + fun move( + playerName: String, playerPos: Position, + targetPos: Position, gameLevel: GameLevel + ): GameMove { + if (!playerName.contentEquals(playersQueue.peek())) { + throw GameServerException("Player with name $playerName cannot move because it is not his time to move.") + } + if (playersQueue.isEmpty()) { + throw GameServerException("No player connected on level.") + } + playersQueue.add(playersQueue.poll()) return if (gameLevel.maze[targetPos] == null) { moveToFreePos(playerPos, targetPos, gameLevel) } else { @@ -92,10 +160,9 @@ object EventsHandler { val npcMove = gameLevel.npcEventProvider.move(targetPos, maze) npcMove.forEach { maze[it.position] = it.newMazeObj } val newPlayerPos = - if (maze[targetPos] != null){ + if (maze[targetPos] != null) { targetPos - } - else { + } else { healths.eliminateUnit(targetPos) Position(-1, -1) } From 9e57ff45da0293c2a5a3ea0f3bc231cfdc1f09f7 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Wed, 26 May 2021 00:29:52 +0300 Subject: [PATCH 25/30] update server api --- .../ru/ifmo/sd/httpapi/models/GameState.kt | 1 + .../ru/ifmo/sd/httpapi/models/JoinInfo.kt | 10 ++++ .../ru/ifmo/sd/httpapi/routes/GameRoutes.kt | 29 +++++------ .../ru/ifmo/sd/world/events/EventsHandler.kt | 52 ++++++++++++------- 4 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinInfo.kt diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt index ff89c11..193203f 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class GameState( val currentMovePlayerName: String, + val isAlive: Boolean, val maze: MazeData, val unitsHealth: List> ) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinInfo.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinInfo.kt new file mode 100644 index 0000000..3d7757c --- /dev/null +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinInfo.kt @@ -0,0 +1,10 @@ +package ru.ifmo.sd.httpapi.models + +import kotlinx.serialization.Serializable + +@Serializable +data class JoinInfo( + val playerName: String, + val length: Int?, + val width: Int? +) 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 index 887b337..5250b01 100644 --- 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 @@ -11,27 +11,24 @@ import ru.ifmo.sd.world.events.EventsHandler fun Route.gameRouting() { route("/") { - post("/start") { - val levelConfiguration = call.receive() - EventsHandler.startGame(levelConfiguration) - } - get("/gameState") { - val gameState = try { - EventsHandler.getActualGameState() - } catch (e: GameServerException) { - call.respond(HttpStatusCode.BadRequest, e.localizedMessage) + val playerName = call.request.queryParameters["playerName"] + if (playerName == null) { + call.respond(HttpStatusCode.BadRequest, "Cannot get game state.") + } else { + val gameState = try { + EventsHandler.getActualGameState(playerName) + } catch (e: GameServerException) { + call.respond(HttpStatusCode.BadRequest, e.localizedMessage) + } + call.respond(HttpStatusCode.Accepted, gameState) } - call.respond(HttpStatusCode.Accepted, gameState) } - get("/join") { - val newPlayerName = call.request.queryParameters["playerName"] - if (newPlayerName == null) { - call.respond(HttpStatusCode.BadRequest, "Could not get player name.") - } + post("/join") { + val joinInfo = call.receive() val startedGame = try { - EventsHandler.join(newPlayerName!!) + EventsHandler.join(joinInfo.playerName, joinInfo.length, joinInfo.width) } catch (e: GameServerException) { call.respond(message = e.localizedMessage, status = HttpStatusCode.BadRequest) } 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 index eba509d..e827d4a 100644 --- 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 @@ -21,14 +21,16 @@ object EventsHandler { * Присоединяет нового игрока к текущей игровой сессии, если таковая есть, или создает новую. * * @param playerName -- имя нового игрока + * @param length -- длина игрового уровня + * @param width -- ширина игрового уровня * @return начальную информацию об игровом уровне */ - fun join(playerName: String): JoinGameInfo { + fun join(playerName: String, length: Int?, width: Int?): JoinGameInfo { if (playersQueue.contains(playerName)) { throw GameServerException("Player with $playerName name already exists.") } if (gameLevel == null) { - throw GameServerException("Game level has not been initialized yet.") + startGame(length, width) } val maze = gameLevel!!.maze val newPlayerPos = maze.freePos.random() @@ -88,7 +90,7 @@ object EventsHandler { * * @return актуальное состояние игры */ - fun getActualGameState(): GameState { + fun getActualGameState(playerName: String): GameState { if (gameLevel == null) { throw GameServerException("Game level has not been initialized yet.") } @@ -96,21 +98,23 @@ object EventsHandler { throw GameServerException("No player connected on level.") } return GameState( - playersQueue.peek(), MazeData(getMazeData(gameLevel!!.maze)), + playersQueue.peek(), playersQueue.contains(playerName), + MazeData(getMazeData(gameLevel!!.maze)), getHealthsData(gameLevel!!.unitsHealthStorage) ) } - /** - * Создает новую игровую сессию. - * - * @param levelConfiguration -- конфигурация игрового уровня - */ - fun startGame(levelConfiguration: LevelConfiguration) { - if (gameLevel == null) { - gameLevel = LevelGenerator.generateLevel( - levelConfiguration.length, - levelConfiguration.width + private fun startGame(length: Int?, width: Int?) { + gameLevel = if (length == null && width == null) { + LevelGenerator.generateLevel() + } else if (length == null) { + LevelGenerator.generateLevel(width!!) + } else if (width == null) { + LevelGenerator.generateLevel(length) + } else { + LevelGenerator.generateLevel( + length, + width ) } } @@ -141,13 +145,16 @@ object EventsHandler { } playersQueue.add(playersQueue.poll()) return if (gameLevel.maze[targetPos] == null) { - moveToFreePos(playerPos, targetPos, gameLevel) + moveToFreePos(playerName, playerPos, targetPos, gameLevel) } else { - moveToOccupiedPos(playerPos, targetPos, gameLevel) + moveToOccupiedPos(playerName, playerPos, targetPos, gameLevel) } } - private fun moveToFreePos(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + private fun moveToFreePos( + playerName: String, playerPos: Position, + targetPos: Position, gameLevel: GameLevel + ): GameMove { val maze = gameLevel.maze val healths = gameLevel.unitsHealthStorage // ход игрока @@ -164,6 +171,7 @@ object EventsHandler { targetPos } else { healths.eliminateUnit(targetPos) + playersQueue.remove(playerName) Position(-1, -1) } return GameMove(newPlayerPos, npcMove.map { @@ -174,7 +182,10 @@ object EventsHandler { }) } - private fun moveToOccupiedPos(playerPos: Position, targetPos: Position, gameLevel: GameLevel): GameMove { + private fun moveToOccupiedPos( + playerName: String, playerPos: Position, + targetPos: Position, gameLevel: GameLevel + ): GameMove { val maze = gameLevel.maze // результат хода игрока val playerMove = maze[targetPos]!!.interact(interactionExecutor, targetPos) @@ -185,7 +196,10 @@ object EventsHandler { npcMove.forEach { maze[it.position] = it.newMazeObj } val newPlayerPos = if (maze[playerPos] != null) playerPos - else Position(-1, -1) + else { + playersQueue.remove(playerName) + Position(-1, -1) + } playerMove.forEach { npcMove.add(it) } From 100c2f6b8674ae164f6a15f6f19f239c1fcbc2fe Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Wed, 26 May 2021 03:19:12 +0500 Subject: [PATCH 26/30] can play in multiplayer --- .../src/main/kotlin/ru/ifmo/sd/Main.kt | 38 ++++- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 114 +++++++++++-- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 150 +++++++++++++++--- .../main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt | 23 ++- .../ru/ifmo/sd/httpapi/models/JoinGameInfo.kt | 15 +- 5 files changed, 284 insertions(+), 56 deletions(-) diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index cf9567d..8e6a322 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -8,25 +8,49 @@ import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.stuff.GUI import ru.ifmo.sd.stuff.ServerAPI import java.awt.EventQueue +import javax.swing.JFrame +import javax.swing.JOptionPane -internal var client: HttpClient? = null - fun main() { - client = HttpClient(CIO) { + ServerAPI.client = HttpClient(CIO) { install(JsonFeature) { serializer = KotlinxSerializer() } } - ServerAPI.restart() // to clear all previous data - val response = ServerAPI.join() - +// ServerAPI.initLocalServer() + val response = inputName() println(response) - EventQueue.invokeLater { createAndShowGUI(response) } } private fun createAndShowGUI(config: JoinGameInfo) { + val frame = GUI("Roguelike", config) frame.isVisible = true +} + +private fun inputName(): JoinGameInfo { + val frame = JFrame() + frame.isVisible = true + frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE + frame.setLocationRelativeTo(null) + + var shouldChooseAnotherNickname = false + var response: JoinGameInfo? = null + while (true) { + val message = "Type your nickname" + if (shouldChooseAnotherNickname) " (choose another one)" else "" + ServerAPI.nickname = JOptionPane.showInputDialog(frame, message) + var success = true + try { + response = ServerAPI.joinMultiplayer() + } catch (e: Exception) { + println(e.localizedMessage) + success = false + shouldChooseAnotherNickname = true + } + if (success) break + } + frame.isVisible = false + return response!! } \ 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 index 175c345..e0d1413 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -1,8 +1,6 @@ package ru.ifmo.sd.stuff -import ru.ifmo.sd.client -import ru.ifmo.sd.httpapi.models.JoinGameInfo -import ru.ifmo.sd.httpapi.models.Position +import ru.ifmo.sd.httpapi.models.* import ru.ifmo.sd.stuff.ColoredSymbol.* import java.awt.BorderLayout import java.awt.Color.* @@ -34,11 +32,14 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene private val infoLabel = JLabel("") private var map = SymbolMap(gameConfiguration) private var mapTextPane = MapTextPane() - private var isMultiplayer = false + private var isMultiplayer = true + private var currPlayer: String? = null + private var prevGameState: GameState? = null init { createUI(title) - start() + gameStateThread() + startRepaintThread() } private fun createUI(title: String) { @@ -51,7 +52,12 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene defaultCloseOperation = EXIT_ON_CLOSE addWindowListener(object : WindowAdapter() { override fun windowClosing(e: WindowEvent) { - client!!.close() + try { + disconnect(currPos) + } catch (e: Exception) { + println(e.localizedMessage) + } + ServerAPI.client!!.close() e.window.dispose() } }) @@ -68,21 +74,45 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene requestFocusInWindow() } - private fun start() { + private fun startRepaintThread() { Thread { while (true) { - try { Thread.sleep(30) } catch (e: InterruptedException) { break } - mainPanel.repaint() } }.start() } + private fun gameStateThread() { + Thread { + while (true) { + try { + Thread.sleep(100) + } catch (e: InterruptedException) { + break + } + if (!isYourTurn()) { + val gameState = getGameState() + if (prevGameState != gameState) { + println("New state $gameState") + prevGameState = gameState + currPlayer = gameState.currentMovePlayerName + isDead = !gameState.isAlive + infoLabel.text = "$currPlayer is making a move" + map = SymbolMap(gameState) + reloadMapTextPane() + } + } else { + infoLabel.text = "Your turn!" + } + } + }.start() + } + private fun createLayout() { add(mainPanel) mainPanel.layout = BorderLayout() @@ -112,15 +142,26 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene val multiplayerItem = if (isMultiplayer) { val item = JMenuItem("Exit multiplayer") item.addActionListener { - ServerAPI.exitMultiplayer() - isMultiplayer = false + proceedChangingGameMod() } item } else { val item = JMenuItem("Join multiplayer") item.addActionListener { - ServerAPI.joinMultiplayer() - isMultiplayer = true + var shouldChooseAnotherNickname = false + while (true) { + val message = "Type your nickname" + if (shouldChooseAnotherNickname) " (choose another one)" else "" + ServerAPI.nickname = JOptionPane.showInputDialog(this, message) + var success = true + try { + join() + } catch (e: Exception) { + success = false + shouldChooseAnotherNickname = true + } + if (success) break + } + proceedChangingGameMod() } item } @@ -194,7 +235,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } else { println("e == null") } - if (!didMove) { + if (!didMove && isYourTurn()) { var newPos = currPos when (e?.keyCode) { null -> { @@ -250,7 +291,8 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene // stepped away from the level remakeMap() } else { - val gameMove = ServerAPI.move(prevPos!!, currPos) + val gameMove = move(prevPos!!, currPos) + currPlayer = null println("gameMove=$gameMove") if (gameMove.playerPosition == Position(-1, -1)) { // player is dead @@ -278,11 +320,23 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } private fun remakeMap(restart: Boolean = false) { - ServerAPI.restart() // may break previous player's progress + disconnect(currPos) if (restart) { ServerAPI.resetMapSize() } - val newConfig = ServerAPI.join() +// start() // may break previous player's progress + val newConfig = join() + map = SymbolMap(newConfig) + val prevPosSaved = prevPos + currPos = newConfig.playerPos + prevPos = prevPosSaved + reloadMapTextPane() + } + + private fun proceedChangingGameMod() { + disconnect(currPos) + isMultiplayer = !isMultiplayer + val newConfig = join() map = SymbolMap(newConfig) val prevPosSaved = prevPos currPos = newConfig.playerPos @@ -317,4 +371,30 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene mapTextPane.replaceSelection(symbol.char.toString()) mapTextPane.isEditable = false } + + /////////////////// ServerAPI helpers + + private fun join(): JoinGameInfo { + return if (isMultiplayer) ServerAPI.joinMultiplayer() + else ServerAPI.joinLocal() + } + + private fun getGameState(): GameState { + return if (isMultiplayer) ServerAPI.getGameStateMultiplayer() + else ServerAPI.getGameStateLocal() + } + + private fun move(oldPos: Position, newPos: Position): GameMove { + return if (isMultiplayer) ServerAPI.moveMultiplayer(oldPos, newPos) + else ServerAPI.moveLocal(oldPos, newPos) + } + + private fun disconnect(currPos: Position) { + if (isMultiplayer) ServerAPI.disconnectMultiplayer(currPos) + else ServerAPI.disconnectLocal(currPos) + } + + private fun isYourTurn(): Boolean { + return currPlayer == ServerAPI.nickname + } } \ No newline at end of file diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt index 40ace63..801003a 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -1,63 +1,163 @@ package ru.ifmo.sd.stuff +import io.ktor.client.* import io.ktor.client.request.* import io.ktor.http.* import kotlinx.coroutines.runBlocking -import ru.ifmo.sd.client import ru.ifmo.sd.httpapi.models.* +import java.io.IOException +import java.net.ServerSocket + object ServerAPI { - const val address = "http://localhost:8080" + var client: HttpClient? = null + private const val MULTIPLAYER_ADDRESS = "http://localhost:8080" + private val LOCAL_ADDRESS + get() = "http://localhost:${port}" private var mapSize = Pair(4, 4) + private var port = 8081 + internal var nickname: String? = "null" + private const val localNickname = "local" - internal fun increaseMapSize() { - if (mapSize.first < 9) { - mapSize = Pair(mapSize.first + 1, mapSize.second + 2) + internal fun initLocalServer() { + while (true) { + println("Checking avaliabilty of port=${port}") + if (isAvailable(port)) { + break + } else port++ + } + println("Trying to start local server at port=${port}") + val cmd = "nohup ../game-server/build/install/game-server/bin/game-server &" // TODO call .bat for windows + val env = arrayOf("PORT=${port}", "JAVA_HOME=/Users/macbook/.jenv/versions/1.8") // TODO JAVA_HOME + val proc = Runtime.getRuntime().exec(cmd, env) // TODO kill it after closing the client + while (true) { + var started = true + try { + joinLocal() + } catch (e: Exception) { + println(e.localizedMessage) + started = false + } + if (started) break } + println("Local server successfully started") } - internal fun resetMapSize() { - mapSize = Pair(4, 4) +// internal fun startLocal() { +// runBlocking { +// println("apiStartLocal") +// client!!.post("$LOCAL_ADDRESS/start") { +// contentType(ContentType.Application.Json) +// body = LevelConfiguration(length = mapSize.first, width = mapSize.second) +// } +// } +// } +// +// internal fun startMultiplayer() { +// runBlocking { +// println("apiStartMult") +// client!!.post("$MULTIPLAYER_ADDRESS/start") { +// contentType(ContentType.Application.Json) +// body = LevelConfiguration(length = mapSize.first, width = mapSize.second) +// } +// } +// } + + internal fun joinLocal(): JoinGameInfo { + return runBlocking { + println("apiJoinLocal") + return@runBlocking client!!.post("$LOCAL_ADDRESS/join") { + contentType(ContentType.Application.Json) + body = JoinInfo(localNickname, length = mapSize.first, width = mapSize.second) + } + } } - internal fun join(): JoinGameInfo { + internal fun joinMultiplayer(): JoinGameInfo { return runBlocking { - println("apiJoin") - return@runBlocking client!!.post("$address/join") { + println("apiJoinMult") + return@runBlocking client!!.post("$MULTIPLAYER_ADDRESS/join") { contentType(ContentType.Application.Json) - body = LevelConfiguration(length = mapSize.first, width = mapSize.second) + body = JoinInfo(nickname!!, length = mapSize.first, width = mapSize.second) + } + } + } + + internal fun getGameStateLocal(): GameState { + return runBlocking { + return@runBlocking client!!.get("$LOCAL_ADDRESS/gameState") { + parameter("playerName", localNickname) + } + } + } + + internal fun getGameStateMultiplayer(): GameState { + return runBlocking { + return@runBlocking client!!.get("$MULTIPLAYER_ADDRESS/gameState") { + parameter("playerName", nickname!!) } } } - internal fun move(oldPos: Position, newPos: Position): GameMove { + internal fun moveLocal(oldPos: Position, newPos: Position): GameMove { return runBlocking { - println("apiMove oldPos=$oldPos, newPos=$newPos") - return@runBlocking client!!.post("$address/move") { + println("apiMoveLocal oldPos=$oldPos, newPos=$newPos") + return@runBlocking client!!.post("$LOCAL_ADDRESS/move") { contentType(ContentType.Application.Json) - body = PlayerPositionChanging(oldPos, newPos) + body = PlayerPositionChanging(localNickname, oldPos, newPos) } } } - internal fun restart() { - runBlocking { - println("apiRestart") - client!!.get("$address/restart") + internal fun moveMultiplayer(oldPos: Position, newPos: Position): GameMove { + return runBlocking { + println("apiMoveMult oldPos=$oldPos, newPos=$newPos") + return@runBlocking client!!.post("$MULTIPLAYER_ADDRESS/move") { + contentType(ContentType.Application.Json) + body = PlayerPositionChanging(nickname!!, oldPos, newPos) + } } } - internal fun joinMultiplayer() { + internal fun disconnectLocal(currPos: Position) { runBlocking { - println("apiJoinMultiplayer") - client!!.get("$address/join_multiplayer") + println("apiDisconnectLocal") + client!!.post("$LOCAL_ADDRESS/disconnect") { + contentType(ContentType.Application.Json) + body = PlayerInfo(localNickname, currPos) + } } } - internal fun exitMultiplayer() { + internal fun disconnectMultiplayer(currPos: Position) { runBlocking { - println("apiExitMultiplayer") - client!!.get("$address/exit_multiplayer") + println("apiDisconnectMultiplayer") + client!!.post("$MULTIPLAYER_ADDRESS/disconnect") { + contentType(ContentType.Application.Json) + body = PlayerInfo(nickname!!, currPos) + } + } + } + + // utilities + + internal fun increaseMapSize() { + if (mapSize.first < 9) { + mapSize = Pair(mapSize.first + 1, mapSize.second + 2) + } + } + + internal fun resetMapSize() { + mapSize = Pair(4, 4) + } + + private fun isAvailable(portNr: Int): Boolean { + var portFree: Boolean + try { + ServerSocket(portNr).use { portFree = true } + } catch (e: IOException) { + portFree = false } + return portFree } } \ 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 index 716f439..9c8bfd4 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/SymbolMap.kt @@ -1,5 +1,6 @@ package ru.ifmo.sd.stuff +import ru.ifmo.sd.httpapi.models.GameState import ru.ifmo.sd.httpapi.models.JoinGameInfo import ru.ifmo.sd.httpapi.models.MazeEventData import ru.ifmo.sd.stuff.ColoredSymbol.* @@ -16,21 +17,31 @@ enum class ColoredSymbol(val char: Char, val color: Color = BLACK) { NONE(' '), } -class SymbolMap(config: JoinGameInfo) { - val rows: List> = config.maze.levelMaze.map { arr -> - arr.map { i -> mazeObjToSymbol(i) }.toMutableList() - } +class SymbolMap { + + val rows: List> val rowSize: Int get() = rows.size val columnSize: Int get() = rows[0].size - var enemyAmount = config.maze.levelMaze.sumBy { arr -> arr.sumBy { i -> if (isEnemy(i)) 1 else 0 } } + var enemyAmount: Int private set - init { + constructor(config: JoinGameInfo) { + this.rows = config.maze.levelMaze.map { arr -> + arr.map { i -> mazeObjToSymbol(i) }.toMutableList() + } + this.enemyAmount = config.maze.levelMaze.sumBy { arr -> arr.sumBy { i -> if (isEnemy(i)) 1 else 0 } } rows[config.playerPos.row][config.playerPos.column] = PLAYER } + constructor(config: GameState) { + this.rows = config.maze.levelMaze.map { arr -> + arr.map { i -> mazeObjToSymbol(i) }.toMutableList() + } + this.enemyAmount = config.maze.levelMaze.sumBy { arr -> arr.sumBy { i -> if (isEnemy(i)) 1 else 0 } } + } + private fun mazeObjToSymbol(mazeObj: Int): ColoredSymbol = when (mazeObj) { 0 -> NONE 1 -> WALL diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt index 3be1636..d8ae667 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/JoinGameInfo.kt @@ -5,7 +5,20 @@ import kotlinx.serialization.Serializable @Serializable data class MazeData( val levelMaze: Array> -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MazeData) return false + + if (!levelMaze.contentDeepEquals(other.levelMaze)) return false + + return true + } + + override fun hashCode(): Int { + return levelMaze.contentDeepHashCode() + } +} @Serializable data class JoinGameInfo( From f100d8143d660d813874e4c6c8d3c15cb72d6003 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Wed, 26 May 2021 14:59:44 +0300 Subject: [PATCH 27/30] update game state dto --- .../main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt | 1 + .../main/kotlin/ru/ifmo/sd/world/events/EventsHandler.kt | 1 + .../main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt index 193203f..acd5e4a 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable data class GameState( val currentMovePlayerName: String, val isAlive: Boolean, + val isGameOver: Boolean, val maze: MazeData, val unitsHealth: List> ) 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 index e827d4a..20c4217 100644 --- 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 @@ -99,6 +99,7 @@ object EventsHandler { } return GameState( playersQueue.peek(), playersQueue.contains(playerName), + gameLevel!!.npcEventProvider.isAtLeastOneNpcAlive(), MazeData(getMazeData(gameLevel!!.maze)), getHealthsData(gameLevel!!.unitsHealthStorage) ) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt index 3a16a12..438f364 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/world/npc/NpcEventProvider.kt @@ -56,4 +56,13 @@ class NpcEventProvider { fun eliminateNpc(pos: Position) { npc.removeIf { it.position == pos } } + + /** + * Возвращает флаг выживания npc. + * + * @return true -- если хотя бы один npc жив, иначе -- false + */ + fun isAtLeastOneNpcAlive(): Boolean { + return npc.isNotEmpty() + } } From e51b6e367108ba07a4538418898b77fa9fb6ff92 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Fri, 28 May 2021 13:34:34 +0300 Subject: [PATCH 28/30] fix disconnect method --- .../ru/ifmo/sd/world/events/EventsHandler.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 index 20c4217..40b6ce9 100644 --- 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 @@ -72,17 +72,21 @@ object EventsHandler { * @param playerPos -- позиция игрока для отсоединения */ fun disconnect(playerName: String, playerPos: Position) { - playersQueue.remove(playerName) - if (playersQueue.isEmpty()) { - closeGame() - } if (gameLevel == null) { - throw GameServerException("Game already finished.") + throw GameServerException("Game level has not been initialized yet.") + } + val wasRemoved = playersQueue.remove(playerName) + if (!wasRemoved) { + throw GameServerException("Could not find player with name ${playerName}.") } val healths = gameLevel!!.unitsHealthStorage healths.eliminateUnit(playerPos) val maze = gameLevel!!.maze maze[playerPos] = null + + if (playersQueue.isEmpty()) { + closeGame() + } } /** @@ -122,7 +126,6 @@ object EventsHandler { private fun closeGame() { gameLevel = null - playersQueue.clear() } /** From 3ceb666830598926defefebfef58fe05c25f2b13 Mon Sep 17 00:00:00 2001 From: Sokolvyak Sergey Date: Fri, 28 May 2021 13:55:28 +0300 Subject: [PATCH 29/30] fix naming --- .../src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt index acd5e4a..2c55da1 100644 --- a/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt +++ b/game-server/src/main/kotlin/ru/ifmo/sd/httpapi/models/GameState.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class GameState( val currentMovePlayerName: String, val isAlive: Boolean, - val isGameOver: Boolean, + val isAtLeastOneNpcAlive: Boolean, val maze: MazeData, val unitsHealth: List> ) From 7c5e414510bb647dabaf5bab6779f8d55c4a62b2 Mon Sep 17 00:00:00 2001 From: Kirill Esakov Date: Fri, 28 May 2021 16:27:23 +0500 Subject: [PATCH 30/30] multiplayer fix --- .../src/main/kotlin/ru/ifmo/sd/Main.kt | 44 +++++--- .../src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt | 104 +++++++++--------- .../main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt | 15 ++- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt index 8e6a322..55f7a02 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/Main.kt @@ -11,6 +11,7 @@ import java.awt.EventQueue import javax.swing.JFrame import javax.swing.JOptionPane +private val inputNameFrame = JFrame() fun main() { ServerAPI.client = HttpClient(CIO) { @@ -20,8 +21,14 @@ fun main() { } // ServerAPI.initLocalServer() val response = inputName() - println(response) - EventQueue.invokeLater { createAndShowGUI(response) } + println(ServerAPI.nickname) + if (ServerAPI.nickname == null) { + ServerAPI.client!!.close() + inputNameFrame.dispose() + } else { + println(response) + EventQueue.invokeLater { createAndShowGUI(response!!) } + } } private fun createAndShowGUI(config: JoinGameInfo) { @@ -30,27 +37,38 @@ private fun createAndShowGUI(config: JoinGameInfo) { frame.isVisible = true } -private fun inputName(): JoinGameInfo { - val frame = JFrame() - frame.isVisible = true - frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE - frame.setLocationRelativeTo(null) +private fun inputName(): JoinGameInfo? { + inputNameFrame.isVisible = true + inputNameFrame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE + inputNameFrame.setLocationRelativeTo(null) - var shouldChooseAnotherNickname = false + var isNameTaken = false + var isNameValid = true var response: JoinGameInfo? = null while (true) { - val message = "Type your nickname" + if (shouldChooseAnotherNickname) " (choose another one)" else "" - ServerAPI.nickname = JOptionPane.showInputDialog(frame, message) + val message = "Type your nickname" + + if (!isNameValid) " (name should contain latin letters or digits)" + else if (isNameTaken) " (choose another one)" + else "" + ServerAPI.nickname = JOptionPane.showInputDialog(inputNameFrame, message) + + isNameValid = validateNickname(ServerAPI.nickname) + if (!isNameValid) continue + var success = true try { response = ServerAPI.joinMultiplayer() } catch (e: Exception) { println(e.localizedMessage) success = false - shouldChooseAnotherNickname = true + isNameTaken = true } if (success) break } - frame.isVisible = false - return response!! + inputNameFrame.isVisible = false + return response +} + +private fun validateNickname(name: String?): Boolean { + return name != null && name.length < 20 && name.matches("[a-zA-Z0-9]+".toRegex()) } \ 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 index e0d1413..d7acffc 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/GUI.kt @@ -38,7 +38,6 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene init { createUI(title) - gameStateThread() startRepaintThread() } @@ -58,6 +57,7 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene println(e.localizedMessage) } ServerAPI.client!!.close() + ServerAPI.killLocalServer() e.window.dispose() } }) @@ -82,33 +82,29 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene } catch (e: InterruptedException) { break } - mainPanel.repaint() - } - }.start() - } - - private fun gameStateThread() { - Thread { - while (true) { - try { - Thread.sleep(100) - } catch (e: InterruptedException) { - break - } if (!isYourTurn()) { val gameState = getGameState() if (prevGameState != gameState) { println("New state $gameState") - prevGameState = gameState - currPlayer = gameState.currentMovePlayerName - isDead = !gameState.isAlive - infoLabel.text = "$currPlayer is making a move" - map = SymbolMap(gameState) - reloadMapTextPane() + if (!gameState.isAtLeastOneNpcAlive) { + ServerAPI.increaseMapSize() + remakeMap() + } else { + prevGameState = gameState + currPlayer = gameState.currentMovePlayerName + isDead = !gameState.isAlive + if (isYourTurn()) { + infoLabel.text = "Your turn!" + } else { + infoLabel.text = "$currPlayer is making a move" + } + map = SymbolMap(gameState) + reloadMapTextPane() + } } - } else { - infoLabel.text = "Your turn!" } + + mainPanel.repaint() } }.start() } @@ -166,17 +162,17 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene item } - val restartItem = JMenuItem("Restart") -// eMenuItem.mnemonic = KeyEvent.VK_R - restartItem.toolTipText = "Restart game" - restartItem.addActionListener { - isDead = false - infoLabel.text = "" - remakeMap(restart = true) - } +// val restartItem = JMenuItem("Restart") +//// eMenuItem.mnemonic = KeyEvent.VK_R +// restartItem.toolTipText = "Restart game" +// restartItem.addActionListener { +// isDead = false +// infoLabel.text = "" +// remakeMap(restart = true) +// } menu.add(multiplayerItem) - menu.add(restartItem) +// menu.add(restartItem) menubar.add(menu) jMenuBar = menubar @@ -286,26 +282,26 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene this.mapPanel.repaint() this.mapTextPane.repaint() - if ((currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) || - (currPos.row == 0 && currPos.column == 1)) { - // stepped away from the level - remakeMap() - } else { +// if ((currPos.row == map.rowSize - 1 && currPos.column == map.columnSize - 2) || +// (currPos.row == 0 && currPos.column == 1)) { +// // stepped away from the level +// remakeMap() +// } else { val gameMove = move(prevPos!!, currPos) currPlayer = null println("gameMove=$gameMove") - if (gameMove.playerPosition == Position(-1, -1)) { - // player is dead - isDead = true - infoLabel.text = "You are dead!" - replacePaneSymbol(prevPos!!, NONE) - } else { +// if (gameMove.playerPosition == Position(-1, -1)) { +// // player is dead +// isDead = true +// infoLabel.text = "You are dead!" +// replacePaneSymbol(prevPos!!, NONE) +// } else { // player moved replaceSymbol(prevPos!!, gameMove.playerPosition) map.applyDiff(gameMove.events) if (map.enemyAmount == 0) { ServerAPI.increaseMapSize() - remakeMap() +// remakeMap() } else { // reload pane val prevPosSaved = prevPos @@ -313,34 +309,38 @@ class GUI(title: String, gameConfiguration: JoinGameInfo) : JFrame(), KeyListene prevPos = prevPosSaved reloadMapTextPane() } - } - } +// } +// } } } } private fun remakeMap(restart: Boolean = false) { disconnect(currPos) - if (restart) { - ServerAPI.resetMapSize() - } +// if (restart) { +// ServerAPI.resetMapSize() +// } // start() // may break previous player's progress val newConfig = join() + println("newConfig") + println(newConfig.toString()) map = SymbolMap(newConfig) - val prevPosSaved = prevPos + isDead = false +// val prevPosSaved = prevPos currPos = newConfig.playerPos - prevPos = prevPosSaved +// prevPos = prevPosSaved reloadMapTextPane() } + // TODO private fun proceedChangingGameMod() { disconnect(currPos) isMultiplayer = !isMultiplayer val newConfig = join() map = SymbolMap(newConfig) - val prevPosSaved = prevPos +// val prevPosSaved = prevPos currPos = newConfig.playerPos - prevPos = prevPosSaved +// prevPos = prevPosSaved reloadMapTextPane() } diff --git a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt index 801003a..fc537c9 100644 --- a/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt +++ b/game-client/src/main/kotlin/ru/ifmo/sd/stuff/ServerAPI.kt @@ -18,6 +18,7 @@ object ServerAPI { private var port = 8081 internal var nickname: String? = "null" private const val localNickname = "local" + private var localServerProc: Process? = null; internal fun initLocalServer() { while (true) { @@ -27,9 +28,11 @@ object ServerAPI { } else port++ } println("Trying to start local server at port=${port}") - val cmd = "nohup ../game-server/build/install/game-server/bin/game-server &" // TODO call .bat for windows - val env = arrayOf("PORT=${port}", "JAVA_HOME=/Users/macbook/.jenv/versions/1.8") // TODO JAVA_HOME - val proc = Runtime.getRuntime().exec(cmd, env) // TODO kill it after closing the client + println(System.getProperty("user.dir")) + val cmd = "../game-server/build/install/game-server/bin/game-server" // TODO call .bat for windows +// val env = arrayOf("PORT=${port}", "JAVA_HOME=/Users/macbook/.jenv/versions/1.8") // TODO JAVA_HOME + val env = arrayOf("PORT=${port}") + localServerProc = Runtime.getRuntime().exec(cmd, env) // TODO kill it after closing the client while (true) { var started = true try { @@ -43,6 +46,10 @@ object ServerAPI { println("Local server successfully started") } + internal fun killLocalServer() { + if (localServerProc?.isAlive == true) localServerProc?.destroy() + } + // internal fun startLocal() { // runBlocking { // println("apiStartLocal") @@ -65,7 +72,7 @@ object ServerAPI { internal fun joinLocal(): JoinGameInfo { return runBlocking { - println("apiJoinLocal") +// println("apiJoinLocal") return@runBlocking client!!.post("$LOCAL_ADDRESS/join") { contentType(ContentType.Application.Json) body = JoinInfo(localNickname, length = mapSize.first, width = mapSize.second)