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
+11
View File
@@ -0,0 +1,11 @@
plugins {
kotlin("multiplatform") version "2.4.10" apply false
alias(libs.plugins.compose) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ktor) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
repositories {
mavenCentral()
}
+31
View File
@@ -0,0 +1,31 @@
plugins {
kotlin("jvm")
alias(libs.plugins.compose)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(project(":shared"))
implementation("com.composables:icons-material-icons-filled-cmp:2.2.1")
implementation(libs.kotlinx.serialization)
implementation(project(":shared"))
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation("com.composables:icons-material-icons-filled-cmp:2.2.1")
implementation("org.jetbrains.compose.material3:material3:1.9.0")
implementation(compose.desktop.currentOs)
}
repositories {
mavenCentral()
google()
}
compose.desktop {
application {
mainClass = "dev.geekback.dopewars.desktop.Main3Kt"
}
}
@@ -0,0 +1,117 @@
package dev.geekback.dopewars.desktop
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
/**
* ApiClient
*
* Client for accessing the Dopewars API server
*/
class ApiClient() {
private var address: String = ""
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(
Json {
allowSpecialFloatingPointValues = true
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
fun new(address: String) {
this.address = address
}
/**
* login
*
* Create a new game
*/
suspend fun login(baseUrl: String): Player {
println("URL = [${baseUrl}/players]")
val response = client.post(urlString = "$baseUrl/players") {
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.Created) {
response.body()
} else {
println("Login fail. -> ${response.status}")
Player(name = "Invalid Login Attempt", id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
}
}
suspend fun market(forPlayer: Player): Market {
val result: Market = client.get("${this.address}/market/${forPlayer.id}") {
contentType(ContentType.Application.Json)
}.body()
return result
}
suspend fun rename(newName: String, player: Player): Player {
val response = client.post("${this.address}/players/${player.id}/rename") {
val newBody = PlayerNameUpdateRequest(newName)
setBody(newBody)
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.OK) {
val result: Player = response.body()
println("Rename Success -> $result")
result
} else {
println("Rename Fail -> ${response.status} / ${this.address}/players/${player.id}/rename")
player
}
}
suspend fun refreshPlayer(initialState: Player): Player {
val response = client.get("${this.address}/players/${initialState.id}") {
contentType(ContentType.Application.Json)
}
return if (response.status == HttpStatusCode.OK) {
val result: Player = response.body()
println("Refresh Success -> $result")
result
} else {
println("Refresh Failure")
initialState
}
}
suspend fun travel(baseUrl: String, playerId: String, destination: TravelRequest) {
val response = client.post("$baseUrl/players/${playerId}/travel") {
setBody(destination)
contentType(ContentType.Application.Json)
}
println("Travel result -> $response")
}
// suspend fun travel(destination: Location): TravelResult {
// val response = client.get("${this.address}/players/${this.player}")
// }
}
@@ -0,0 +1,42 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTheme(content: @Composable () -> Unit) {
Scaffold {
MaterialTheme {
content()
}
}
}
@Composable
fun BorderedCard(title: String, content: @Composable () -> Unit) {
Card(modifier = Modifier
.border(BorderStroke(width = 1.dp, color = Color.Black))
.fillMaxWidth()
.padding(16.dp)) {
Column {
Text(
modifier = Modifier.padding(16.dp),
text = title,
style = MaterialTheme.typography.titleMedium)
content()
}
}
}
@@ -0,0 +1,64 @@
package dev.geekback.dopewars.desktop
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.ApiClient
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.PlayerUi
class ClientState(player: Player) {
var player by mutableStateOf(player)
var apiUrl: String = "http://localhost:8080"
private var api: ApiClient = ApiClient()
suspend fun renamePlayer(newName: String) {
println("Renaming player from ${this.player.name} to $newName")
this.player = this.api.rename(newName, this.player)
}
suspend fun login() : Player {
val p = this.api.login("http://localhost:8080")
println("Logged in as ${p.name} / ${p.id}")
return p
}
suspend fun updateMarket() {
println("Updating market")
this.player.lastMarket = this.api.market(this.player)
println("Market updated to this -> ${this.player.lastMarket}")
}
suspend fun refreshPlayer() {
println("Refreshing player")
this.player = this.api.refreshPlayer(this.player)
println("Refreshed player")
}
fun isLoggedIn(): Boolean {
println("Testing if user is logged in - ${this.player.id.isNotEmpty()}")
return this.player.id.isNotEmpty()
}
fun refreshApi(url: String) {
println("[XXX] Refreshing API UrL to $url")
this.api.new(url)
}
suspend fun travel(player: Player, destination: TravelRequest) {
val result = this.api.travel("http://localhost:8080", player.id, destination)
}
companion object {
fun new(): ClientState {
return ClientState(
Player.new()
)
}
}
}
@@ -0,0 +1,181 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.shared.Drugs
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
import dev.geekback.dopewars.shared.api.TradeAction
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsUi
import kotlinx.coroutines.launch
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars Dev Client"
) {
DemoApp()
}
}
data class ApiData(
var marketRequest: MarketRequest,
var marketResult: MarketResult,
var playerNameUpdateRequest: PlayerNameUpdateRequest,
var playerNameUpadteResponse: PlayerNameUpdateResponse,
var tradeAction: TradeAction,
var tradeRequest: TradeRequest,
var travelRequest: TravelRequest,
var travelResult: TravelResult
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DemoApp() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf(Screens.HOME) }
var drug by remember { mutableStateOf(DrugInfo(drug = Drug.WEED, minValue = 100.0f, maxValue = 1000.0f)) }
var player by remember { mutableStateOf(Player(id = "____", name = "TBD")) }
var location by remember { mutableStateOf(Location(name = "LocationDefault"))}
var market by remember { mutableStateOf(Market(
location = Location(name = "MarketDefault"),
prices = mapOf()
))}
val ui = DopeWarsUi.new()
val travelResultLocation = Locations.random();
var api by remember { mutableStateOf(ApiData(
marketRequest = MarketRequest(
location = Location(name = "MarketRequestLocation")
),
marketResult = MarketResult(
market = Market(location = Location(name = "MarketResultLocation"),
prices = emptyMap()
)
),
playerNameUpdateRequest = PlayerNameUpdateRequest("newPlayerName"),
playerNameUpadteResponse = PlayerNameUpdateResponse(
updatedPlayer = Player(id = "____", name = "updatedPlayerName")
),
tradeAction = TradeAction.BUY,
tradeRequest = TradeRequest(
action = TradeAction.BUY,
drug = Drug.random(),
quantity = 10
),
travelRequest = TravelRequest(
destination = Locations.random()
),
travelResult = TravelResult(
location = travelResultLocation,
day = 1,
market = Market(location = travelResultLocation, mapOf())
)
)) }
MaterialTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Button(onClick = {
currentScreen = Screens.HOME
scope.launch { drawerState.close() }
}) { Text("Home")}
Button( onClick = {
currentScreen = Screens.DRUG
scope.launch { drawerState.close() }
}) { Text("Drug") }
Button( onClick = {
currentScreen = Screens.PLAYER
scope.launch { drawerState.close() }
}) { Text("Player") }
Button(onClick = {
currentScreen = Screens.LOCATION
scope.launch { drawerState.close() }
}) { Text("Location") }
Button( onClick = {
currentScreen = Screens.API
scope.launch { drawerState.close() }
}) { Text("API") }
}
}
) {
// Main content
when (currentScreen) {
Screens.HOME -> {
Column {
ui.player.view(player)
// ui.drug.view(drug)
}
}
Screens.DRUG -> {
// ui.drug.form(drug) {
// drug = it
// }
}
Screens.PLAYER -> {
ui.player.form(player) {
player = it
}
}
Screens.LOCATION -> {
ui.location.form(location) {
location = it
}
}
Screens.MARKET -> {
ui.market.form(market) {
market = it
}
}
Screens.API -> {
Column {
ui.api.marketRequest.form(api.marketRequest) { api.marketRequest = it }
ui.api.marketResponse.form(api.marketResult) { api.marketResult = it }
ui.api.playerNameUpdateRequest.form(api.playerNameUpdateRequest) { api.playerNameUpdateRequest = it }
ui.api.playerNameUpdateResponse.form(api.playerNameUpadteResponse) { api.playerNameUpadteResponse = it }
ui.api.travelRequest.form(api.travelRequest) { api.travelRequest = it }
ui.api.travelResponse.form(api.travelResult) { api.travelResult = it}
ui.api.tradeRequest.form(api.tradeRequest) { api.tradeRequest = it}
}
}
}
}
}
}
@@ -0,0 +1,139 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Menu
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Player
import kotlinx.coroutines.launch
fun main() {
val state = ClientState(Player(id = "", name = "TestClient"))
// var drug = Drug("TestDrug", minPrice = 10.0f, maxPrice = 100.0f)
var player = Player(id = "default", name = "defaultName")
var location = Location(name = "DefaultLocation")
application {
var currentScreen = remember { Screens.DRUG }
val scope = rememberCoroutineScope()
// state.updateApiUrl("http://localhost:8080")
// val scaffoldState = rememberScaffoldState(drawerState = DrawerState(DrawerValue.Closed))
// var menuVisible by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "DopeWars") {
/*
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
Button(onClick = {
currentScreen = Screens.DRUG
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Drug")}
Button(onClick = {
currentScreen = Screens.PLAYER
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Player") }
},
topBar = {
TopAppBar(
title = {
Text("Title")
},
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(imageVector = MaterialIcons.Filled.Menu , contentDescription = "Menu")
}
})
}
) {
*/
AppTheme {
Text("Test123")
Column(modifier = Modifier.fillMaxSize()) {
/*
when (currentScreen) {
Screens.DRUG -> {
drugForm(drug) {
drug = it
}
drugView(drug)
}
Screens.PLAYER -> {
playerView(player)
}
Screens.LOCATION -> {
locationView(location)
locationForm(location) {
location = it
}
}
Screens.INVENTORY -> {
inventoryView(emptyList())
}
Screens.MARKET -> {
marketView(Market(Location("Unknown Location"), emptyMap()))
}
}
*/
}
}
}
/*
println("Session ID = ${state.player.id}")
Column {
Text("DopeWars Desktop Client")
configPanel(config = state, onUrlChanged = {
state.updateApiUrl(it)
})
PlayerForm(
initialState = state.player,
onRequestLogin = {
scope.launch {
state.login()
}
},
onRequestPlayerRename = { newName ->
scope.launch {
state.renamePlayer(newName)
}
},
onGetMarket = {
scope.launch {
state.updateMarket()
}
},
onRefresh = {
scope.launch {
state.refreshPlayer()
}
}
)
}
*/
}
}
@@ -0,0 +1,124 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Login
import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.ui.TravelUi
import kotlinx.coroutines.launch
fun main() {
application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars"
) {
App()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun App() {
var state by remember { mutableStateOf(ClientState.new() ) }
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
AppTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
if (state.isLoggedIn()) {
// Display the current player state
PlayerUi().view(state.player)
} else {
NavigationDrawerItem(
label = {
Row {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = MaterialIcons.Filled.Login,
contentDescription = "Login Icon"
)
Text(
modifier = Modifier.padding(16.dp),
text = "Login"
)
}
},
selected = false,
onClick = {
scope.launch {
state.player = state.login()
}
}
)
}
}
}
) {
if (state.isLoggedIn()) {
mainWindow(state)
} else {
Column(modifier = Modifier.padding(16.dp)) {
Text("<-- Start a new game")
}
}
}
}
}
@Composable
fun mainWindow(state: ClientState) {
val scope = rememberCoroutineScope();
var showingTravelWizard by remember { mutableStateOf(false) }
PlayerUi().view(state.player)
Row {
Button(onClick = {
println("Transactions")
}) {
Text("Transactions")
}
Button(onClick = {
showingTravelWizard = true
}) {
Text("Travel")
}
if (showingTravelWizard) {
TravelUi().wizard(state.player) {
println("Player wants to go to $it")
showingTravelWizard = false
scope.launch {
state.travel(state.player, it)
}
}
}
}
}
@@ -0,0 +1,10 @@
package dev.geekback.dopewars.desktop
enum class Screens {
HOME,
DRUG,
PLAYER,
LOCATION,
MARKET,
API
}
@@ -0,0 +1,37 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun configPanel(config: ClientState, onUrlChanged: (String) -> Unit) {
var apiUrl by remember { mutableStateOf(config.apiUrl) }
Card {
Column {
Text("Configuration")
Row {
Text("ApiURL = $apiUrl")
Text("Config.apiUrl = ${config.apiUrl}")
TextField(value = apiUrl, onValueChange = { apiUrl = it })
Button({
onUrlChanged(apiUrl)
}) {
Text("Update API Url")
}
}
}
}
}
@@ -0,0 +1,61 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
@Composable
fun MarketDisplay(market: Market?) {
if (market == null) {
Text("No Market Available")
} else {
Card {
Column {
Text("Market for ${market.location}", modifier = Modifier.padding(16.dp))
market.prices.forEach { (name, price) ->
Text("$name = $${price}")
}
}
}
}
}
@Composable
fun PlayerForm(
initialState: Player,
onRequestLogin: () -> Unit,
onRequestPlayerRename: (String) -> Unit,
onGetMarket: () -> Unit,
onRefresh: () -> Unit
) {
var username by remember { mutableStateOf(initialState.name) }
Column {
Text("This is for player $username")
Text("Session ID: [${initialState.id}]")
Card {
Column {
Button({ onRequestLogin() }) { Text("New Session") }
TextField(value = username, onValueChange = { username = it }, label = { Text("Username") })
Button(onClick = { onRequestPlayerRename(username) }) { Text("Update Player Name") }
Button(onClick = { onGetMarket() }) { Text("Get the Market") }
Button(onClick = { onRefresh() }) { Text("Refresh Player") }
MarketDisplay(initialState.lastMarket)
}
}
}
// form fields
}
@@ -0,0 +1,36 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.ui.api.ApiUi
interface DopeWarsModelUi<T> {
@Composable
fun view(model: T);
@Composable
fun form(model: T, onModelUpdate: (T) -> Unit)
}
data class DopeWarsUi(
val drug: DrugInfoUi,
val inventory: InventoryUi,
val inventoryItem: InventoryItemUi,
val location: LocationUi,
val market: MarketUi,
val player: PlayerUi,
val api: ApiUi
) {
companion object {
fun new() : DopeWarsUi {
return DopeWarsUi(
drug = DrugInfoUi(),
inventory = InventoryUi(),
inventoryItem = InventoryItemUi(),
location = LocationUi(),
market = MarketUi(),
player = PlayerUi(),
api = ApiUi.new()
)
}
}
}
@@ -0,0 +1,90 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.onClick
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import com.composables.icons.materialicons.MaterialIcons
import com.composables.icons.materialicons.filled.Menu
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest
class TravelUi {
/*
Wizard
Holds the users hand through picking a new location
*/
@Composable
fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) {
var stage by remember { mutableStateOf(0) }
var pickedLocation by remember { mutableStateOf(0) }
val locations = Locations.all
var expanded by remember { mutableStateOf(false) }
Window(onCloseRequest = { onTicketPunched(TravelRequest(destination = current.location)) }, title = "Select New Location") {
Card(modifier = Modifier.padding(16.dp).fillMaxSize()) {
Column {
Text("Wizard")
var index = 0;
when (stage) {
0 -> {
Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) {
Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
locations.forEach {
DropdownMenuItem(
text = { Text("$it.name / $index")},
onClick = {
expanded = false
pickedLocation = (index - 1)
println("location index to $index")
}
)
index += 1;
}
}
Button(onClick = {
stage += 1
}) {
Text("Next Step")
}
}
1 -> {
Text("Stage 1")
Button(onClick = {
println("Selected Location - $pickedLocation")
onTicketPunched(TravelRequest(locations[pickedLocation]))
}) {
Text("Finish")
}
}
}
}
}
}
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
data class ApiUi(
val marketRequest: MarketRequestUi,
val marketResponse: MarketResponseUi,
val playerNameUpdateRequest: PlayerNameUpdateRequestUi,
val playerNameUpdateResponse: PlayerNameUpdateResponseUi,
val tradeRequest: TradeRequestUi,
// TODO: tradeResponse
val travelRequest: TravelRequestUi,
val travelResponse: TravelResponseUi,
) {
companion object {
fun new(): ApiUi {
return ApiUi(
marketRequest = MarketRequestUi(),
marketResponse = MarketResponseUi(),
playerNameUpdateRequest = PlayerNameUpdateRequestUi(),
playerNameUpdateResponse = PlayerNameUpdateResponseUi(),
tradeRequest = TradeRequestUi(),
travelRequest = TravelRequestUi(),
travelResponse = TravelResponseUi()
)
}
}
}
@@ -0,0 +1,47 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
@Composable
override fun view(model: MarketRequest) {
BorderedCard("Market Request for ${model.location.name}") {
LocationUi().view(model.location)
}
}
@Composable
override fun form(
model: MarketRequest,
onModelUpdate: (MarketRequest) -> Unit
) {
var localModel by remember { mutableStateOf(model) }
BorderedCard("Market Request Form") {
Column {
LocationUi().form(model.location) {
localModel = MarketRequest(location = it)
}
Row {
Button(onClick = { onModelUpdate(localModel) }) {
Text("Apply")
}
Button(onClick = { localModel = model }) {
Text("Cancel")
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.MarketUi
class MarketResponseUi: DopeWarsModelUi<MarketResult> {
@Composable
override fun view(model: MarketResult) {
BorderedCard("Market Result for ${model.market.location.name}") {
MarketUi().view(model.market)
}
}
@Composable
override fun form(
model: MarketResult,
onModelUpdate: (MarketResult) -> Unit
) {
BorderedCard("Market Result Form") {
MarketUi().form(model.market) {
onModelUpdate(model)
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
class PlayerNameUpdateRequestUi(): DopeWarsModelUi<PlayerNameUpdateRequest> {
@Composable
override fun view(model: PlayerNameUpdateRequest) {
BorderedCard("Player Name Update Request for ${model.newName}") {
}
}
@Composable
override fun form(model: PlayerNameUpdateRequest, onModelUpdate: (PlayerNameUpdateRequest) -> Unit) {
Text("Player Name Update Request Form")
Button(onClick = { onModelUpdate(model) }) {
Text("Update with same data")
}
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> {
@Composable
override fun view(model: PlayerNameUpdateResponse) {
BorderedCard("Player Name Update Response") {
PlayerUi().view(model.updatedPlayer)
}
}
@Composable
override fun form(
model: PlayerNameUpdateResponse,
onModelUpdate: (PlayerNameUpdateResponse) -> Unit
) {
PlayerUi().form(model.updatedPlayer) {
onModelUpdate(model)
}
}
}
@@ -0,0 +1,50 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.ui.DopeWarsModelUi
class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
@Composable
override fun view(model: TradeRequest) {
BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") {
}
}
@Composable
override fun form(
model: TradeRequest,
onModelUpdate: (TradeRequest) -> Unit
) {
var localModel by remember { mutableStateOf(model) }
var actionIndex by remember { mutableStateOf(0) }
var quantity by remember { mutableStateOf(model.quantity) }
var drug by remember { mutableStateOf(model.drug) }
BorderedCard("Trade Requesst Form") {
Row {
Button(onClick = {
onModelUpdate(localModel)
println("Trade Request Form Apply Clicked")
}) {
Text("Apply")
}
Button(onClick = {
localModel = model
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.TravelRequest
class TravelRequestUi: DopeWarsModelUi<TravelRequest> {
@Composable
override fun view(model: TravelRequest) {
BorderedCard("Travel Request to ${model.destination.name}") {
}
}
@Composable
override fun form(
model: TravelRequest,
onModelUpdate: (TravelRequest) -> Unit
) {
BorderedCard("Travel Request Form") {
Text("Location Picker goes here.")
}
}
}
@@ -0,0 +1,54 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
import dev.geekback.dopewars.ui.MarketUi
class TravelResponseUi: DopeWarsModelUi<TravelResult> {
@Composable
override fun view(model: TravelResult) {
BorderedCard("Travel Result ${model.location.name}") {
LocationUi().view(model.location)
Text("Day: ${model.day}")
MarketUi().view(model.market)
}
}
@Composable
override fun form(model: TravelResult, onModelUpdate: (TravelResult) -> Unit) {
var day by remember { mutableStateOf(model.day) }
var location by remember { mutableStateOf(model.location) }
var market by remember { mutableStateOf(model.market) }
var localModel by remember { mutableStateOf(model) }
BorderedCard("Travel Result Form") {
LocationUi().form(model.location) {
println("Update location to $it")
}
Row {
Button(onClick = {}) {
Text("Apply")
}
Button(onClick = {}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,94 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.DRUG_MAX_PRICE
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
class DrugInfoUi: DopeWarsModelUi<DrugInfo> {
@Composable
override fun view(model: DrugInfo) {
BorderedCard(model.drug.name) {
Column {
Text("Price: ${model.minValue} - ${model.maxValue}")
}
}
}
@Composable
override fun form(
model: DrugInfo,
onModelUpdate: (DrugInfo) -> Unit
) {
var drug by remember { mutableStateOf(model) }
var name by remember { mutableStateOf(model.drug.name) }
var minPrice by remember { mutableFloatStateOf(model.minValue) }
var maxPrice by remember { mutableFloatStateOf(model.maxValue) }
BorderedCard("Drug Form") {
Column {
// Name
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
)
Row {
TextField(
value = minPrice.toString(),
onValueChange = { minPrice = it.toFloatOrNull() ?: minPrice },
label = { Text("Min Price") },
)
TextField(
value = maxPrice.toString(),
onValueChange = { maxPrice = it.toFloatOrNull() ?: maxPrice },
label = { Text("Max Price") },
)
}
RangeSlider(
value = (minPrice)..(maxPrice),
onValueChangeFinished = { },
valueRange = 1f..DRUG_MAX_PRICE,
onValueChange = { it ->
minPrice = it.start
maxPrice = it.endInclusive
}
)
Row {
Button(onClick = {
drug = DrugInfo(Drug.WEED , minPrice, maxPrice)
onModelUpdate(drug)
}) {
Text("Apply")
}
Button(onClick = {
name = Drug.WEED.name
minPrice = drug.minValue
maxPrice = drug.maxValue
}) {
Text("Cancel")
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package dev.geekback.dopewars.ui
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.InventoryItem
class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
@Composable
override fun view(model: List<InventoryItem>) {
BorderedCard("Inventory Items") {
}
}
@Composable
override fun form(
model: List<InventoryItem>,
onModelUpdate: (List<InventoryItem>) -> Unit
) {
BorderedCard("Inventory Items Form") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@@ -0,0 +1,19 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.InventoryItem
class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
@Composable
override fun view(model: InventoryItem) {
}
@Composable
override fun form(
model: InventoryItem,
onModelUpdate: (InventoryItem) -> Unit
) {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,55 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Location
class LocationUi: DopeWarsModelUi<Location> {
@Composable
override fun view(model: Location) {
BorderedCard("Location - ${model.name}") {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun form(
model: Location,
onModelUpdate: (Location) -> Unit
) {
var name by remember { mutableStateOf(model.name) }
var location by remember { mutableStateOf(model)}
BorderedCard("Location") {
TextField(value = name, label = {
Text("Location Name")
}, onValueChange = {
name = it
})
Row {
Button(onClick = {
location.name = name;
onModelUpdate(location)
}) {
Text("Apply")
}
Button(onClick = {
name = location.name
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,38 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Market
class MarketUi: DopeWarsModelUi<Market> {
@Composable
override fun view(model: Market) {
val prices = model.prices.toList()
BorderedCard("Market for ${model.location.name}") {
Column {
LazyColumn {
items(prices.size) { index ->
val item = prices[index];
val name = item.first.name
val price = item.second.toFloat()
Text("Drug $name $${price}")
}
}
}
}
}
@Composable
override fun form(
model: Market,
onModelUpdate: (Market) -> Unit
) {
BorderedCard("Market for ${model.location.name}") {
Text("Market goes here.")
}
}
}
@@ -0,0 +1,47 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.desktop.BorderedCard
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import kotlin.time.Instant
class PlayerUi: DopeWarsModelUi<Player> {
@Composable
override fun view(model: Player) {
BorderedCard(model.name) {
Column {
Text("ID: ${model.id}")
Text("Cash: $${model.cash}")
Text("Debt: $${model.debt}")
Text("Health: ${model.health}")
Text("Location: ${model.location.name}")
// TODO: this is an ugly memory hack
InventoryUi().view(model.inventory)
// marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf()))
val lastTime = Instant.fromEpochSeconds(model.lastActionTime)
Text("Last Action: $lastTime")
}
}
}
@Composable
override fun form(
model: Player,
onModelUpdate: (Player) -> Unit
) {
BorderedCard("Player ${model.name}") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@Composable
fun playerView(player: Player) {
}
+36
View File
@@ -0,0 +1,36 @@
[versions]
ktor = "3.5.1"
kotlin = "2.4.0"
compose = "1.11.1"
datetime = "0.8.0"
logback = "1.5.38"
serialization = "1.11.0"
[plugins]
compose = { id = "org.jetbrains.compose", version.ref="compose" }
ktor = { id = "io.ktor.plugin", version.ref="ktor" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref="kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
[libraries]
# KotlinX Modules
kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref="serialization" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name="kotlinx-datetime", version.ref="datetime"}
logback = { group = "ch.qos.logback", name ="logback-classic", version.ref = "logback" }
# KTOR Common
ktor-serialization-kotlinx-json = { group = "io.ktor", name="ktor-serialization-kotlinx-json", version.ref="ktor" }
# KTOR Server
ktor-server-swagger = { group = "io.ktor", name="ktor-server-swagger", version.ref="ktor"}
ktor-server-test-host = { group = "io.ktor", name="ktor-server-test-host", version.ref="ktor"}
ktor-server-openapi = { group = "io.ktor", name = "ktor-server-openapi", version.ref = "ktor" }
ktor-server-routing-openapi = { group = "io.ktor", name = "ktor-server-routing-openapi", version.ref = "ktor" }
ktor-server-content-negotiation = { group = "io.ktor", name="ktor-server-content-negotiation", version.ref="ktor" }
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-status-pages = { group ="io.ktor", name="ktor-server-status-pages", version.ref="ktor" }
ktor-server-logging = { group = "io.ktor", name="ktor-server-call-logging", version.ref="ktor" }
# KTOR client
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref="ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref="ktor" }
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+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>
+17
View File
@@ -0,0 +1,17 @@
rootProject.name = "dopewars"
include(":shared", ":server", ":desktop")
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
kotlin("multiplatform")
alias(libs.plugins.kotlin.serialization)
}
repositories {
mavenCentral()
}
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization)
implementation(libs.kotlinx.datetime)
}
jvmMain.dependencies {}
}
}
@@ -0,0 +1,22 @@
package dev.geekback.dopewars.shared
// import io.ktor.openapi.JsonSchema
import kotlinx.serialization.Serializable
import kotlin.random.Random
@Serializable
enum class Drug {
WEED,
COCAINE,
HASH,
ACID,
HEROIN,
MUSHROOMS,
METH;
companion object {
fun all() = listOf(WEED, COCAINE, HASH, ACID, HEROIN, MUSHROOMS, METH)
fun random() = Drug.WEED
}
}
@@ -0,0 +1,15 @@
package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
import kotlin.random.Random
@Serializable
data class DrugInfo(
val drug: Drug,
val minValue: Float,
val maxValue: Float,
) {
fun newPrice(): Float {
return Random.nextFloat() * (this.maxValue - this.minValue)
}
}
@@ -0,0 +1,15 @@
package dev.geekback.dopewars.shared
object Drugs {
val all = listOf(
DrugInfo(Drug.WEED, 300.0f, 1000.0f),
DrugInfo(Drug.ACID, 1000.0f, 4500.0f),
DrugInfo(Drug.COCAINE, 15000.0f, 30000.0f),
DrugInfo(Drug.HASH, 5000.0f, 14000.0f),
DrugInfo(Drug.MUSHROOMS, 500.0f, 2500.0f),
DrugInfo(Drug.METH, 2000.0f, 9000.0f),
DrugInfo(Drug.HEROIN, minValue = 4000.0f, 9500.0f)
)
fun random() = all.random()
}
@@ -0,0 +1,9 @@
package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
@Serializable
data class InventoryItem(
val drug: Drug,
var quantity: Int
)
@@ -0,0 +1,8 @@
package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
@Serializable
data class Location(
var name: String
)
@@ -0,0 +1,13 @@
package dev.geekback.dopewars.shared
object Locations {
val all = listOf(
Location("Brooklyn"),
Location("Bronx"),
Location("Queens"),
Location("Statten Island"),
)
fun random() = all.random()
}
@@ -0,0 +1,12 @@
package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
@Serializable
data class Market(
val location: Location,
val prices: Map<Drug, Float>
) {
companion object {
}
}
@@ -0,0 +1,27 @@
package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
import kotlin.time.Clock
@Serializable
data class Player(
val id: String,
val name: String,
var cash: Float = 2000f,
var debt: Float = 5500f,
var health: Int = 100,
var location: Location = Location("Brooklyn"),
var day: Int = 1,
val inventory: MutableList<InventoryItem> = mutableListOf(),
var lastMarket: Market? = null,
val lastActionTime: Long = Clock.System.now().epochSeconds
) {
companion object {
fun new(): Player {
return Player(
id = "",
name = "Default Player",
)
}
}
}
@@ -0,0 +1,3 @@
package dev.geekback.dopewars.shared
const val DRUG_MAX_PRICE: Float = 100_000f;
@@ -0,0 +1,9 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Location
import kotlinx.serialization.Serializable
@Serializable
data class MarketRequest(
val location: Location
)
@@ -0,0 +1,9 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Market
import kotlinx.serialization.Serializable
@Serializable
data class MarketResult(
val market: Market) {
}
@@ -0,0 +1,8 @@
package dev.geekback.dopewars.shared.api
import kotlinx.serialization.Serializable
@Serializable
data class PlayerNameUpdateRequest(
val newName: String
)
@@ -0,0 +1,7 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Player
import kotlinx.serialization.Serializable
@Serializable
data class PlayerNameUpdateResponse(val updatedPlayer: Player)
@@ -0,0 +1,10 @@
package dev.geekback.dopewars.shared.api
import kotlinx.serialization.Serializable
@Serializable
enum class TradeAction {
BUY,
SELL
}
@@ -0,0 +1,13 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Drug
import kotlinx.serialization.Serializable
@Serializable
data class TradeRequest(
val action: TradeAction,
val drug: Drug,
val quantity: Int
)
@@ -0,0 +1,9 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Location
import kotlinx.serialization.Serializable
@Serializable
data class TravelRequest(
val destination: Location
)
@@ -0,0 +1,12 @@
package dev.geekback.dopewars.shared.api
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import kotlinx.serialization.Serializable
@Serializable
data class TravelResult(
val location: Location,
val day: Int,
val market: Market
)