Commit unzipped original NetworkedTicTacToeStarter folder

This commit is contained in:
2026-03-13 14:40:56 +00:00
parent 64af2ed168
commit 212119adac
50 changed files with 2359 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
eclipse.project.name = appName + '-core'
dependencies {
api "com.badlogicgames.ashley:ashley:$ashleyVersion"
api "com.badlogicgames.gdx:gdx-ai:$aiVersion"
api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
api "com.badlogicgames.gdx:gdx:$gdxVersion"
api "com.kotcrab.vis:vis-ui:$visUiVersion"
api "io.github.libktx:ktx-actors:$ktxVersion"
api "io.github.libktx:ktx-ai:$ktxVersion"
api "io.github.libktx:ktx-app:$ktxVersion"
api "io.github.libktx:ktx-artemis:$ktxVersion"
api "io.github.libktx:ktx-ashley:$ktxVersion"
api "io.github.libktx:ktx-assets-async:$ktxVersion"
api "io.github.libktx:ktx-assets:$ktxVersion"
api "io.github.libktx:ktx-async:$ktxVersion"
api "io.github.libktx:ktx-box2d:$ktxVersion"
api "io.github.libktx:ktx-collections:$ktxVersion"
api "io.github.libktx:ktx-freetype-async:$ktxVersion"
api "io.github.libktx:ktx-freetype:$ktxVersion"
api "io.github.libktx:ktx-graphics:$ktxVersion"
api "io.github.libktx:ktx-i18n:$ktxVersion"
api "io.github.libktx:ktx-inject:$ktxVersion"
api "io.github.libktx:ktx-json:$ktxVersion"
api "io.github.libktx:ktx-log:$ktxVersion"
api "io.github.libktx:ktx-math:$ktxVersion"
api "io.github.libktx:ktx-preferences:$ktxVersion"
api "io.github.libktx:ktx-reflect:$ktxVersion"
api "io.github.libktx:ktx-scene2d:$ktxVersion"
api "io.github.libktx:ktx-style:$ktxVersion"
api "io.github.libktx:ktx-tiled:$ktxVersion"
api "io.github.libktx:ktx-vis-style:$ktxVersion"
api "io.github.libktx:ktx-vis:$ktxVersion"
api "net.onedaybeard.artemis:artemis-odb:$artemisOdbVersion"
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion"
if(enableGraalNative == 'true') {
implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion"
}
}

View File

@@ -0,0 +1,110 @@
package u0012604.tictactoe
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.viewport.Viewport
import ktx.assets.disposeSafely
import ktx.graphics.use
import u0012604.tictactoe.screens.FirstScreen.Companion.NOUGHT_RADIUS
class Board(val viewport: Viewport) : Disposable {
private var thirdOfWidth = 0f
private var thirdOfHeight = 0f
private var halfCellW = 0f
private var halfCellH = 0f
private val shapeRenderer = ShapeRenderer()
private var boardLines = emptyArray<Pair<Vector2, Vector2>>()
private val board = arrayOf(
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY),
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY),
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY)
)
fun placeToken(row: Int, col: Int, token: Token) =
if(row in (0..2) && col in (0 .. 2) && board[row][col] == Token.EMPTY) {
board[row][col] = token
true
}
else {
false
}
fun draw(batch: Batch) {
shapeRenderer.use(ShapeRenderer.ShapeType.Filled, viewport.camera.combined) { sr ->
sr.color = Color.RED
boardLines.forEach { line ->
// Gdx.app.log(TAG, "p0:${line.first}, ${line.second}")
sr.rectLine(line.first, line.second, 10f)
}
board.forEachIndexed { rowIndex, row ->
row.forEachIndexed { colIndex, col ->
when(col) {
Token.NOUGHT -> drawNought(rowIndex, colIndex, sr)
Token.CROSS -> drawCross(rowIndex, colIndex, sr)
else -> {}
}
}
}
}
}
fun resize(width: Int, height: Int) {
boardLines = emptyArray()
thirdOfWidth = width / 3f
thirdOfHeight = height / 3f
// Vertical lines
val x1 = thirdOfWidth
val x2 = Gdx.graphics.width / 1.5f
boardLines += Pair(Vector2(x1, 0f), Vector2(x1, height.toFloat()))
boardLines += Pair(Vector2(x2, 0f), Vector2(x2, height.toFloat()))
// Horizontal lines
val y1 = thirdOfHeight
val v2 = Gdx.graphics.height.toFloat() / 1.5f
boardLines += Pair(Vector2(0f, y1), Vector2(width.toFloat(), y1))
boardLines += Pair(Vector2(0f, v2), Vector2(width.toFloat(), v2))
halfCellW = x1 / 2f
halfCellH = y1 / 2f
}
private fun drawNought(row: Int, col: Int, sr: ShapeRenderer) {
val x = col * thirdOfWidth + halfCellW
val y = (2 - row) * thirdOfHeight + halfCellH
sr.circle(x, y, NOUGHT_RADIUS)
}
private fun drawCross(row: Int, col: Int, sr: ShapeRenderer) {
val flipRow = 2 - row
val l1x1 = col * thirdOfWidth + 50f
val l1y1 = flipRow * thirdOfHeight + 50f
val l1x2 = col * thirdOfWidth - 50f + 2 * halfCellW
val l1y2 = flipRow * thirdOfHeight - 50f + 2 * halfCellH
sr.rectLine(l1x1, l1y1, l1x2, l1y2, 10f)
sr.rectLine(l1x1, l1y2, l1x2, l1y1, 10f)
}
override fun dispose() {
shapeRenderer.disposeSafely()
}
}

View File

@@ -0,0 +1,65 @@
package u0012604.tictactoe
import java.nio.ByteBuffer
interface Serializable {
fun serialize() : ByteArray
}
interface Deserializable {
fun deserialize(bb: ByteBuffer) : GameMessage
}
enum class GameMessageType(val id: Byte) {
JOIN_GAME(1),
PLACE_TOKEN(2);
companion object {
fun fromByte(id: Byte) = entries.first { it.id == id }
}
}
sealed class GameMessage(val type: GameMessageType) : Serializable {
override fun serialize() = byteArrayOf(type.id)
// -----------------------------------------------------------------------------------------------
data class JoinGameMessage(val token: Token) : GameMessage(GameMessageType.JOIN_GAME) {
override fun serialize(): ByteArray = with(ByteBuffer.allocate(2)) {
put(token.type)
super.serialize() + array()
}
companion object : Deserializable {
override fun deserialize(bb: ByteBuffer) = with(bb) {
JoinGameMessage(Token.fromByte(get()))
}
}
}
// -----------------------------------------------------------------------------------------------
data class PlaceTokenMessage(val row: Int, val col: Int, val token: Token) : GameMessage(GameMessageType.PLACE_TOKEN) {
override fun serialize(): ByteArray = with(ByteBuffer.allocate(10)) {
putInt(row)
putInt(col)
put(token.type)
super.serialize() + array()
}
companion object : Deserializable {
override fun deserialize(bb: ByteBuffer) = with(bb) {
val row = getInt()
val col = getInt()
val token = Token.fromByte(get())
PlaceTokenMessage(row, col, token)
}
}
}
}

View File

@@ -0,0 +1,40 @@
package u0012604.tictactoe
import kotlinx.coroutines.channels.Channel
import ktx.app.KtxGame
import ktx.app.KtxScreen
import ktx.assets.disposeSafely
import ktx.async.KtxAsync
import u0012604.tictactoe.networking.NetworkHandler
import u0012604.tictactoe.screens.FirstScreen
import u0012604.tictactoe.screens.GameOverScreen
class Main : KtxGame<KtxScreen>() {
private val clientChannel = Channel<GameMessage>(10)
private val serverChannel = Channel<GameMessage>(10)
private lateinit var networkHandler: NetworkHandler
override fun create() {
KtxAsync.initiate()
networkHandler = NetworkHandler("A.B.C.D", 4300, serverChannel, clientChannel)
addScreen(FirstScreen(this, clientChannel, serverChannel))
addScreen(GameOverScreen())
setScreen<FirstScreen>()
}
override fun dispose() {
super.dispose()
serverChannel.close()
clientChannel.close()
networkHandler.disposeSafely()
}
}

View File

@@ -0,0 +1,9 @@
package u0012604.tictactoe
enum class Token(val type: Byte) {
EMPTY(0), NOUGHT(1), CROSS(2);
companion object {
fun fromByte(type: Byte) = Token.entries.first { it.type == type }
}
}

View File

@@ -0,0 +1,166 @@
package u0012604.tictactoe.networking
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.utils.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import u0012604.tictactoe.GameMessage
import u0012604.tictactoe.GameMessageType
import java.net.ConnectException
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketException
import java.net.SocketTimeoutException
import java.nio.ByteBuffer
class NetworkHandler(
private val host: String,
private val port: Int,
private val toServerChannel: Channel<GameMessage>, // Messages being received elsewhere in the client to send onwards to the client
private val fromServerChannel: SendChannel<GameMessage>, // Messages being sent from the server to elsewhere within client
private val connectionTimeout: Int = 3000,
private val maxRetries: Int = 10
) : Disposable
{
fun ByteArray.processMessage() = decodeToString().trimEnd{it == Char(0)} // Removes trailing null character
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.IO).apply {
launch {
var retries = 0
while(retries < maxRetries) {
if(startNetwork(host, port)) {
break;
}
delay(retries * 500L)
retries++
}
if(retries == maxRetries) {
Gdx.app.error(TAG, "Maximum retries ($maxRetries) exceeded, giving up :(")
}
}
}
private var socket: Socket? = null
val isReady: Boolean
get() = socket?.isConnected ?: false
@OptIn(ExperimentalStdlibApi::class)
private fun startNetwork(
host: String,
port: Int
) = try {
var assignedClientId: UShort = 0U
// Create our socket
socket = Socket()
// Connect with timeout set.
socket?.let {
val socketAddress = InetSocketAddress(host, port)
it.connect(socketAddress, connectionTimeout)
// -------------------------------------------------------------
// Coroutine to handle messages
// to be sent to the server
// -------------------------------------------------------------
coroutineScope.launch {
try {
it.outputStream.apply {
while (true) {
// 1. Get the next message in the channel
// to send onward to the server.
val nextMessage = toServerChannel.receive()
// 2. Write the message to the server
// via the socket's output stream
write(nextMessage.serialize())
flush()
Gdx.app.log(TAG, "Sent the message $nextMessage")
delay(10L)
}
}
} catch (ex: java.net.SocketException) {
Gdx.app.error(TAG, "[SEND] Socket Failed: ${ex.message}")
}
}
// -------------------------------------------------------------
// Coroutine to handle messages
// being received from the server
//
// Messages received are then forwarded via the
// -------------------------------------------------------------
coroutineScope.launch {
try {
it.inputStream.apply {
launch {
while (true) {
val byteArray = ByteArray(1024)
// delay(250L)
// 1. Read data from the socket's
// input stream.
val count = read(byteArray, 0, 1024)
if (count == -1) {
Gdx.app.error(TAG, "Socket Read Error!")
break
}
val gameMessage = buildGameMessage(byteArray)
fromServerChannel.send(gameMessage)
}
}
}
} catch (ex: java.net.SocketException) {
Gdx.app.log(TAG, "[RECEIVE] Socket Failure::[${ex.message}")
}
}
}
true
} catch (ex: java.net.SocketTimeoutException) {
Gdx.app.error(TAG, "Timeout Exception: ${ex.message}")
false
} catch (ex: java.net.ConnectException) {
Gdx.app.error(TAG, "Connection Exception: ${ex.message}")
false
} catch (ex: java.net.SocketException) {
Gdx.app.error(TAG, "Exception Raised: ${ex.message}")
false
}
fun buildGameMessage(ba: ByteArray) = with(ByteBuffer.wrap(ba)) {
val messageType = GameMessageType.fromByte(get())
when(messageType) {
GameMessageType.JOIN_GAME -> GameMessage.JoinGameMessage.deserialize(this)
GameMessageType.PLACE_TOKEN -> GameMessage.PlaceTokenMessage.deserialize(this)
}
}
override fun dispose() {
coroutineScope.cancel()
}
companion object {
val TAG = NetworkHandler::class.simpleName!!
}
}

View File

@@ -0,0 +1,177 @@
package u0012604.tictactoe.screens
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.InputProcessor
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.viewport.FitViewport
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ktx.app.KtxGame
import ktx.app.KtxScreen
import ktx.app.clearScreen
import ktx.assets.disposeSafely
import ktx.graphics.use
import u0012604.tictactoe.Board
import u0012604.tictactoe.GameMessage
import u0012604.tictactoe.Token
class FirstScreen(
val game: KtxGame<KtxScreen>,
private val receiveChannel: ReceiveChannel<GameMessage>,
private val sendChannel: SendChannel<GameMessage>
) : KtxScreen, InputProcessor {
private val batch = SpriteBatch()
private val camera =
OrthographicCamera(WIDTH, HEIGHT).apply {
position.set(Gdx.graphics.width / 2f, Gdx.graphics.height / 2f, 0f)
};
private val viewport = FitViewport(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat(), camera)
private val board = Board(viewport)
private var touchPosition = Vector2.Zero
private var localPlayerToken: Token? = null
private var localPlayerTurn = false
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
init {
Gdx.input.inputProcessor = this
initiateIncomingGameMessageHandling()
}
override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
camera.update()
batch.use { board.draw(it) }
}
override fun resize(width: Int, height: Int) {
viewport.update(width, height)
board.resize(width, height)
}
private fun initiateIncomingGameMessageHandling() {
coroutineScope.launch {
while(true) {
val gm = receiveChannel.receive()
when(gm) {
is GameMessage.JoinGameMessage -> {
localPlayerToken = gm.token
localPlayerTurn = localPlayerToken == Token.NOUGHT
}
is GameMessage.PlaceTokenMessage -> {
Gdx.app.log(TAG, "The other player placed a token at: (${gm.row}, ${gm.col})")
board.placeToken(gm.row, gm.col, gm.token)
localPlayerTurn = true
}
}
Gdx.app.log(TAG, "THE MESSAGE RECEIVED IS: $gm")
delay(10L)
}
}
}
override fun dispose() {
batch.disposeSafely()
board.disposeSafely()
}
override fun keyDown(keycode: Int) = true
override fun keyUp(keycode: Int) = true
override fun keyTyped(character: Char) = true
override fun touchDown(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
): Boolean {
if(!localPlayerTurn)
return true;
val col = ((screenX.toFloat() / Gdx.graphics.width) * 3).toInt()
val row = ((screenY.toFloat() / Gdx.graphics.height) * 3).toInt()
localPlayerToken?.let {
if (board.placeToken(row, col, it)) {
localPlayerTurn = false
val gameMessage = GameMessage.PlaceTokenMessage(row, col, it)
coroutineScope.launch {
sendChannel.send(gameMessage)
}
}
}
Gdx.app.log(TAG, "THE ROW IS: ($row, $col)")
touchPosition.set(screenX.toFloat(), screenY.toFloat())
viewport.unproject(touchPosition)
return true
}
override fun touchUp(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
) = true
override fun touchCancelled(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
) = true
override fun touchDragged(
screenX: Int,
screenY: Int,
pointer: Int
) = true
override fun mouseMoved(screenX: Int, screenY: Int) = true
override fun scrolled(amountX: Float, amountY: Float) = true
companion object {
val TAG = FirstScreen::class.simpleName!!
const val WIDTH = 100f
const val HEIGHT = 16f * WIDTH / 9f
const val NOUGHT_RADIUS = 100f
}
}

View File

@@ -0,0 +1,21 @@
package u0012604.tictactoe.screens
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import ktx.app.KtxScreen
import ktx.app.clearScreen
import ktx.graphics.use
class GameOverScreen : KtxScreen {
private val font = BitmapFont()
private val batch = SpriteBatch()
override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
batch.use {
font.draw(it, "Game Over!", 10f, 10f)
}
}
}