Skip to content

Instantly share code, notes, and snippets.

@define-private-public
Last active July 26, 2016 13:09
Show Gist options
  • Select an option

  • Save define-private-public/1705b604b47ece09cdbc8e40588788c2 to your computer and use it in GitHub Desktop.

Select an option

Save define-private-public/1705b604b47ece09cdbc8e40588788c2 to your computer and use it in GitHub Desktop.
Text based games that go over TCP
// Filename: GuessMyNumberGame.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Net.Sockets;
using System.Threading;
namespace TcpGames
{
public class GuessMyNumberGame : IGame
{
// Objects for the game
private TcpGamesServer _server;
private TcpClient _player;
private Random _rng;
private bool _needToDisconnectClient = false;
// Name of the game
public string Name
{
get { return "Guess My Number"; }
}
// Just needs only one player
public int RequiredPlayers
{
get { return 1; }
}
// Constructor
public GuessMyNumberGame(TcpGamesServer server)
{
_server = server;
_rng = new Random();
}
// Adds only a single player to the game
public bool AddPlayer(TcpClient client)
{
// Make sure only one player was added
if (_player == null)
{
_player = client;
return true;
}
return false;
}
// If the client who disconnected is ours, we need to quit our game
public void DisconnectClient(TcpClient client)
{
_needToDisconnectClient = (client == _player);
}
// Main loop of the Game
// Packets are sent sent synchronously though
public void Run()
{
// Make sure we have a player
bool running = (_player != null);
if (running)
{
// Send a instruction packet
Packet introPacket = new Packet("message",
"Welcome player, I want you to guess my number.\n" +
"It's somewhere between (and including) 1 and 100.\n");
_server.SendPacket(_player, introPacket).GetAwaiter().GetResult();
}
else
return;
// Should be [1, 100]
int theNumber = _rng.Next(1, 101);
Console.WriteLine("Our number is: {0}", theNumber);
// Some bools for game state
bool correct = false;
bool clientConncted = true;
bool clientDisconnectedGracefully = false;
// Main game loop
while (running)
{
// Poll for input
Packet inputPacket = new Packet("input", "Your guess: ");
_server.SendPacket(_player, inputPacket).GetAwaiter().GetResult();
// Read their answer
Packet answerPacket = null;
while (answerPacket == null)
{
answerPacket = _server.ReceivePacket(_player).GetAwaiter().GetResult();
Thread.Sleep(10);
}
// Check for graceful disconnect
if (answerPacket.Command == "bye")
{
_server.HandleDisconnectedClient(_player);
clientDisconnectedGracefully = true;
}
// Check input
if (answerPacket.Command == "input")
{
Packet responsePacket = new Packet("message");
int theirGuess;
if (int.TryParse(answerPacket.Message, out theirGuess))
{
// See if they won
if (theirGuess == theNumber)
{
correct = true;
responsePacket.Message = "Correct! You win!\n";
}
else if (theirGuess < theNumber)
responsePacket.Message = "Too low.\n";
else if (theirGuess > theNumber)
responsePacket.Message = "Too high.\n";
}
else
responsePacket.Message = "That wasn't a valid number, try again.\n";
// Send the message
_server.SendPacket(_player, responsePacket).GetAwaiter().GetResult();
}
// Take a small nap
Thread.Sleep(10);
// If they aren't correct, keep them here
running &= !correct;
// Check for disconnect, may have happend gracefully before
if (!_needToDisconnectClient && !clientDisconnectedGracefully)
clientConncted &= !TcpGamesServer.IsDisconnected(_player);
else
clientConncted = false;
running &= clientConncted;
}
// Thank the player and disconnect them
if (clientConncted)
_server.DisconnectClient(_player, "Thanks for playing \"Guess My Number\"!");
else
Console.WriteLine("Client disconnected from game.");
Console.WriteLine("Ending a \"{0}\" game.", Name);
}
}
}
// Filename: IGame.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System.Net.Sockets;
namespace TcpGames
{
interface IGame
{
#region Properties
// Name of the game
string Name { get; }
// How many players are needed to start
int RequiredPlayers { get; }
#endregion // Properties
#region Functions
// Adds a player to the game (should be before it starts)
bool AddPlayer(TcpClient player);
// Tells the server to disconnect a player
void DisconnectClient(TcpClient client);
// The main game loop
void Run();
#endregion // Functions
}
}
// Filename: Packet.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using Newtonsoft.Json;
namespace TcpGames
{
public class Packet
{
[JsonProperty("command")]
public string Command { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
// Makes a packet
public Packet(string command="", string message="")
{
Command = command;
Message = message;
}
public override string ToString()
{
return string.Format(
"[Packet:\n" +
" Command=`{0}`\n" +
" Message=`{1}`]",
Command, Message);
}
// Serialize to Json
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
// Deserialize
public static Packet FromJson(string jsonData)
{
return JsonConvert.DeserializeObject<Packet>(jsonData);
}
}
}
// Filename: TcpGamesClient.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace TcpGames
{
public class TcpGamesClient
{
// Conneciton objects
public readonly string ServerAddress;
public readonly int Port;
public bool Running { get; private set; }
private TcpClient _client;
private bool _clientRequestedDisconnect = false;
// Messaging
private NetworkStream _msgStream = null;
private Dictionary<string, Func<string, Task>> _commandHandlers = new Dictionary<string, Func<string, Task>>();
public TcpGamesClient(string serverAddress, int port)
{
// Create a non-connectec TcpClient
_client = new TcpClient();
Running = false;
// Set other data
ServerAddress = serverAddress;
Port = port;
}
// Cleans up any leftover network resources
private void _cleanupNetworkResources()
{
_msgStream?.Close();
_msgStream = null;
_client.Close();
}
// Connects to the games server
public void Connect()
{
// Connect to the server
try
{
_client.Connect(ServerAddress, Port); // Resolves DNS for us
}
catch (SocketException se)
{
Console.WriteLine("[ERROR] {0}", se.Message);
}
// check that we've connected
if (_client.Connected)
{
// Connected!
Console.WriteLine("Connected to the server at {0}.", _client.Client.RemoteEndPoint);
Running = true;
// Get the message stream
_msgStream = _client.GetStream();
// Hook up some packet command handlers
_commandHandlers["bye"] = _handleBye;
_commandHandlers["message"] = _handleMessage;
_commandHandlers["input"] = _handleInput;
}
else
{
// Nope...
_cleanupNetworkResources();
Console.WriteLine("Wasn't able to connect to the server at {0}:{1}.", ServerAddress, Port);
}
}
// Requests a disconnect, will send a "bye," message to the server
// This should only be called by the user
public void Disconnect()
{
Console.WriteLine("Disconnecting from the server...");
Running = false;
_clientRequestedDisconnect = true;
_sendPacket(new Packet("bye")).GetAwaiter().GetResult();
}
// Main loop for the Games Client
public void Run()
{
bool wasRunning = Running;
// Listen for messages
List<Task> tasks = new List<Task>();
while (Running)
{
// Check for new packets
tasks.Add(_handleIncomingPackets());
// Use less CPU
Thread.Sleep(10);
// Make sure that we didn't have a graceless disconnect
if (_isDisconnected(_client) && !_clientRequestedDisconnect)
{
Running = false;
Console.WriteLine("The server has disconnected from us ungracefully.\n:[");
}
}
// Just incase we have anymore packets, give them one second to be processed
Task.WaitAll(tasks.ToArray(), 1000);
// Cleanup
_cleanupNetworkResources();
if (wasRunning)
Console.WriteLine("Disconnected.");
}
// Sends packets to the server asynchronously
private async Task _sendPacket(Packet packet)
{
try
{ // convert JSON to buffer and its length to a 16 bit unsigned integer buffer
byte[] jsonBuffer = Encoding.UTF8.GetBytes(packet.ToJson());
byte[] lengthBuffer = BitConverter.GetBytes(Convert.ToUInt16(jsonBuffer.Length));
// Join the buffers
byte[] packetBuffer = new byte[lengthBuffer.Length + jsonBuffer.Length];
lengthBuffer.CopyTo(packetBuffer, 0);
jsonBuffer.CopyTo(packetBuffer, lengthBuffer.Length);
// Send the packet
await _msgStream.WriteAsync(packetBuffer, 0, packetBuffer.Length);
//Console.WriteLine("[SENT]\n{0}", packet);
}
catch(Exception) { }
}
// Checks for new incoming messages and handles them
// This method will handle one Packet at a time, even if more than one is in the memory stream
private async Task _handleIncomingPackets()
{
try
{
// Check for new incomding messages
if (_client.Available > 0)
{
// There must be some incoming data, the first two bytes are the size of the Packet
byte[] lengthBuffer = new byte[2];
await _msgStream.ReadAsync(lengthBuffer, 0, 2);
ushort packetByteSize = BitConverter.ToUInt16(lengthBuffer, 0);
// Now read that many bytes from what's left in the stream, it must be the Packet
byte[] jsonBuffer = new byte[packetByteSize];
await _msgStream.ReadAsync(jsonBuffer, 0, jsonBuffer.Length);
// Convert it into a packet datatype
string jsonString = Encoding.UTF8.GetString(jsonBuffer);
Packet packet = Packet.FromJson(jsonString);
// Dispatch it
try
{
await _commandHandlers[packet.Command](packet.Message);
}
catch (KeyNotFoundException) { }
//Console.WriteLine("[RECEIVED]\n{0}", packet);
}
} catch (Exception) { }
}
#region Command Handlers
private Task _handleBye(string message)
{
// Print the message
Console.WriteLine("The server is disconnecting us with this message:");
Console.WriteLine(message);
// Will start the disconnection process in Run()
Running = false;
return Task.FromResult(0); // Task.CompletedTask exists in .NET v4.6
}
// Just prints out a message sent from the server
private Task _handleMessage(string message)
{
Console.Write(message);
return Task.FromResult(0); // Task.CompletedTask exists in .NET v4.6
}
// Gets input from the user and sends it to the server
private async Task _handleInput(string message)
{
// Print the prompt and get a response to send
Console.Write(message);
string responseMsg = Console.ReadLine();
// Send the response
Packet resp = new Packet("input", responseMsg);
await _sendPacket(resp);
}
#endregion // Command Handlers
#region TcpClient Helper Methods
// Checks if a client has disconnected ungracefully
// Adapted from: http://stackoverflow.com/questions/722240/instantly-detect-client-disconnection-from-server-socket
private static bool _isDisconnected(TcpClient client)
{
try
{
Socket s = client.Client;
return s.Poll(10 * 1000, SelectMode.SelectRead) && (s.Available == 0);
}
catch(SocketException)
{
// We got a socket error, assume it's disconnected
return true;
}
}
#endregion // TcpClient Helper Methods
#region Program Execution
public static TcpGamesClient gamesClient;
public static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
{
// Perform a graceful disconnect
args.Cancel = true;
gamesClient?.Disconnect();
}
public static void Main(string[] args)
{
// Setup the Games Client
string host = "localhost";//args[0].Trim();
int port = 6000;//int.Parse(args[1].Trim());
gamesClient = new TcpGamesClient(host, port);
// Add a handler for a Ctrl-C press
Console.CancelKeyPress += InterruptHandler;
// Try to connecct & interact with the server
gamesClient.Connect();
gamesClient.Run();
}
#endregion // Program Execution
}
}
// Filename: TcpGamesServer.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace TcpGames
{
public class TcpGamesServer
{
// Listens for new incoming connections
private TcpListener _listener;
// Clients objects
private List<TcpClient> _clients = new List<TcpClient>();
private List<TcpClient> _waitingLobby = new List<TcpClient>();
// Game stuff
private Dictionary<TcpClient, IGame> _gameClientIsIn = new Dictionary<TcpClient, IGame>();
private List<IGame> _games = new List<IGame>();
private List<Thread> _gameThreads = new List<Thread>();
private IGame _nextGame;
// Other data
public readonly string Name;
public readonly int Port;
public bool Running { get; private set; }
// Construct to create a new Games Server
public TcpGamesServer(string name, int port)
{
// Set some of the basic data
Name = name;
Port = port;
Running = false;
// Create the listener
_listener = new TcpListener(IPAddress.Any, Port);
}
// Shutsdown the server if its running
public void Shutdown()
{
if (Running)
{
Running = false;
Console.WriteLine("Shutting down the Game(s) Server...");
}
}
// The main loop for the games server
public void Run()
{
Console.WriteLine("Starting the \"{0}\" Game(s) Server on port {1}.", Name, Port);
Console.WriteLine("Press Ctrl-C to shutdown the server at any time.");
// Start the next game
// (current only the Guess My Number Game)
_nextGame = new GuessMyNumberGame(this);
// Start running the server
_listener.Start();
Running = true;
List<Task> newConnectionTasks = new List<Task>();
Console.WriteLine("Waiting for incommming connections...");
while (Running)
{
// Handle any new clients
if (_listener.Pending())
newConnectionTasks.Add(_handleNewConnection());
// Once we have enough clients for the next game, add them in and start the game
if (_waitingLobby.Count >= _nextGame.RequiredPlayers)
{
// Get that many players from the waiting lobby and start the game
int numPlayers = 0;
while (numPlayers < _nextGame.RequiredPlayers)
{
// Pop the first one off
TcpClient player = _waitingLobby[0];
_waitingLobby.RemoveAt(0);
// Try adding it to the game. If failure, put it back in the lobby
if (_nextGame.AddPlayer(player))
numPlayers++;
else
_waitingLobby.Add(player);
}
// Start the game in a new thread!
Console.WriteLine("Starting a \"{0}\" game.", _nextGame.Name);
Thread gameThread = new Thread(new ThreadStart(_nextGame.Run));
gameThread.Start();
_games.Add(_nextGame);
_gameThreads.Add(gameThread);
// Create a new game
_nextGame = new GuessMyNumberGame(this);
}
// Check if any clients have disconnected in waiting, gracefully or not
// NOTE: This could (and should) be parallelized
foreach (TcpClient client in _waitingLobby.ToArray())
{
EndPoint endPoint = client.Client.RemoteEndPoint;
bool disconnected = false;
// Check for graceful first
Packet p = ReceivePacket(client).GetAwaiter().GetResult();
disconnected = (p?.Command == "bye");
// Then ungraceful
disconnected |= IsDisconnected(client);
if (disconnected)
{
HandleDisconnectedClient(client);
Console.WriteLine("Client {0} has disconnected from the Game(s) Server.", endPoint);
}
}
// Take a small nap
Thread.Sleep(10);
}
// In the chance a client connected but we exited the loop, give them 1 second to finish
Task.WaitAll(newConnectionTasks.ToArray(), 1000);
// Shutdown all of the threads, regardless if they are done or not
foreach (Thread thread in _gameThreads)
thread.Abort();
// Disconnect any clients still here
Parallel.ForEach(_clients, (client) =>
{
DisconnectClient(client, "The Game(s) Server is being shutdown.");
});
// Cleanup our resources
_listener.Stop();
// Info
Console.WriteLine("The server has been shut down.");
}
// Awaits for a new connection and then adds them to the waiting lobby
private async Task _handleNewConnection()
{
// Get the new client using a Future
TcpClient newClient = await _listener.AcceptTcpClientAsync();
Console.WriteLine("New connection from {0}.", newClient.Client.RemoteEndPoint);
// Store them and put them in the waiting lobby
_clients.Add(newClient);
_waitingLobby.Add(newClient);
// Send a welcome message
string msg = String.Format("Welcome to the \"{0}\" Games Server.\n", Name);
await SendPacket(newClient, new Packet("message", msg));
}
// Will attempt to gracefully disconnect a TcpClient
// This should be use for clients that may be in a game, or the waiting lobby
public void DisconnectClient(TcpClient client, string message="")
{
Console.WriteLine("Disconnecting the client from {0}.", client.Client.RemoteEndPoint);
// If there wasn't a message set, use the default "Goodbye."
if (message == "")
message = "Goodbye.";
// Send the "bye," message
Task byePacket = SendPacket(client, new Packet("bye", message));
// Notify a game that might have them
try
{
_gameClientIsIn[client]?.DisconnectClient(client);
} catch (KeyNotFoundException) { }
// Give the client some time to send and proccess the graceful disconnect
Thread.Sleep(100);
// Cleanup resources on our end
byePacket.GetAwaiter().GetResult();
HandleDisconnectedClient(client);
}
// Cleans up the resources if a client has disconnected,
// gracefully or not. Will remove them from clint list and lobby
public void HandleDisconnectedClient(TcpClient client)
{
// Remove from collections and free resources
_clients.Remove(client);
_waitingLobby.Remove(client);
_cleanupClient(client);
}
#region Packet Transmission Methods
// Sends a packet to a client asynchronously
public async Task SendPacket(TcpClient client, Packet packet)
{
try
{
// convert JSON to buffer and its length to a 16 bit unsigned integer buffer
byte[] jsonBuffer = Encoding.UTF8.GetBytes(packet.ToJson());
byte[] lengthBuffer = BitConverter.GetBytes(Convert.ToUInt16(jsonBuffer.Length));
// Join the buffers
byte[] msgBuffer = new byte[lengthBuffer.Length + jsonBuffer.Length];
lengthBuffer.CopyTo(msgBuffer, 0);
jsonBuffer.CopyTo(msgBuffer, lengthBuffer.Length);
// Send the packet
await client.GetStream().WriteAsync(msgBuffer, 0, msgBuffer.Length);
//Console.WriteLine("[SENT]\n{0}", packet);
}
catch (Exception e)
{
// There was an issue is sending
Console.WriteLine("There was an issue receiving a packet.");
Console.WriteLine("Reason: {0}", e.Message);
}
}
// Will get a single packet from a TcpClient
// Will return null if there isn't any data available or some other
// issue getting data from the client
public async Task<Packet> ReceivePacket(TcpClient client)
{
Packet packet = null;
try
{
// First check there is data available
if (client.Available == 0)
return null;
NetworkStream msgStream = client.GetStream();
// There must be some incoming data, the first two bytes are the size of the Packet
byte[] lengthBuffer = new byte[2];
await msgStream.ReadAsync(lengthBuffer, 0, 2);
ushort packetByteSize = BitConverter.ToUInt16(lengthBuffer, 0);
// Now read that many bytes from what's left in the stream, it must be the Packet
byte[] jsonBuffer = new byte[packetByteSize];
await msgStream.ReadAsync(jsonBuffer, 0, jsonBuffer.Length);
// Convert it into a packet datatype
string jsonString = Encoding.UTF8.GetString(jsonBuffer);
packet = Packet.FromJson(jsonString);
//Console.WriteLine("[RECEIVED]\n{0}", packet);
}
catch (Exception e)
{
// There was an issue in receiving
Console.WriteLine("There was an issue sending a packet to {0}.", client.Client.RemoteEndPoint);
Console.WriteLine("Reason: {0}", e.Message);
}
return packet;
}
#endregion // Packet Transmission Methods
#region TcpClient Helper Methods
// Checks if a client has disconnected ungracefully
// Adapted from: http://stackoverflow.com/questions/722240/instantly-detect-client-disconnection-from-server-socket
public static bool IsDisconnected(TcpClient client)
{
try
{
Socket s = client.Client;
return s.Poll(10 * 1000, SelectMode.SelectRead) && (s.Available == 0);
}
catch(SocketException)
{
// We got a socket error, assume it's disconnected
return true;
}
}
// cleans up resources for a TcpClient and closes it
private static void _cleanupClient(TcpClient client)
{
client.GetStream().Close(); // Close network stream
client.Close(); // Close client
}
#endregion // TcpClient Helper Methods
#region Program Execution
public static TcpGamesServer gamesServer;
// For when the user Presses Ctrl-C, this will gracefully shutdown the server
public static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
{
args.Cancel = true;
gamesServer?.Shutdown();
}
public static void Main(string[] args)
{
// Some arguments
string name = "Bad BBS";//args[0];
int port = 6000;//int.Parse(args[1]);
// Handler for Ctrl-C presses
Console.CancelKeyPress += InterruptHandler;
// Create and run the server
gamesServer = new TcpGamesServer(name, port);
gamesServer.Run();
}
#endregion // Program Execution
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment