Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
<html>
|
||||
<body><h1>This is the default page.</h1></body>
|
||||
</html>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user