Complete most of networked tick-tac-toe. Finish the questions to ponder!

This commit is contained in:
2026-03-18 13:01:42 +00:00
parent 212119adac
commit 1b07a4e414
6 changed files with 77 additions and 12 deletions

View File

@@ -12,7 +12,7 @@
// A thourough rework is necessary for SFML 3.0. // A thourough rework is necessary for SFML 3.0.
enum GameMessageType : unsigned char { enum GameMessageType : unsigned char {
JOIN_GAME = 0x01, PLACE_TOKEN = 0x02 JOIN_GAME = 0x01, PLACE_TOKEN = 0x02, START_GAME = 0x03, GAME_OVER = 0x04
}; };
enum Token : unsigned char { enum Token : unsigned char {
@@ -24,13 +24,26 @@ public:
GameServer(unsigned short tcp_port) : GameServer(unsigned short tcp_port) :
m_tcp_port(tcp_port) {} m_tcp_port(tcp_port) {}
bool send_start_game_to_clients() {
char buf[1] = { START_GAME };
std::cout << "Starting the game..." << std::endl;
return broadcast_message(buf, nullptr);
}
bool send_game_over_to_clients() {
char buf[1] = { GAME_OVER };
std::cout << "Game Over!" << std::endl;
return broadcast_message(buf, nullptr);
}
// Binds to a port and then loops around. For every client that connects, // Binds to a port and then loops around. For every client that connects,
// we start a new thread receiving their messages. // we start a new thread receiving their messages.
void tcp_start() void tcp_start()
{ {
// BINDING // BINDING
sf::TcpListener listener; sf::TcpListener listener;
sf::Socket::Status status = listener.listen(m_tcp_port, sf::IpAddress("A.B.C.D")); sf::Socket::Status status = listener.listen(m_tcp_port, sf::IpAddress("152.105.66.120")); // Make sure to change this!
if (status != sf::Socket::Status::Done) if (status != sf::Socket::Status::Done)
{ {
std::cerr << "Error binding listener to port" << std::endl; std::cerr << "Error binding listener to port" << std::endl;
@@ -71,6 +84,10 @@ public:
// Slight pause to ensure the all threads have started // Slight pause to ensure the all threads have started
// -------------------------------------------------------------- // --------------------------------------------------------------
std::this_thread::sleep_for(std::chrono::milliseconds(250)); std::this_thread::sleep_for(std::chrono::milliseconds(250));
if(!send_start_game_to_clients()) {
std::cerr << "Could not start game. One or both players did not recieve START_GAME" << std::endl;
}
} }
} }
} }
@@ -79,11 +96,12 @@ public:
// The connection is closed automatically when the listener object is out of scope. // The connection is closed automatically when the listener object is out of scope.
} }
private: private:
unsigned short m_tcp_port; unsigned short m_tcp_port;
unsigned short m_player_count { 0 }; unsigned short m_player_count { 0 };
unsigned short m_turns_played { 0 };
int board[3][3];
std::vector<sf::TcpSocket*> m_clients; std::vector<sf::TcpSocket*> m_clients;
std::mutex m_clients_mutex; std::mutex m_clients_mutex;
@@ -144,6 +162,12 @@ private:
debug_message(payload); debug_message(payload);
broadcast_message(payload, client); broadcast_message(payload, client);
std::this_thread::sleep_for(std::chrono::milliseconds(250));
if(++m_turns_played == 9) {
send_game_over_to_clients();
}
} }
} }
@@ -194,6 +218,8 @@ private:
switch(messageType) { switch(messageType) {
case JOIN_GAME: return 2; case JOIN_GAME: return 2;
case PLACE_TOKEN: return sizeof(int) * 2 + 2; case PLACE_TOKEN: return sizeof(int) * 2 + 2;
case START_GAME: return 1;
case GAME_OVER: return 1;
default: return 0; default: return 0;
} }
} }
@@ -222,7 +248,7 @@ private:
int main() int main()
{ {
GameServer server(4300); GameServer server(4331);
server.tcp_start(); server.tcp_start();

View File

@@ -12,7 +12,9 @@ interface Deserializable {
enum class GameMessageType(val id: Byte) { enum class GameMessageType(val id: Byte) {
JOIN_GAME(1), JOIN_GAME(1),
PLACE_TOKEN(2); PLACE_TOKEN(2),
START_GAME(3),
GAME_OVER(4);
companion object { companion object {
fun fromByte(id: Byte) = entries.first { it.id == id } fun fromByte(id: Byte) = entries.first { it.id == id }
@@ -23,6 +25,9 @@ sealed class GameMessage(val type: GameMessageType) : Serializable {
override fun serialize() = byteArrayOf(type.id) override fun serialize() = byteArrayOf(type.id)
object StartGameMessage : GameMessage(GameMessageType.START_GAME)
object GameOverMessage : GameMessage(GameMessageType.GAME_OVER)
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
data class JoinGameMessage(val token: Token) : GameMessage(GameMessageType.JOIN_GAME) { data class JoinGameMessage(val token: Token) : GameMessage(GameMessageType.JOIN_GAME) {

View File

@@ -20,11 +20,11 @@ class Main : KtxGame<KtxScreen>() {
override fun create() { override fun create() {
KtxAsync.initiate() KtxAsync.initiate()
networkHandler = NetworkHandler("A.B.C.D", 4300, serverChannel, clientChannel) networkHandler = NetworkHandler("152.105.66.120", 4331, serverChannel, clientChannel)
addScreen(FirstScreen(this, clientChannel, serverChannel)) addScreen(FirstScreen(this, clientChannel, serverChannel))
addScreen(GameOverScreen()) addScreen(GameOverScreen(0))
setScreen<FirstScreen>() setScreen<FirstScreen>()

View File

@@ -153,6 +153,10 @@ class NetworkHandler(
GameMessageType.JOIN_GAME -> GameMessage.JoinGameMessage.deserialize(this) GameMessageType.JOIN_GAME -> GameMessage.JoinGameMessage.deserialize(this)
GameMessageType.PLACE_TOKEN -> GameMessage.PlaceTokenMessage.deserialize(this) GameMessageType.PLACE_TOKEN -> GameMessage.PlaceTokenMessage.deserialize(this)
GameMessageType.START_GAME -> GameMessage.StartGameMessage
GameMessageType.GAME_OVER -> GameMessage.GameOverMessage
} }
} }

View File

@@ -27,6 +27,9 @@ class FirstScreen(
private val receiveChannel: ReceiveChannel<GameMessage>, private val receiveChannel: ReceiveChannel<GameMessage>,
private val sendChannel: SendChannel<GameMessage> private val sendChannel: SendChannel<GameMessage>
) : KtxScreen, InputProcessor { ) : KtxScreen, InputProcessor {
private var gameStarted = false
private var gameOver = false
private val batch = SpriteBatch() private val batch = SpriteBatch()
@@ -56,6 +59,12 @@ class FirstScreen(
override fun render(delta: Float) { override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f) clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
if (gameOver) {
game.setScreen<GameOverScreen>()
disposeSafely()
return
}
camera.update() camera.update()
batch.use { board.draw(it) } batch.use { board.draw(it) }
@@ -87,6 +96,18 @@ class FirstScreen(
localPlayerTurn = true localPlayerTurn = true
} }
is GameMessage.StartGameMessage -> {
Gdx.app.log(TAG, "The Server has started the game")
gameStarted = true
}
is GameMessage.GameOverMessage -> {
Gdx.app.log(TAG, "The Server has sent Game Over")
gameStarted = false
gameOver = true
}
} }
Gdx.app.log(TAG, "THE MESSAGE RECEIVED IS: $gm") Gdx.app.log(TAG, "THE MESSAGE RECEIVED IS: $gm")
@@ -114,7 +135,7 @@ class FirstScreen(
button: Int button: Int
): Boolean { ): Boolean {
if(!localPlayerTurn) if(!gameStarted || !localPlayerTurn)
return true; return true;
val col = ((screenX.toFloat() / Gdx.graphics.width) * 3).toInt() val col = ((screenX.toFloat() / Gdx.graphics.width) * 3).toInt()

View File

@@ -6,16 +6,25 @@ import ktx.app.KtxScreen
import ktx.app.clearScreen import ktx.app.clearScreen
import ktx.graphics.use import ktx.graphics.use
class GameOverScreen : KtxScreen { class GameOverScreen(private val winType: Int) : KtxScreen {
private var label = winType.toString()
private val font = BitmapFont() private val font = BitmapFont()
private val batch = SpriteBatch() private val batch = SpriteBatch()
override fun show() {
when (winType) {
0 -> label = "You Win!"
1 -> label = "You Lose."
2 -> label = "Game Over!"
else -> "Invalid type!"
}
}
override fun render(delta: Float) { override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f) clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
batch.use { batch.use {
font.draw(it, "Game Over!", 10f, 10f) font.draw(it, label, 10f, 10f)
} }
} }
} }