Skip to content

Commit ecc3cf9

Browse files
committed
Add ActionListRequestHandler and step-by-step construct actions
1 parent e020e22 commit ecc3cf9

File tree

10 files changed

+194
-21
lines changed

10 files changed

+194
-21
lines changed

games-core/src/main/kotlin/net/zomis/games/dsl/DslSplendor.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ fun <K, V> Map<K, V>.mergeWith(other: Map<K, V>, merger: (V?, V?) -> V): Map<K,
3030
}
3131
}
3232

33-
enum class MoneyType { BLACK, WHITE, RED, BLUE, GREEN }
33+
enum class MoneyType {
34+
BLACK, WHITE, RED, BLUE, GREEN;
35+
36+
fun toMoney(count: Int): Money {
37+
return Money(mutableMapOf(this to count))
38+
}
39+
}
3440
data class Money(val moneys: MutableMap<MoneyType, Int> = mutableMapOf()) {
3541
// TODO: Add wildcards as a separate field
3642
constructor(vararg money: Pair<MoneyType, Int>) : this(mutableMapOf<MoneyType, Int>(*money))
@@ -56,6 +62,10 @@ data class MoneyChoice(val moneys: List<MoneyType>) {
5662
}
5763
}
5864

65+
fun startingStockForPlayerCount(playerCount: Int): Int {
66+
return 10
67+
}
68+
5969
class SplendorGame(playerCount: Int = 2) {
6070

6171
// TODO: Add nobles
@@ -71,7 +81,7 @@ class SplendorGame(playerCount: Int = 2) {
7181
val deck: MutableList<Card> = cardLevels.flatMap { level ->
7282
(0 until 30).map { randomCard(level) }
7383
}.toMutableList()
74-
var stock: Money = Money()
84+
var stock: Money = MoneyType.values().fold(Money()) {money, type -> money + type.toMoney(startingStockForPlayerCount(playerCount))}
7585
var currentPlayerIndex: Int = 0
7686

7787
init {
@@ -165,12 +175,16 @@ class DslSplendor {
165175
}
166176
}
167177
allowed {
168-
// if (it.parameter.isTwoOfSame()) {
169-
// check if there's at least 4 left of each kind
170-
// } else {
171-
// check if there's at least 1 left of each kind
172-
// }
173-
true
178+
val moneyChosen = it.parameter.toMoney()
179+
if (!it.game.stock.has(moneyChosen)) {
180+
return@allowed false
181+
}
182+
val chosen = it.parameter.moneys
183+
return@allowed when {
184+
chosen.size == 2 -> chosen.distinct().size == 1 && it.game.stock.has(moneyChosen.plus(moneyChosen))
185+
chosen.size == 3 -> chosen.distinct().size == chosen.size
186+
else -> false
187+
}
174188
}
175189
effect {
176190
it.game.stock -= it.parameter.toMoney()

games-core/src/main/kotlin/net/zomis/games/dsl/DslUR.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ class DslUR {
99
val gameUR = createGame<RoyalGameOfUr>("UR") {
1010
setup(Unit::class) {
1111
// players(2)
12-
defaultConfig { Unit }
1312
init { RoyalGameOfUr() }
1413
}
1514
logic {

games-core/src/main/kotlin/net/zomis/games/dsl/impl/ActionsImpl.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package net.zomis.games.dsl.impl
33
import net.zomis.games.dsl.*
44
import kotlin.reflect.KClass
55

6+
// TODO: Can we reduce generics here? get rid of the `A : Actionable`? Only `GameLogicActionType2D` is special.
67
interface GameLogicActionType<T : Any, P : Any, A : Actionable<T, P>> {
78
val actionType: String
89
fun availableActions(playerIndex: Int): Iterable<A>
@@ -107,7 +108,35 @@ class GameLogicActionTypeComplexNext<T : Any, A : Any>(val yielder: (A) -> Unit)
107108
next.invoke(nextScope, it)
108109
}
109110
}
111+
}
112+
113+
class GameLogicActionTypeComplexNextOnly<T : Any, A : Any>(
114+
private val chosen: List<Any>,
115+
private val evaluateOptions: Boolean,
116+
private val nextYielder: (List<Any>) -> Unit, private val actionYielder: (A) -> Unit): ActionComplexScopeResultNext<T, A> {
117+
override fun actionParameter(action: A) {
118+
actionYielder(action)
119+
}
120+
121+
override fun <E : Any> option(options: Array<E>, next: ActionComplexScopeResultNext<T, A>.(E) -> Unit) {
122+
if (chosen.isNotEmpty()) {
123+
val nextChosenIndex = chosen[0]
124+
val nextChosen = options[nextChosenIndex as Int] // TODO: Add string support or something
125+
val nextChosenList = chosen.subList(1, chosen.size)
110126

127+
val nextScope = GameLogicActionTypeComplexNextOnly<T, A>(nextChosenList,true, nextYielder, actionYielder)
128+
next.invoke(nextScope, nextChosen)
129+
} else {
130+
nextYielder(options.toList())
131+
if (!evaluateOptions) {
132+
return
133+
}
134+
options.forEach {
135+
val nextScope = GameLogicActionTypeComplexNextOnly<T, A>(emptyList(), false, {}, actionYielder)
136+
next.invoke(nextScope, it)
137+
}
138+
}
139+
}
111140
}
112141

113142
class GameLogicActionTypeComplex<T : Any, A : Any>(override val actionType: String,
@@ -119,6 +148,17 @@ class GameLogicActionTypeComplex<T : Any, A : Any>(override val actionType: Stri
119148
this.options = options
120149
}
121150

151+
fun availableOptionsNext(playerIndex: Int, chosen: List<Any>): Pair<List<Any>, List<A>> {
152+
val nexts = mutableListOf<Any>()
153+
val actionParams = mutableListOf<A>()
154+
val yielder: (List<Any>) -> Unit = { nexts.addAll(it) }
155+
val actionYielder: (A) -> Unit = { actionParams.add(it) }
156+
157+
val nextScope = GameLogicActionTypeComplexNextOnly<T, A>(chosen, true, yielder, actionYielder)
158+
this.options.invoke(nextScope)
159+
return nexts to actionParams
160+
}
161+
122162
override fun availableActions(playerIndex: Int): Iterable<Action<T, A>> {
123163
val result = mutableListOf<A>()
124164
val yielder: (A) -> Unit = { result.add(it) }
@@ -191,6 +231,7 @@ class GameLogicContext<T : Any>(private val model: T, private val replayState: R
191231

192232
}
193233

234+
data class ActionInfo<P>(val nextOptions: List<Any>, val parameters: List<P>)
194235
class ActionTypeImplEntry<T : Any, P : Any, A : Actionable<T, P>>(private val model: T,
195236
private val replayState: ReplayState,
196237
private val actionType: ActionType<P>,
@@ -208,6 +249,17 @@ class ActionTypeImplEntry<T : Any, P : Any, A : Actionable<T, P>>(private val mo
208249
}
209250
fun createAction(playerIndex: Int, parameter: P): A = impl.createAction(playerIndex, parameter)
210251
fun isAllowed(action: A): Boolean = impl.actionAllowed(action)
252+
fun availableParameters(playerIndex: Int, previouslySelected: List<Any>): ActionInfo<P> {
253+
return if (impl is GameLogicActionTypeComplex) {
254+
val actionInfo = impl.availableOptionsNext(playerIndex, previouslySelected)
255+
ActionInfo(actionInfo.first, actionInfo.second)
256+
} else {
257+
if (previouslySelected.isNotEmpty()) {
258+
throw IllegalArgumentException("Unable to select any options for action ${actionType.name}")
259+
}
260+
ActionInfo(emptyList(), availableActions(playerIndex).map { it.parameter })
261+
}
262+
}
211263

212264
val name: String
213265
get() = actionType.name

games-server/src/main/kotlin/net/zomis/common/Utils.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ fun convertFromDBFormat(obj: Any?): Any? {
2929
return when (obj) {
3030
null -> null
3131
is BigDecimal -> obj.toInt()
32-
is String -> obj
33-
is Boolean -> obj
3432
is Map<*, *> -> {
3533
val map = obj as Map<String, *>
3634
map.mapValues { convertFromDBFormat(it.value) }

games-server/src/main/kotlin/net/zomis/games/dsl/DslConsoleView.kt

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import net.zomis.games.dsl.impl.*
66
import net.zomis.games.server2.Server2
77
import net.zomis.games.server2.ServerConfig
88
import net.zomis.games.server2.clients.FakeClient
9+
import net.zomis.games.server2.games.GameSystem
10+
import net.zomis.games.server2.handlers.games.ActionListRequestHandler
911
import java.util.Scanner
1012
import java.util.UUID
1113

@@ -26,6 +28,17 @@ class DslConsoleView<T : Any>(private val game: GameSpec<T>) {
2628
}
2729
}
2830

31+
fun choiceActionable(actionLogic: ActionTypeImplEntry<T, Any, Actionable<T, Any>>, playerIndex: Int, scanner: Scanner): Actionable<T, Any>? {
32+
val options = actionLogic.availableActions(playerIndex).toList()
33+
options.forEachIndexed { index, actionable -> println("$index. $actionable") }
34+
if (options.size <= 1) { return options.getOrNull(0) }
35+
else {
36+
println("Choose your action.")
37+
val actionIndex = scanner.nextLine().toIntOrNull()
38+
return options.getOrNull(actionIndex ?: -1)
39+
}
40+
}
41+
2942
fun queryInput(game: GameImpl<T>, scanner: Scanner): Boolean {
3043
println("Available actions is: ${game.actions.actionTypes}. Who is playing and what is your action?")
3144
val line = scanner.nextLine()
@@ -49,14 +62,8 @@ class DslConsoleView<T : Any>(private val game: GameSpec<T>) {
4962
actionLogic.createAction(playerIndex.toInt(), Point(x, y))
5063
}
5164
else -> {
52-
val options = actionLogic.availableActions(playerIndex.toInt()).toList()
53-
options.forEachIndexed { index, actionable -> println("$index. $actionable") }
54-
if (options.size <= 1) { options.getOrNull(0) }
55-
else {
56-
println("Choose your action.")
57-
val actionIndex = scanner.nextLine().toIntOrNull()
58-
options.getOrNull(actionIndex ?: -1)
59-
}
65+
stepByStepActionable(game, playerIndex.toInt(), actionType, scanner)
66+
// choiceActionable(actionLogic, playerIndex.toInt(), scanner)
6067
}
6168
}
6269
if (action == null) {
@@ -71,6 +78,34 @@ class DslConsoleView<T : Any>(private val game: GameSpec<T>) {
7178
return allowed
7279
}
7380

81+
private fun stepByStepActionable(game: GameImpl<T>, playerIndex: Int, moveType: String, scanner: Scanner): Actionable<T, Any>? {
82+
val reqHandler = ActionListRequestHandler(GameSystem())
83+
84+
val chosen = mutableListOf<Any>()
85+
while (true) {
86+
val act = reqHandler.availableActionsMessage(game, playerIndex, moveType, chosen).singleOrNull()
87+
?: return null
88+
89+
println(act.first)
90+
println(" Next " + act.second.nextOptions.size + ". Params " + act.second.parameters.size)
91+
val next = act.second.nextOptions
92+
next.forEachIndexed { index, value -> println("$index. Next $value") }
93+
94+
val params = act.second.parameters
95+
params.forEachIndexed { index, value -> println("${index + next.size}. Choice $value") }
96+
97+
val choice = scanner.nextLine().toIntOrNull() ?: return null
98+
if (choice >= next.size) {
99+
val param = params.getOrNull(choice - next.size) ?: return null
100+
return game.actions.type(moveType)?.createAction(playerIndex, param)
101+
} else {
102+
// val chosenNext = next.getOrNull(choice) ?: return null
103+
// chosen.add(chosenNext)
104+
chosen.add(choice)
105+
}
106+
}
107+
}
108+
74109
fun display(indentation: Int, name: String, data: Any?) {
75110
val prefix = (0 until indentation).joinToString("") { " " }
76111
when (data) {

games-server/src/main/kotlin/net/zomis/games/server2/Server2.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import net.zomis.games.server2.debug.AIGames
2020
import net.zomis.games.server2.games.*
2121
import net.zomis.games.server2.games.impl.ECSGameSystem
2222
import net.zomis.games.server2.games.impl.TTControllerSystem
23+
import net.zomis.games.server2.handlers.games.ActionListRequestHandler
2324
import net.zomis.games.server2.handlers.games.ViewRequestHandler
2425
import net.zomis.games.server2.invites.InviteSystem
2526
import net.zomis.games.server2.invites.LobbySystem
@@ -100,6 +101,7 @@ class Server2(val events: EventSystem) {
100101

101102
val gameSystem = GameSystem()
102103
private val messageHandler = MessageHandler(events, mapOf(
104+
"ActionListRequest" to ActionListRequestHandler(gameSystem),
103105
"ViewRequest" to ViewRequestHandler(gameSystem)
104106
))
105107

games-server/src/main/kotlin/net/zomis/games/server2/db/DBGame.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class DBGame(
2121

2222
init {
2323
moveHistory.forEachIndexed { index, it ->
24-
// TODO: Set replay mode in replayState
2524
val logic = game.actions[it.moveType]
2625
?: throw BadReplayException("Unable to perform $it: No such move type")
2726
val param = if (it.move == null) Unit

games-server/src/main/kotlin/net/zomis/games/server2/games/GameSystem.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class ServerGame(val gameType: GameType, val gameId: String, val gameMeta: Serve
3333
return players.indexOf(client).takeIf { it >= 0 }
3434
}
3535

36+
fun verifyPlayerIndex(client: Client, playerIndex: Int): Boolean {
37+
return players.getOrNull(playerIndex) == client
38+
}
39+
3640
internal val players: MutableList<Client> = mutableListOf()
3741
var obj: Any? = null
3842

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package net.zomis.games.server2.handlers.games
2+
3+
import klog.KLoggers
4+
import net.zomis.games.dsl.impl.ActionInfo
5+
import net.zomis.games.dsl.impl.GameImpl
6+
import net.zomis.games.server2.ClientJsonMessage
7+
import net.zomis.games.server2.IncomingMessageHandler
8+
import net.zomis.games.server2.games.GameSystem
9+
import net.zomis.games.server2.getTextOrDefault
10+
11+
class ActionListRequestHandler(private val gameSystem: GameSystem): IncomingMessageHandler {
12+
private val logger = KLoggers.logger(this)
13+
14+
fun availableActionsMessage(obj: GameImpl<*>, playerIndex: Int, moveType: String?, chosen: List<Any>?): List<Pair<String, ActionInfo<Any>>> {
15+
if (moveType != null) {
16+
val actionType = obj.actions.type(moveType)
17+
return if (actionType != null) {
18+
listOf(actionType.name to actionType.availableParameters(playerIndex, chosen ?: emptyList()))
19+
} else {
20+
emptyList()
21+
}
22+
} else {
23+
return obj.actions.types().map {
24+
it.name to it.availableParameters(playerIndex, emptyList())
25+
}
26+
}
27+
}
28+
29+
override fun invoke(message: ClientJsonMessage) {
30+
val gameType = message.data.getTextOrDefault("gameType", "")
31+
val type = gameSystem.getGameType(gameType)
32+
if (type == null) {
33+
logger.error("No such gameType: $gameType")
34+
return
35+
}
36+
val gameId = message.data.getTextOrDefault("gameId", "")
37+
val game = type.runningGames[gameId]
38+
if (game == null) {
39+
logger.error("No such game: $gameId of type $gameType")
40+
return
41+
}
42+
43+
if (game.obj !is GameImpl<*>) {
44+
logger.error("Game $gameId of type $gameType is not a valid DSL game")
45+
return
46+
}
47+
48+
val obj = game.obj as GameImpl<*>
49+
val playerIndex = message.data.getTextOrDefault("playerIndex", "-1").toInt()
50+
if (!game.verifyPlayerIndex(message.client, playerIndex)) {
51+
logger.error("Client ${message.client} does not have index $playerIndex in Game $gameId of type $gameType")
52+
return
53+
}
54+
55+
val moveType = message.data.get("moveType")?.asText()
56+
val chosen = message.data.get("chosen")?.map { if (it.isInt) it.asInt() else it.asText() } ?: emptyList()
57+
58+
val actionParams = availableActionsMessage(obj, playerIndex, moveType, chosen)
59+
60+
logger.info { "Sending action list data for $gameId of type $gameType to $playerIndex" }
61+
message.client.send(mapOf(
62+
"type" to "ActionList",
63+
"gameType" to gameType,
64+
"gameId" to gameId,
65+
"playerIndex" to playerIndex,
66+
"actions" to actionParams
67+
))
68+
}
69+
70+
}

games-server/src/main/kotlin/net/zomis/games/server2/handlers/games/ViewRequestHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import net.zomis.games.server2.IncomingMessageHandler
77
import net.zomis.games.server2.games.GameSystem
88
import net.zomis.games.server2.getTextOrDefault
99

10-
class ViewRequestHandler(val gameSystem: GameSystem): IncomingMessageHandler {
10+
class ViewRequestHandler(private val gameSystem: GameSystem): IncomingMessageHandler {
1111
private val logger = KLoggers.logger(this)
1212

1313
override fun invoke(message: ClientJsonMessage) {

0 commit comments

Comments
 (0)