package uk.co.nickthecoder.gamescupboard.server
import Message
import ChangeZOrder
import ResetGame
import io.ktor.websocket.*
import uk.co.nickthecoder.gamescupboard.common.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicInteger
class Game(
val gameType: GameType,
val gameVariation: GameVariation,
val label: String,
/**
* The expected number of players.
*/
var seatCount: Int,
val invitationCode: String? = null,
val baseVariation: GameVariation = gameVariation
) {
/**
* Generates ids for [GameObject]s.
*/
val id = objecIdGenerator.getAndIncrement()
/**
* The list of players connected to this game.
* NOTE, in many games, there is a label, or an avatar, which represents each player.
* These are NOT included here, and are instead in [playingObjects].
*/
val connectedPlayers = ConcurrentHashMap<Int, ConnectedPlayer>()
private val gameObjectIdGenerator = AtomicInteger().apply { set(gameVariation.playingObjects.size) }
var lastActive: Date = Date()
/**
* See [generatePlayerIdFromPreferredSeatNumber]
*/
private val freeSeats = Collections.synchronizedList((1..seatCount).toMutableList())
/**
* See [generatePlayerIdFromPreferredSeatNumber]
*/
private val spectatorIdGenerator = AtomicInteger(gameVariation.maxPlayers + 1)
/**
* The list of all objects in the game, such as cards, pieces, player's names...
* Noes NOT include background images, but does include GUI components, such as Buttons.
*/
private val playingObjects = ConcurrentLinkedDeque<GameObject>()
/**
* Contains the same items as [playingObjects], but keyed by their id.
*/
private val gameObjectsById = ConcurrentHashMap<Int, GameObject>()
/**
* The named items from [playingObjects], keyed by their name.
*/
private val gameObjectsByName = ConcurrentHashMap<String, GameObject>()
private val avatars = ConcurrentHashMap<Int, Avatar>()
var allowSpectators: Boolean = true
val binnedObjects = mutableListOf<GameObject>()
init {
for (go in gameVariation.playingObjects) {
add(go.copy())
}
}
fun avatars(): List<Avatar> = avatars.values.toList()
fun findAvatar(playerId: Int) = avatars[playerId]
fun findPrivateAreaAt(x: Int, y: Int): SpecialArea? {
return gameVariation.specialAreas.firstOrNull {
it.areaType == AreaType.PRIVATE && it.contains(x, y)
}
}
/**
* Returns a unique player id (for the game).
* If [seatNumber] isn't already occupied, then that is returned.
* Otherwise, it returns the first available seat.
* If there are no seats available, then an atomically increasing number is returned
* (which should be for spectators only - but spectators hasn't been written yet!)
*/
fun generatePlayerIdFromPreferredSeatNumber(seatNumber: Int): Int {
if (freeSeats.remove(seatNumber)) return seatNumber
return freeSeats.removeAt(0) // This will THROW if there are no more seats left.
}
fun generatePlayerIdForSpectator() = spectatorIdGenerator.getAndIncrement()
fun generateObjectId() = gameObjectIdGenerator.incrementAndGet()
fun isFull() = freeSeats.size == 0
fun backgroundObjects() = gameVariation.backgroundObjects
fun playingObjects() = playingObjects.toList()
fun foregroundObjects() = gameVariation.foregroundObjects
fun findObject(id: Int) = gameObjectsById[id]
fun findObjectByName(name: String) = gameObjectsByName[name]
/**
* Removes the game objects from the [GameVariation] as well as clones created from ImageObject.regenerate.
* Create new copies of the objects from [GameVariation].
* Returns the newly added [GameObject]s.
*/
suspend fun reset() {
binnedObjects.clear()
val newObjects = gameVariation.playingObjects.map { it.copy() }
val fromId = newObjects.minOfOrNull { it.id } ?: 0
val toId = newObjects.maxOfOrNull { it.id } ?: 0
val removeObjects = playingObjects.filter { it.id in fromId..toId || (it is ImageObject && it.isCopy) }
removeObjects.forEach {
remove(it)
}
for (go in newObjects) {
add(go)
}
send(ResetGame(removeObjects.map { it.id }, newObjects))
}
fun add(connectedPlayer: ConnectedPlayer) {
connectedPlayers[connectedPlayer.id] = connectedPlayer
}
fun remove(connectedPlayer: ConnectedPlayer) {
if (!connectedPlayer.isSpectator) {
freeSeats.add(connectedPlayer.id)
}
connectedPlayers.remove(connectedPlayer.id)
}
fun addAvatar(connectedPlayer: ConnectedPlayer, avatar: Avatar) {
add(avatar)
avatars[connectedPlayer.id] = avatar
}
fun removeAvatar(connectedPlayer: ConnectedPlayer): Avatar? {
avatars.remove(connectedPlayer.id)?.let { avatar ->
remove(avatar)
return avatar
}
return null
}
fun add(gameObject: GameObject) {
playingObjects.add(gameObject)
gameObjectsById[gameObject.id] = gameObject
if (gameObject.name.isNotBlank()) {
gameObjectsByName[gameObject.name] = gameObject
}
}
fun add(goList: List<GameObject>) {
for (go in goList) {
add(go)
}
}
fun remove(gameObject: GameObject) {
playingObjects.remove(gameObject)
gameObjectsById.remove(gameObject.id)
gameObjectsByName.remove(gameObject.name)
}
fun removeById(ids: List<Int>) {
for (id in ids) {
gameObjectsById[id]?.let { remove(it) }
}
}
fun remove(goList: List<GameObject>) {
for (go in goList) {
remove(go)
}
}
fun moveZ(gameObject: GameObject, changeZOrder: ChangeZOrder) {
when (changeZOrder) {
ChangeZOrder.TOP -> {
playingObjects.remove(gameObject)
playingObjects.add(gameObject)
}
ChangeZOrder.BOTTOM -> {
playingObjects.remove(gameObject)
playingObjects.addFirst(gameObject)
}
ChangeZOrder.NO_CHANGE -> Unit
}
}
suspend fun send(message: Message) {
send(message.toJsonPacket())
}
suspend fun send(frameText: String) {
connectedPlayers.values.forEach { it.session.send(frameText) }
}
suspend fun sendToOthers(excludingPlayerId: Int, message: Message) {
sendToOthers(excludingPlayerId, message.toJsonPacket())
}
suspend fun sendToOthers(excludingPlayerId: Int, frameText: String) {
connectedPlayers.values.filter {
it.id != excludingPlayerId
}.forEach {
it.session.send(frameText)
}
}
override fun toString() = "Game #$id objects=${playingObjects.size} players=${connectedPlayers.size}"
companion object {
private val objecIdGenerator = AtomicInteger()
}
}