Skip to content

Commit fde3c81

Browse files
committed
added logic to game server
1 parent d4d1e62 commit fde3c81

15 files changed

Lines changed: 265 additions & 87 deletions

File tree

watch/app/src/main/java/com/imsproject/watch/utils/Angle.kt renamed to common/src/main/java/com/imsproject/common/utils/Angle.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package com.imsproject.watch.utils
1+
package com.imsproject.common.utils
22

3-
import androidx.annotation.IntRange
4-
import com.imsproject.watch.UNDEFINED_ANGLE
53
import kotlin.math.absoluteValue
64

5+
const val UNDEFINED_ANGLE = 600f
6+
77
/**
88
* This class represents an angle in degrees in the range of (-180,180]
99
*
@@ -105,7 +105,7 @@ class Angle(
105105
}
106106

107107

108-
private fun isInQuadrant(@IntRange(1,4) quadrant: Int) : Boolean {
108+
private fun isInQuadrant(quadrant: Int) : Boolean {
109109
return floatValue.isInQuadrant(quadrant)
110110
}
111111

@@ -121,7 +121,7 @@ class Angle(
121121
*
122122
* meaning, clockwise side of the quadrant is inclusive and the counter-clockwise side is exclusive
123123
*/
124-
private fun Float.isInQuadrant(@IntRange(1,4) quadrant: Int) : Boolean {
124+
private fun Float.isInQuadrant(quadrant: Int) : Boolean {
125125
return when(quadrant){
126126
1 -> 0f < this && this <= 90f
127127
2 -> 90f < this && this <= 180f
@@ -152,6 +152,6 @@ class Angle(
152152
}
153153
}
154154

155-
fun undefined(): Angle = Angle(UNDEFINED_ANGLE)
155+
val undefined: Angle = Angle(UNDEFINED_ANGLE)
156156
}
157157
}

game_server/src/main/kotlin/com/imsproject/gameserver/business/GameController.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import java.util.concurrent.ConcurrentHashMap
1717
@Component
1818
class GameController(
1919
private val clients: ClientController,
20-
private val timeServer: TimeServerHandler,
2120
private val lobbies: LobbyController
2221
) {
2322

@@ -84,7 +83,7 @@ class GameController(
8483
}
8584
log.debug("onClientConnect: Player was in game, rejoining player to game")
8685
GameRequest.builder(Type.RECONNECT_TO_GAME)
87-
.timestamp(game.startTime.toString())
86+
.timestamp(game.timeServerStartTime.toString())
8887
.build().toJson()
8988
.also{ client.sendTcp(it) } // notify the client
9089

@@ -166,7 +165,7 @@ class GameController(
166165
clientIdToGame[player2Id] = game
167166

168167
// game.startGame() notifies the clients
169-
game.startGame(timeServer.timeServerCurrentTimeMillis(), sessionId)
168+
game.startGame(sessionId)
170169

171170
log.debug("startGame() successful")
172171
}

game_server/src/main/kotlin/com/imsproject/gameserver/business/TimeServerHandler.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.springframework.context.event.EventListener
1212
import org.springframework.stereotype.Component
1313
import java.io.IOException
1414
import java.net.SocketTimeoutException
15+
import java.util.concurrent.Semaphore
1516

1617
@Component
1718
class TimeServerHandler {
@@ -28,6 +29,7 @@ class TimeServerHandler {
2829
private set
2930

3031
private val timeServerUdp = UdpClient()
32+
private val lock = Semaphore(1,true)
3133

3234
/**
3335
* Sends a request to the time server to get the current time in milliseconds.
@@ -39,6 +41,7 @@ class TimeServerHandler {
3941
* @throws IOException If there is an I/O error while sending or receiving the request.
4042
*/
4143
fun timeServerCurrentTimeMillis(): Long {
44+
lock.acquire()
4245
val request = TimeRequest.request(TimeRequest.Type.CURRENT_TIME_MILLIS).toJson()
4346
try{
4447
val startTime = System.currentTimeMillis()
@@ -57,28 +60,50 @@ class TimeServerHandler {
5760
} catch (e: IOException){
5861
log.error("Failed to fetch time", e)
5962
throw e
63+
} finally {
64+
lock.release()
6065
}
6166
}
6267

6368
private fun run(){
69+
val request = TimeRequest.request(TimeRequest.Type.CURRENT_TIME_MILLIS).toJson()
6470
while(true){
71+
Thread.sleep(10000)
6572
try{
66-
val data : List<Long> = List(100) {
67-
val currentLocal = System.currentTimeMillis()
68-
val currentTimeServer = timeServerCurrentTimeMillis()
69-
currentLocal-currentTimeServer
73+
lock.acquire()
74+
var count = 0
75+
val data = mutableListOf<Long>()
76+
while(count < 100){
77+
try {
78+
val currentLocalTime = System.currentTimeMillis()
79+
timeServerUdp.send(request)
80+
val response = timeServerUdp.receive()
81+
val timeDelta = System.currentTimeMillis() - currentLocalTime
82+
val timeResponse = fromJson<TimeRequest>(response)
83+
val currentServerTime = timeResponse.time!! - timeDelta / 2 // approximation
84+
data.add(currentLocalTime-currentServerTime)
85+
count++
86+
} catch(e: SocketTimeoutException){
87+
log.error("Time request timeout", e)
88+
} catch(e: JsonParseException){
89+
log.error("Failed to parse time response", e)
90+
} catch (e: IOException){
91+
log.error("Failed to fetch time", e)
92+
}
7093
}
7194
timeServerDelta = data.average().toLong()
7295
} catch(e: Exception){
7396
log.error("Failed to fetch time from time server", e)
7497
continue
98+
} finally {
99+
lock.release()
75100
}
76-
Thread.sleep(10000)
77101
}
78102
}
79103

80104
@EventListener
81105
fun onApplicationReady(event: ApplicationReadyEvent){
106+
instance = this
82107
timeServerIp = if(runningLocal) "localhost" else "host.docker.internal"
83108
// set up udp client for time server
84109
timeServerUdp.remoteAddress = timeServerIp
@@ -89,6 +114,10 @@ class TimeServerHandler {
89114
}
90115

91116
companion object {
117+
lateinit var instance: TimeServerHandler
92118
private val log = LoggerFactory.getLogger(TimeServerHandler::class.java)
93119
}
120+
121+
122+
94123
}

game_server/src/main/kotlin/com/imsproject/gameserver/business/games/FlourMillGame.kt

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ package com.imsproject.gameserver.business.games
22

33
import com.imsproject.common.gameserver.GameAction
44
import com.imsproject.common.gameserver.GameRequest
5+
import com.imsproject.common.utils.Angle
56
import com.imsproject.common.utils.toJson
67
import com.imsproject.gameserver.business.ClientHandler
8+
import com.imsproject.gameserver.business.TimeServerHandler
9+
import kotlinx.coroutines.*
710
import org.slf4j.LoggerFactory
11+
import java.util.concurrent.Semaphore
12+
import java.util.concurrent.atomic.AtomicLong
13+
import kotlin.math.abs
814

915

1016
class FlourMillGame(
@@ -13,24 +19,137 @@ class FlourMillGame(
1319
player2: ClientHandler
1420
) : Game(lobbyId, player1, player2) {
1521

22+
private var packetNumber = 0L
23+
24+
// player 1 data
25+
private var player1TouchPoint = -1f to Angle.undefined
26+
private var player1InBounds = false
27+
private var player1LastUpdate = 0L
28+
private var player1LastSequenceNumber = 0L
29+
30+
// player 2 data
31+
private var player2TouchPoint = -1f to Angle.undefined
32+
private var player2InBounds = false
33+
private var player2LastUpdate = 0L
34+
private var player2LastSequenceNumber = 0L
35+
36+
// axle data
37+
private var axleAngle = Angle.undefined
38+
39+
private val scope = CoroutineScope(Dispatchers.Default)
40+
41+
private suspend fun manageAxle(){
42+
while(true){
43+
delay(16)
44+
val timestamp = System.currentTimeMillis() - localStartTime
45+
if(axleAngle == Angle.undefined) {
46+
if(player1TouchPoint.first > 0f && player2TouchPoint.first > 0f){
47+
if(player1LastUpdate > player2LastUpdate) {
48+
axleAngle = player1TouchPoint.second + Angle(90f)
49+
} else {
50+
axleAngle = player2TouchPoint.second + Angle(-90f)
51+
}
52+
}
53+
} else {
54+
if(player1TouchPoint.first > 0f && player2TouchPoint.first > 0f) {
55+
if(player1InBounds && player2InBounds){
56+
val player1Angle = player1TouchPoint.second
57+
val player2Angle = player2TouchPoint.second
58+
val player1SideAngle = axleAngle + -90f
59+
val player2SideAngle = axleAngle + 90f
60+
val player1AngleDiff = player1Angle - player1SideAngle
61+
val player2AngleDiff = player2Angle - player2SideAngle
62+
val player1Direction = if(Angle.isClockwise(player1SideAngle,player1Angle)) 1 else -1
63+
val player2Direction = if(Angle.isClockwise(player2SideAngle, player2Angle)) 1 else -1
64+
if(player1AngleDiff > 0 && player2AngleDiff > 0 && player1Direction == player2Direction){
65+
val amountToRotate = abs(player1AngleDiff - player2AngleDiff)
66+
axleAngle += amountToRotate * player1Direction
67+
}
68+
}
69+
}
70+
}
71+
sendGameAction(GameAction.builder(GameAction.Type.USER_INPUT)
72+
.actor("system")
73+
.timestamp(timestamp.toString())
74+
.sequenceNumber(packetNumber++)
75+
.data("$axleAngle")
76+
.build())
77+
}
78+
79+
}
80+
1681
override fun handleGameAction(actor: ClientHandler, action: GameAction) {
1782
when(action.type) {
1883
GameAction.Type.USER_INPUT -> {
19-
sendGameAction(action)
84+
val dataSplit = action.data?.split(",") ?: run {
85+
log.error("Missing data in USER_INPUT GameAction")
86+
return
87+
}
88+
if (dataSplit.size != 3) {
89+
log.error("Invalid data in USER_INPUT GameAction: $dataSplit")
90+
return
91+
}
92+
val timestamp = action.timestamp?.toLong() ?: run {
93+
log.error("Missing timestamp in USER_INPUT GameAction")
94+
return
95+
}
96+
val sequenceNumber = action.sequenceNumber ?: run {
97+
log.error("Missing sequence number in USER_INPUT GameAction")
98+
return
99+
}
100+
val relativeRadius = dataSplit[0].toFloat()
101+
val angle = Angle(dataSplit[1].toFloat())
102+
val inBounds = dataSplit[2].toBoolean()
103+
104+
if (actor == player1) {
105+
if(sequenceNumber <= player1LastSequenceNumber){
106+
// Ignore old messages that are not in order
107+
return
108+
}
109+
player1TouchPoint = relativeRadius to angle
110+
player1InBounds = inBounds
111+
player1LastUpdate = timestamp
112+
player1LastSequenceNumber = sequenceNumber
113+
} else if (actor == player2) {
114+
if(sequenceNumber <= player2LastSequenceNumber){
115+
// Ignore old messages that are not in order
116+
return
117+
}
118+
player2TouchPoint = relativeRadius to angle
119+
player2InBounds = inBounds
120+
player2LastUpdate = timestamp
121+
player2LastSequenceNumber = sequenceNumber
122+
} else {
123+
log.error("Unknown actor: ${actor.id}")
124+
return
125+
}
126+
127+
val otherPlayer = if (actor == player1) player2 else player1
128+
otherPlayer.sendUdp(action.toString())
20129
}
21130
else -> {
22131
log.debug("Unexpected action type: {}", action.type)
23132
}
24133
}
25134
}
26135

27-
override fun startGame(timestamp: Long, sessionId: Int) {
28-
startTime = timestamp
136+
override fun startGame(sessionId: Int) {
137+
val timeHandler = TimeServerHandler.instance
138+
val timeServerCurr = timeHandler.timeServerCurrentTimeMillis().toString()
139+
localStartTime = System.currentTimeMillis() + timeHandler.timeServerDelta
29140
val toSend = GameRequest.builder(GameRequest.Type.START_GAME)
30-
.sessionId(sessionId.toString())
31-
.timestamp(timestamp.toString())
141+
.sessionId(timeServerCurr)
142+
.timestamp(timeServerCurr)
32143
player1.sendTcp(toSend.data(listOf("left")).build().toJson())
33144
player2.sendTcp(toSend.data(listOf("right")).build().toJson())
145+
scope.launch {
146+
manageAxle()
147+
}
148+
}
149+
150+
override fun endGame(errorMessage: String?) {
151+
super.endGame(errorMessage)
152+
scope.cancel()
34153
}
35154

36155
companion object {

game_server/src/main/kotlin/com/imsproject/gameserver/business/games/Game.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.imsproject.common.gameserver.GameAction
44
import com.imsproject.common.gameserver.GameRequest
55
import com.imsproject.common.utils.toJson
66
import com.imsproject.gameserver.business.ClientHandler
7+
import com.imsproject.gameserver.business.TimeServerHandler
78

89
abstract class Game (
910
val lobbyId: String,
@@ -12,19 +13,22 @@ abstract class Game (
1213
) {
1314

1415
abstract fun handleGameAction(actor: ClientHandler, action: GameAction)
15-
var startTime: Long = -1
16+
var localStartTime = -1L
17+
var timeServerStartTime = -1L
1618

17-
open fun startGame(timestamp: Long, sessionId: Int) {
18-
startTime = timestamp
19+
open fun startGame(sessionId: Int) {
20+
val timeHandler = TimeServerHandler.instance
21+
timeServerStartTime = timeHandler.timeServerCurrentTimeMillis()
22+
localStartTime = System.currentTimeMillis() + timeHandler.timeServerDelta
1923
val startMessage = GameRequest.builder(GameRequest.Type.START_GAME)
20-
.timestamp(timestamp.toString())
24+
.timestamp(timeServerStartTime.toString())
2125
.sessionId(sessionId.toString())
2226
.build().toJson()
2327
player1.sendTcp(startMessage)
2428
player2.sendTcp(startMessage)
2529
}
2630

27-
fun endGame(errorMessage: String? = null) {
31+
open fun endGame(errorMessage: String? = null) {
2832
// Send exit message
2933
val exitMessage = GameRequest.builder(GameRequest.Type.END_GAME)
3034
.apply { errorMessage?.let { message(it) } }

game_server/src/main/resources/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ database.name=ims-db
1111
database.driver-class-name=org.postgresql.Driver
1212

1313

14-
running.local=false
14+
running.local=true

manager/src/managers/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
import json
33
from ..ENUMS import *
44

5-
RUNNING_LOCAL = False
6-
7-
5+
RUNNING_LOCAL = True
86

97
if RUNNING_LOCAL:
108
URL = "http://localhost:8080"
119
else:
1210
URL = "http://ims-game-server:8080/"
1311

14-
1512
GAL = False
1613

1714
if GAL:

watch/app/src/main/java/com/imsproject/watch/Properties.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.sp
1111

1212
const val PACKAGE_PREFIX = "com.imsproject.watch"
1313

14-
const val ACTIVITY_DEBUG_MODE = true // set true to be able run the activity directly from the IDE
14+
const val ACTIVITY_DEBUG_MODE = false // set true to be able run the activity directly from the IDE
1515

1616
// ============== Screen size related =============== |
1717

@@ -51,7 +51,6 @@ var RIPPLE_MAX_SIZE = 0
5151

5252
// general
5353
const val MARKER_FADE_DURATION = 500
54-
const val UNDEFINED_ANGLE = 600f
5554
const val ARC_DEFAULT_ALPHA = 0.8f
5655
const val MAX_ANGLE_SKEW = 60f
5756
const val MIN_ANGLE_SKEW = 15f

0 commit comments

Comments
 (0)