Initial commit

This commit is contained in:
2026-08-23 15:51:15 -04:00
commit 92c14dfc81
62 changed files with 2685 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
plugins {
kotlin("jvm")
alias(libs.plugins.ktor)
application
}
repositories {
mavenCentral()
}
dependencies {
// Ktor
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
// JSON
implementation(libs.ktor.serialization.kotlinx.json)
// Status Pages / Exception Handling
implementation(libs.ktor.server.status.pages)
// Logging
implementation(libs.ktor.server.logging)
implementation(libs.logback)
implementation(libs.ktor.server.swagger)
// Testing
testImplementation(libs.ktor.server.test.host)
testImplementation(kotlin("test"))
// Websockets
implementation(libs.ktor.server.websockets)
// Swagger/OpenAPI
implementation(libs.ktor.server.openapi)
implementation(libs.ktor.server.routing.openapi)
implementation(libs.ktor.server.content.negotiation)
// General Use
implementation(libs.kotlinx.datetime)
implementation(project(":shared"))
implementation("ch.qos.logback:logback-classic:1.6.3")
}
application {
mainClass = "dev.geekback.dopewars.server.ServerKt"
}
//
//// Copy the JS+HTML from web to here
//tasks.processResources {
// dependsOn(":web:wasmJsBrowserProductionWebpack")
//
// from(project(":web")
// .layout
// .buildDirectory
// .dir("dist/js/productionExecutable"))
//}
@@ -0,0 +1,88 @@
package dev.geekback.dopewars.server
import dev.geekback.dopewars.server.routes.gameRoutes
import dev.geekback.dopewars.server.routes.marketRoutes
import dev.geekback.dopewars.server.routes.modelCrudRoutes
import dev.geekback.dopewars.server.routes.playerRoutes
import dev.geekback.dopewars.server.services.GameService
import dev.geekback.dopewars.server.services.MarketService
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.http.content.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.openapi.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.serialization.json.Json
import org.slf4j.event.Level
fun Application.module() {
println("[ # ] Loading Call Logging Plugin")
install(CallLogging) {
level = Level.INFO
}
println("[ # ] Loading Web Sockets Plugin")
install(WebSockets);
println("[ # ] Loading Content Negotiation Plugin")
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
allowStructuredMapKeys = true
})
}
println("[ # ] Plugins Loaded")
val marketService = MarketService()
val gameService = GameService(marketService)
routing {
playerRoutes(marketService, gameService)
marketRoutes(marketService, gameService)
gameRoutes(gameService)
modelCrudRoutes(marketService)
webSocket("/ws") {
send("Welcome to DopeWars");
for (frame in incoming) {
if (frame is Frame.Text) send("Echo: ${frame.readText()}")
}
}
openAPI(path = "openapi") {
/*
Documentation source configuration goes here.
This can be from file (documentation.yaml), or it can be served dynamically from your sources using the
`describe {}` API on routes. When `openApi` enabled in Gradle, these calls will be automatically injected
based on your code and comments.
*/
}
staticResources("/", "")
}
}
fun main() {
val port = 8080
val host = "0.0.0.0"
val server = embeddedServer(Netty, port, host = host) {
module()
println("[+] Listening on http://$host:$port")
}
server.start(wait = true)
println("Past the server.start")
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.server.repository
import dev.geekback.dopewars.shared.Player
import java.time.LocalDateTime
object PlayerRepository {
private val players = mutableMapOf<String, Player>()
fun save(player: Player) {
players[player.id] = player
}
fun find(id: String): Player? {
return players[id]
}
fun all(): Collection<Player> {
return players.values
}
fun getNextName(): String {
return "Player at ${LocalDateTime.now()}"
}
}
@@ -0,0 +1,98 @@
package dev.geekback.dopewars.server.routes
import dev.geekback.dopewars.server.repository.PlayerRepository
import dev.geekback.dopewars.server.services.GameService
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import io.ktor.server.response.respond
import io.ktor.utils.io.ExperimentalKtorApi
@OptIn(ExperimentalKtorApi::class)
fun Route.gameRoutes(
gameService: GameService
) {
//
post("/players/{playerId}/travel") {
val playerId =
call.parameters["playerId"]
?: return@post call.respond(
HttpStatusCode.BadRequest
)
println("Player = $playerId")
val request =
call.receive<TravelRequest>()
println("TravelRequest = $request")
val player =
PlayerRepository.find(playerId)
?: return@post call.respond(
HttpStatusCode.NotFound
)
println("Player loaded")
try {
val result = gameService.travel(
player,
request.destination
)
call.respond(result)
} catch (e: IllegalArgumentException) {
println("ERROR = $e")
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to (e.message ?: "Travel failed")
)
)
}
}
post("/players/{playerId}/trade") {
val playerId =
call.parameters["playerId"]
?: return@post call.respond(
HttpStatusCode.BadRequest,
mapOf("error" to "Missing playerId")
)
val player =
PlayerRepository.find(playerId)
?: return@post call.respond(
HttpStatusCode.NotFound,
mapOf("error" to "Player not found")
)
val request = call.receive<TradeRequest>()
try {
val updatedPlayer =
gameService.trade(player, request)
call.respond(updatedPlayer)
} catch (e: IllegalArgumentException) {
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to (e.message ?: "Invalid trade")
)
)
}
}
}
@@ -0,0 +1,63 @@
package dev.geekback.dopewars.server.routes
import dev.geekback.dopewars.server.repository.PlayerRepository
import dev.geekback.dopewars.server.services.GameService
import dev.geekback.dopewars.server.services.MarketService
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.api.MarketResult
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.openapi.jsonSchema
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.openapi.describe
import io.ktor.server.routing.route
import io.ktor.utils.io.ExperimentalKtorApi
@OptIn(ExperimentalKtorApi::class)
fun Route.marketRoutes(
marketService: MarketService,
gameService: GameService
) {
route("/market") {
get {
val players = PlayerRepository.all().toList()
val response: MutableList<Market> = mutableListOf();
players.forEach { player ->
response.add(player.lastMarket!!)
}
call.respond(response)
}
get("/{playerId}") {
val playerId =
call.parameters["playerId"]!!
val player =
PlayerRepository.find(playerId)
?: return@get call.respond(
HttpStatusCode.NotFound
)
call.respond(
gameService.currentMarket(player)
)
}.describe {
summary = "Get Market for Specified Player"
description = "Get a list of the products and the prices in a market"
responses {
HttpStatusCode.OK {
description = "Market Snapshot"
schema = jsonSchema<MarketResult>()
}
HttpStatusCode.BadRequest {
description = "Invalid Query"
ContentType.Text.Plain
}
}
}
}
}
@@ -0,0 +1,38 @@
package dev.geekback.dopewars.server.routes
import dev.geekback.dopewars.server.services.MarketService
import dev.geekback.dopewars.shared.Location
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
fun Route.modelCrudRoutes(marketService: MarketService) {
route("crud") {
route("{modelName}") {
// List
get {
val modelName = call.parameters["modelName"]
when (modelName) {
"market" -> {
call.respond(marketService.generateMarket(Location("BRONX")))
}
else -> {
call.respond("Model name is $modelName")
}
}
}
// R
get("{modelId}") {
val modelId = call.parameters["modelId"]
val modelName = call.parameters["modelName"]
call.respond("Model is $modelName and id is $modelId")
}
}
}
}
@@ -0,0 +1,102 @@
package dev.geekback.dopewars.server.routes
import dev.geekback.dopewars.server.repository.PlayerRepository
import dev.geekback.dopewars.server.services.GameService
import dev.geekback.dopewars.server.services.MarketService
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Player
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.openapi.jsonSchema
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.openapi.describe
import io.ktor.utils.io.ExperimentalKtorApi
import java.util.UUID
@OptIn(ExperimentalKtorApi::class)
fun Route.playerRoutes(
marketService: MarketService,
gameService: GameService
) {
// Player CRUD
route("/players") {
// Create
post {
val location = Location("Brooklyn")
val mkt = marketService.generateMarket(location)
val player = Player(
id = UUID.randomUUID().toString(),
name = PlayerRepository.getNextName(),
lastMarket = mkt
)
PlayerRepository.save(player)
call.respond(
HttpStatusCode.Created,
player
)
}.describe {
summary = "Create a new Player"
description = "Create a new Player with a unique ID"
responses {
HttpStatusCode.OK {
description = "A Player with unique ID"
schema = jsonSchema<Player>()
}
HttpStatusCode.BadRequest {
description = "An error occured creating the user"
ContentType.Text.Plain
}
}
}
// Read
get {
val players = PlayerRepository.all();
call.respond(
HttpStatusCode.OK,
players
)
}.describe {
summary = "Get all Players"
description = "Get a list of all the players on the server"
responses {
HttpStatusCode.OK {
description = "A list of Users"
schema = jsonSchema<List<Player>>()
}
}
}
// Update
post("/{playerId}/rename") {
val playerId = call.parameters["playerId"] ?: return@post call.respond(HttpStatusCode.BadRequest)
val newPlayer = Player(
id = playerId,
name = call.parameters["name"] ?: return@post call.respond(HttpStatusCode.BadRequest)
)
PlayerRepository.save(newPlayer)
}
// Read
get("/{playerId}") {
val playerId = call.parameters["playerId"]
?: return@get call.respond(HttpStatusCode.BadRequest)
val player = PlayerRepository.find(playerId)
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(HttpStatusCode.OK, player)
}
}
}
@@ -0,0 +1,135 @@
package dev.geekback.dopewars.server.services
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.InventoryItem
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TradeAction
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelResult
class GameService(
private val marketService: MarketService
) {
private val markets = mutableMapOf<String, Market>()
fun currentMarket(player: Player): Market {
return markets.getOrPut(player.id) {
marketService.generateMarket(player.location)
}
}
fun trade(
player: Player,
request: TradeRequest
): Player {
require(request.quantity > 0) {
"Quantity must be greater than zero"
}
val market = currentMarket(player)
val price = market.prices[request.drug]
?: throw IllegalArgumentException(
"Drug not found: ${request.drug}"
)
when (request.action) {
TradeAction.BUY -> buy(
player,
request.drug,
request.quantity,
price
)
TradeAction.SELL -> sell(
player,
request.drug,
request.quantity,
price
)
}
return player
}
private fun buy(
player: Player,
drug: Drug,
quantity: Int,
price: Float
) {
val cost = price * quantity
require(player.cash >= cost) {
"Not enough cash"
}
player.cash -= cost
val existing = player.inventory
.find { it.drug == drug }
if (existing != null) {
existing.quantity += quantity
} else {
player.inventory.add(
InventoryItem(
drug = drug,
quantity = quantity
)
)
}
}
private fun sell(
player: Player,
drug: Drug,
quantity: Int,
price: Float
) {
val inventoryItem = player.inventory
.find { it.drug == drug }
?: throw IllegalArgumentException(
"You do not own any $drug"
)
require(inventoryItem.quantity >= quantity) {
"Not enough inventory"
}
inventoryItem.quantity -= quantity
player.cash += price * quantity.toFloat()
if (inventoryItem.quantity == 0) {
player.inventory.remove(inventoryItem)
}
}
fun travel(
player: Player,
destination: Location
): TravelResult {
require(destination != player.location) {
"Player is already in $destination"
}
player.location = destination
player.day += 1
val market = marketService.generateMarket(destination)
markets[player.id] = market
return TravelResult(
location = market.location,
day = player.day,
market = market,
)
}
}
@@ -0,0 +1,22 @@
package dev.geekback.dopewars.server.services
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.Drugs
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
class MarketService {
fun generateMarket(location: Location): Market {
val prices: MutableMap<Drug, Float> = mutableMapOf()
Drugs.all.forEach { drugInfo ->
prices[drugInfo.drug] = drugInfo.newPrice()
}
return Market(
location = location,
prices = prices.toMap()
)
}
}
@@ -0,0 +1 @@
package dev.geekback.dopewars.server.services
+3
View File
@@ -0,0 +1,3 @@
<html>
<body><h1>This is the default page.</h1></body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="io.netty" level="INFO"/>
</configuration>