Exit Full View

Games Cupboard / gamescupboard-server / src / main / kotlin / uk / co / nickthecoder / gamescupboard / server / Game.kt

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()
    }

}