diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a05b287
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+# IDE0011: Add braces
+csharp_prefer_braces = when_multiline
diff --git a/.gitignore b/.gitignore
index 27ae6f1..2f72ce3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -373,3 +373,6 @@ FodyWeavers.xsd
# Local History for Visual Studio Code
.history/
+# Ignore testing board
+TestingBoard/
+
diff --git a/PDGServer_WPF/App.xaml b/PDGServer_WPF/App.xaml
new file mode 100644
index 0000000..7124177
--- /dev/null
+++ b/PDGServer_WPF/App.xaml
@@ -0,0 +1,6 @@
+
+
diff --git a/PDGServer_WPF/App.xaml.cs b/PDGServer_WPF/App.xaml.cs
new file mode 100644
index 0000000..c866abb
--- /dev/null
+++ b/PDGServer_WPF/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace RBG_Server
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/PDGServer_WPF/AssemblyInfo.cs b/PDGServer_WPF/AssemblyInfo.cs
new file mode 100644
index 0000000..8b5504e
--- /dev/null
+++ b/PDGServer_WPF/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/PDGServer_WPF/MainWindow.xaml b/PDGServer_WPF/MainWindow.xaml
new file mode 100644
index 0000000..bc7a7da
--- /dev/null
+++ b/PDGServer_WPF/MainWindow.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PDGServer_WPF/MainWindow.xaml.cs b/PDGServer_WPF/MainWindow.xaml.cs
new file mode 100644
index 0000000..17aca1b
--- /dev/null
+++ b/PDGServer_WPF/MainWindow.xaml.cs
@@ -0,0 +1,290 @@
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using System.Windows.Media.Effects;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace RBG_Server.WPF
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ GameServer gameServer;
+ MemoryStream gameBoard;
+ public MainWindow()
+ {
+ InitializeComponent();
+ //gameServer = new GameServer(8, 0, 9, 9, 3, 3, 3, 3, null);
+ }
+
+ ///
+ /// Browse for file button
+ ///
+ ///
+ ///
+ private void BrowseButton_Click(object sender, RoutedEventArgs e)
+ {
+ OpenFileDialog ofd = new();
+ ofd.Filter = "Image Files (.png; .jpg)|*.png;*.jpg";
+ if (ofd.ShowDialog() == true)
+ {
+ ImageSourceTextBox.Text = ofd.FileName;
+ UpdateImage();
+ }
+ }
+
+ ///
+ /// Update the image when the text box looses focus
+ ///
+ ///
+ ///
+ private void ImageSourceTextBox_LostFocus(object sender, RoutedEventArgs e)
+ {
+ UpdateImage();
+ }
+
+ private void UpdateImage()
+ {
+ try
+ {
+ using FileStream fs = File.OpenRead(ImageSourceTextBox.Text);
+ gameBoard = new();
+ fs.CopyTo(gameBoard); // Load into memory, so the server can also utilise the stream
+
+ BitmapImage gameBoardImage = new();
+ gameBoardImage.BeginInit();
+ gameBoardImage.StreamSource = gameBoard;
+ //gameBoardImage.CacheOption = BitmapCacheOption.OnLoad; // Must preload the image into memory, before it is unloaded
+ gameBoardImage.EndInit();
+ PreviewImage.Source = gameBoardImage;
+ RedrawGrid();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+
+ }
+
+ }
+ ///
+ /// Creates and starts the game server
+ ///
+ ///
+ ///
+ private void StartServerButton_Click(object sender, RoutedEventArgs e)
+ {
+ gameServer = new((int)StartingRow.Value, (int)StartingColumn.Value, (int)NumberOfRows.Value, (int)NumberOfColumns.Value, (int)ZoomBoxStartRow.Value, (int)ZoomBoxStartColumn.Value, (int)ZoomBoxSpan.Value, (int)ZoomBoxSpan.Value, gameBoard);
+ }
+
+
+ private void NumberOfRows_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (IsInitialized)
+ {
+ StartingRow.Maximum = NumberOfRows.Value;
+ ZoomBoxStartRow.Maximum = NumberOfRows.Value;
+ ZoomBoxSpan.Maximum = Math.Min(NumberOfRows.Value - ZoomBoxStartRow.Value, NumberOfColumns.Value - ZoomBoxStartColumn.Value);
+ RedrawGrid();
+ }
+ }
+
+ private void NumberOfColumns_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (IsInitialized)
+ {
+ StartingColumn.Maximum = NumberOfColumns.Value;
+ ZoomBoxStartColumn.Maximum = NumberOfColumns.Value;
+ ZoomBoxSpan.Maximum = Math.Min(NumberOfRows.Value - ZoomBoxStartRow.Value, NumberOfColumns.Value - ZoomBoxStartColumn.Value);
+ RedrawGrid();
+ }
+ }
+
+ private void RedrawGrid()
+ {
+ SolidColorBrush borderBrush = new(Color.FromRgb(255, 0, 0));
+ PreviewImageOverlay.RowDefinitions.Clear();
+ PreviewImageOverlay.ColumnDefinitions.Clear();
+ PreviewImageOverlay.Children.Clear();
+ for (int i = 0; i < NumberOfColumns.Value; i++)
+ {
+ PreviewImageOverlay.ColumnDefinitions.Add(new ColumnDefinition());
+ }
+ for (int i = 0; i < NumberOfRows.Value; i++)
+ {
+ PreviewImageOverlay.RowDefinitions.Add(new RowDefinition());
+ }
+ // Draw the overlay
+ Rectangle overlay = new();
+ overlay.Fill = new SolidColorBrush(Color.FromArgb(128, 0, 128, 255));
+ _ = PreviewImageOverlay.Children.Add(overlay);
+ Grid.SetRow(overlay, (int)ZoomBoxStartRow.Value);
+ Grid.SetColumn(overlay, (int)ZoomBoxStartColumn.Value);
+ Grid.SetRowSpan(overlay, (int)ZoomBoxSpan.Value);
+ Grid.SetColumnSpan(overlay, (int)ZoomBoxSpan.Value);
+ // Draw the starting position
+ overlay = new();
+ overlay.Fill = new SolidColorBrush(Color.FromArgb(64, 0, 255, 128));
+ _ = PreviewImageOverlay.Children.Add(overlay);
+ Grid.SetRow(overlay, (int)StartingRow.Value);
+ Grid.SetColumn(overlay, (int)StartingColumn.Value);
+ // Draw grids onto the preview image
+ for (int v = 0; v < PreviewImageOverlay.RowDefinitions.Count; v++)
+ {
+ for (int u = 0; u < PreviewImageOverlay.ColumnDefinitions.Count; u++)
+ {
+ Border border = new();
+ border.BorderBrush = borderBrush;
+ border.BorderThickness = new Thickness(2);
+ _ = PreviewImageOverlay.Children.Add(border);
+ Grid.SetRow(border, v);
+ Grid.SetColumn(border, u);
+ }
+ }
+
+ }
+
+ ///
+ /// Generates elements to go inside the game grid from the provided list of players
+ /// The grid should be appropriately sized before using this
+ ///
+ private void GenerateGameBoard(List players)
+ {
+ SolidColorBrush buttonBrush = new(Color.FromArgb(1, 255, 255, 255));
+ Grid[] cells = new Grid[PreviewImageOverlay.RowDefinitions.Count * PreviewImageOverlay.ColumnDefinitions.Count];
+ List unaddedPlayers = players;
+ for (int i = 0; i < cells.Length; i++)
+ {
+ cells[i] = new Grid();
+ // Each cell has several components:
+ // Stackpanel, which has each sprite in the cell
+ UniformGrid cellStack = new();
+ cellStack.Columns = 4;
+ //cellStack.ItemWidth = cellStack.Width / 4;
+ Queue removedPlayers = new();
+ foreach (Player item in unaddedPlayers)
+ {
+ if ((item.Row * PreviewImageOverlay.ColumnDefinitions.Count) + item.Column == i)
+ {
+ removedPlayers.Enqueue(item);
+ }
+ }
+ while (removedPlayers.Count > 0)
+ {
+ Player removed = removedPlayers.Dequeue();
+ _ = unaddedPlayers.Remove(removed);
+ _ = cellStack.Children.Add(removed);
+ }
+ _ = cells[i].Children.Add(cellStack);
+ // Button, whose content is empty
+ Button cellButton = new()
+ {
+ Content = ""
+ };
+ cellButton.Background = buttonBrush;
+ cellButton.Tag = new Tuple(i / PreviewImageOverlay.ColumnDefinitions.Count, i % PreviewImageOverlay.ColumnDefinitions.Count); // Tag is a Tuple
+ cellButton.Click += CellButton_Click;
+ _ = cells[i].Children.Add(cellButton);
+ _ = PreviewImageOverlay.Children.Add(cells[i]);
+ Grid.SetColumn(cells[i], i % PreviewImageOverlay.ColumnDefinitions.Count);
+ Grid.SetRow(cells[i], i / PreviewImageOverlay.ColumnDefinitions.Count);
+ }
+ }
+ ///
+ /// Represents a cell on the board
+ ///
+ class BoardCell
+ {
+ // brush the button should have
+ static SolidColorBrush buttonBrush = new(Color.FromArgb(1, 255, 255, 255));
+
+ // Each cell has a uniform grid that
+ UniformGrid cellStack = new()
+ {
+ Columns = 4,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ VerticalAlignment = VerticalAlignment.Stretch
+ };
+ Button cellButton = new()
+ {
+ Content = ""
+ };
+ Queue removedPlayers = new();
+ Grid parentGrid;
+
+ public BoardCell(Grid parentGrid)
+ {
+ this.parentGrid = parentGrid;
+ }
+
+ }
+
+ private void GenerateNewGameBoard(List players)
+ {
+ SolidColorBrush buttonBrush = new(Color.FromArgb(1, 255, 255, 255));
+ Grid[] cells = new Grid[PreviewImageOverlay.RowDefinitions.Count * PreviewImageOverlay.ColumnDefinitions.Count];
+ List unaddedPlayers = players;
+ for (int i = 0; i < cells.Length; i++)
+ {
+
+ }
+ }
+
+ private void CellButton_Click(object sender, RoutedEventArgs e)
+ {
+ Button cellButton = sender as Button;
+ Console.WriteLine("Button at {0}, {1} was clicked", ((Tuple)cellButton.Tag).Item2, ((Tuple)cellButton.Tag).Item1);
+ }
+
+ private void Sliders_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (IsInitialized)
+ {
+ RedrawGrid();
+ }
+ }
+
+ private void GridVisibilityToggleButton_Click(object sender, RoutedEventArgs e)
+ {
+ PreviewImageOverlay.Visibility = PreviewImageOverlay.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ private static readonly string[] sprites = new string[]
+ {
+ "01.png",
+ "02.png",
+ "03.png",
+ "04.png",
+ };
+ private void DrawRandomizedPlayers_Click(object sender, RoutedEventArgs e)
+ {
+ List randPlayers = new();
+ Random rand = new();
+ int count = rand.Next(1, 10);
+ for (int i = 0; i < count; i++)
+ {
+ // Create a new player at a random location
+ Player p = new("Player " + i, rand.Next(0, PreviewImageOverlay.RowDefinitions.Count), rand.Next(PreviewImageOverlay.ColumnDefinitions.Count));
+ // Load the sprite
+ BitmapSource pSprite = new BitmapImage(new Uri("pack://application:,,,/Sprites/" + sprites[rand.Next(0, sprites.Length)]));
+ // Recolour:
+ if (rand.Next(1, 4) == 1)
+ {
+ pSprite = RBG.Helpers.ImageProcessing.UpdatePixelColours(pSprite, 0.0, 0.0, 1.0);
+ }
+ p.Source = pSprite;
+ randPlayers.Add(p);
+ }
+
+ GenerateGameBoard(randPlayers);
+ }
+ }
+
+}
diff --git a/PDGServer_WPF/RBG Server WPF.csproj b/PDGServer_WPF/RBG Server WPF.csproj
new file mode 100644
index 0000000..fbf7f6a
--- /dev/null
+++ b/PDGServer_WPF/RBG Server WPF.csproj
@@ -0,0 +1,22 @@
+
+
+
+ WinExe
+ net5.0-windows10.0.19041.0
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pokemon Drinking Game/.editorconfig b/Pokemon Drinking Game/.editorconfig
new file mode 100644
index 0000000..a05b287
--- /dev/null
+++ b/Pokemon Drinking Game/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+# IDE0011: Add braces
+csharp_prefer_braces = when_multiline
diff --git a/Pokemon Drinking Game/App.xaml b/Pokemon Drinking Game/App.xaml
new file mode 100644
index 0000000..f6e2017
--- /dev/null
+++ b/Pokemon Drinking Game/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Pokemon Drinking Game/App.xaml.cs b/Pokemon Drinking Game/App.xaml.cs
new file mode 100644
index 0000000..ef5edec
--- /dev/null
+++ b/Pokemon Drinking Game/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace Pokemon_Drinking_Game
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/Pokemon Drinking Game/AssemblyInfo.cs b/Pokemon Drinking Game/AssemblyInfo.cs
new file mode 100644
index 0000000..8b5504e
--- /dev/null
+++ b/Pokemon Drinking Game/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/Pokemon Drinking Game/MainWindow.xaml b/Pokemon Drinking Game/MainWindow.xaml
new file mode 100644
index 0000000..44345a3
--- /dev/null
+++ b/Pokemon Drinking Game/MainWindow.xaml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pokemon Drinking Game/MainWindow.xaml.cs b/Pokemon Drinking Game/MainWindow.xaml.cs
new file mode 100644
index 0000000..1b3b7b0
--- /dev/null
+++ b/Pokemon Drinking Game/MainWindow.xaml.cs
@@ -0,0 +1,338 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace Pokemon_Drinking_Game
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ BitmapImage subImage;
+ List players;
+ int ourPlayerIndex;
+ int currentTurn;
+ Timer updateTimer;
+ TcpClient client;
+
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ // Load the sub image just once
+ loadImage();
+ players = new List();
+ //TcpClient client = new TcpClient("pdgserver.kauripeak.co.nz", 13000);
+
+ }
+
+ void loadImage()
+ {
+ subImage = new(new Uri("pack://application:,,,/pokemon_v3.png"));
+ int i = 0;
+ }
+
+ private void buttons_MouseEnter(object sender, MouseEventArgs e)
+ {
+ // Replace center image
+
+ zoomPieceImage.Source = retrieveSubImage(subImage, (Button)sender);
+ zoomPieceImage.Visibility = Visibility.Visible;
+ }
+
+ private void buttons_MouseLeave(object sender, MouseEventArgs e)
+ {
+ // Hide center image
+ //zoomPieceImage.Source = new BitmapImage();
+ zoomPieceImage.Visibility = Visibility.Collapsed;
+ }
+
+ CroppedBitmap retrieveSubImage(BitmapImage source, Button button)
+ {
+ // Get various element heights & widths
+ int imageWidth = source.PixelWidth;
+ int imageHight = source.PixelHeight;
+
+ int pixPerTile = imageWidth / 9 -1;
+
+ double parentHeight = ((Grid)button.Parent).ActualHeight;
+ double parentWidth = ((Grid)button.Parent).ActualWidth;
+
+
+ int row = -1;
+ int col = -1;
+
+ // Hard coding cause its a bitch
+
+ if (button == palletTownButton)
+ {
+ col = 0;
+ row = 8;
+ }
+ else if (button == rattataButton) { col = 0; row = 7; }
+ else if (button == pidgeyButton) { col = 0; row = 6; }
+ else if (button == caterpieButton) { col = 0; row = 5; }
+ else if (button == pikachuButton) { col = 0; row = 4; }
+ else if (button == beedrilButton) { col = 0; row = 3; }
+ else if (button == pewterButton) { col = 0; row = 2; }
+ else if (button == nidoranButton) { col = 0; row = 1; }
+ else if (button == zubatButton) { col = 0; row = 0; }
+ //
+ else if (button == clefairyButton) { col = 1; row = 0; }
+ else if (button == jigglypuffButton) { col = 2; row = 0; }
+ else if (button == abraButton) { col = 3; row = 0; }
+ else if (button == garyButton) { col = 4; row = 0; }
+ else if (button == ceruleanButton) { col = 5; row = 0; }
+ else if (button == slowpokeButton) { col = 6; row = 0; }
+ else if (button == bellsproutButton) { col = 7; row = 0; }
+ else if (button == meowthButton) { col = 8; row = 0; }
+ //
+ else if (button == diglettButton) { col = 8; row = 1; }
+ else if (button == ssanneButton) { col = 8; row = 2; }
+ else if (button == vermillionButton) { col = 8; row = 3; }
+ else if (button == bicycleButton) { col = 8; row = 4; }
+ else if (button == magikarpButton) { col = 8; row = 5; }
+ else if (button == sandshrewButton) { col = 8; row = 6; }
+ else if (button == pokemontowerButton) { col = 8; row = 7; }
+ else if (button == channelerButton) { col = 8; row = 8; }
+ //
+ else if (button == haunterButton) { col = 7; row = 8; }
+ else if (button == cuboneButton) { col = 6; row = 8; }
+ else if (button == ghostButton) { col = 5; row = 8; }
+ else if (button == abra2Button) { col = 4; row = 8; }
+ else if (button == snorlaxButton) { col = 3; row = 8; }
+ else if (button == gary2Button) { col = 2; row = 8; }
+ else if (button == eeveeButton) { col = 1; row = 8; }
+ //
+ else if (button == celadonButton) { col = 1; row = 7; }
+ else if (button == psyduckButton) { col = 1; row = 6; }
+ else if (button == evolutionButton) { col = 1; row = 5; }
+ else if (button == porygonButton) { col = 1; row = 4; }
+ else if (button == silphButton) { col = 1; row = 3; }
+ else if (button == scientistButton) { col = 1; row = 2; }
+ else if (button == laprasButton) { col = 1; row = 1; }
+ //
+ else if (button == rocketButton) { col = 2; row = 1; }
+ else if (button == giovanniButton) { col = 3; row = 1; }
+ else if (button == rarecandyButton) { col = 4; row = 1; }
+ else if (button == gary3Button) { col = 5; row = 1; }
+ else if (button == saffronButton) { col = 6; row = 1; }
+ else if (button == hitmonsButton) { col = 7; row = 1; }
+ //
+ else if (button == krabbyButton) { col = 7; row = 2; }
+ else if (button == dittoButton) { col = 7; row = 3; }
+ else if (button == doduoButton) { col = 7; row = 4; }
+ else if (button == safariButton) { col = 7; row = 5; }
+ else if (button == dratiniButton) { col = 7; row = 6; }
+ else if (button == taurosButton) { col = 7; row = 7; }
+ //
+ else if (button == chanseyButton) { col = 6; row = 7; }
+ else if (button == fuchsiaButton) { col = 5; row = 7; }
+ else if (button == electrodeButton) { col = 4; row = 7; }
+ else if (button == electabuzzButton) { col = 3; row = 7; }
+ else if (button == poliwagButton) { col = 2; row = 7; }
+ //
+ else if (button == seakingButton) { col = 2; row = 6; }
+ else if (button == missingnoButton) { col = 2; row = 5; }
+ else if (button == cinnabarButton) { col = 2; row = 4; }
+ else if (button == koffingButton) { col = 2; row = 3; }
+ else if (button == fossilButton) { col = 2; row = 2; }
+ //
+ else if (button == pokeballButton) { col = 3; row = 2; }
+ else if (button == persianButton) { col = 4; row = 2; }
+ else if (button == viridianButton) { col = 5; row = 2; }
+ else if (button == fearowButton) { col = 6; row = 2; }
+ //
+ else if (button == gravellerButton) { col = 6; row = 3; }
+ else if (button == gyradosButton) { col = 6; row = 4; }
+ else if (button == dragoniteButton) { col = 6; row = 5; }
+ else if (button == legendbirdButton) { col = 6; row = 6; }
+ //
+ else if (button == elitefourButton) { col = 5; row = 6; }
+ else if (button == championButton) { col = 4; row = 6; }
+ else if (button == masterButton) { col = 3; row = 6; }
+
+
+
+ Int32Rect pixelRect = new Int32Rect(pixPerTile * col, pixPerTile * row , pixPerTile, pixPerTile);
+ CroppedBitmap cb = new CroppedBitmap(source, pixelRect);
+ return cb;
+ }
+
+
+ byte[] overflowBuffer;
+ ///
+ /// Server logic
+ /// Called periodically (e.g. 1/10 of a second), retrieves new data from the buffer, and
+ /// sends out any updates that have occured.
+ ///
+ /// Data packets are 0xFF terminated (new command is the proceeding byte)
+ /// Data terminations are 0xFE (next data type is the proceeding byte; does not apply to deterministic primative sizes)
+ /// strings are expected to be sent in UTF-8 format
+ ///
+ /// As this is all in TCP, data is guaranteed to be in the correct order
+ ///
+ void ClientUpdate()
+ {
+ // ***************************
+ // **** Ensure connected ***
+ // ***************************
+ if (!client.Connected)
+ {
+
+ }
+ // ***************************
+ // ****** Recieve Data *****
+ // ***************************
+ if (client.Available > 0)
+ {
+ int available = client.Available;
+ // Internal byte array to store all awaiting bytes
+ byte[] buffer = new byte[available];
+ // Get the underlying networkstream, to read and write
+ NetworkStream connectionStream = client.GetStream();
+ connectionStream.Read(buffer, 0, available);
+
+ int pos = 0;
+
+ while (pos < available)
+ {
+ int command = buffer[pos] >> 4; // Get the four command bits
+ ourPlayerIndex = buffer[pos++] & 0xF; // Ensure we are the correct indexed player
+
+ switch (command) // post-increment past the command byte
+ {
+ case 0: // Server queries a heartbeat response
+ connectionStream.Write(new byte[] { (byte)ourPlayerIndex, 1, 0xFF });
+ break;
+ case 1: // Server has a board (game) update
+ while (buffer[pos] != 0xFF)
+ {
+ // First four bits, after the command is the number of players
+ int playerCount = buffer[pos] >> 4;
+ // Next four are the number of players
+ currentTurn = buffer[pos++] | 0xF;
+ // Next
+
+
+ // If there is a different number of players, create a new list of players
+ if (players.Count != playerCount)
+ {
+ players = new List(playerCount);
+ }
+ // Next bit is a flag, denoting if the current player is a
+
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+ UpdateUI();
+ }
+ // ***************************
+ // ******* Send Data *******
+ // ***************************
+
+ }
+
+ void UpdateUI()
+ {
+ Brush labelBrush = new SolidColorBrush(Color.FromRgb(255, 255, 255));
+
+ Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Render, new Action(() =>
+ {
+ PlayerList.Children.Clear();
+ // do UI updates here
+ foreach (var item in players)
+ {
+ Label playerLabel = new Label()
+ {
+ Content = item.name,
+ Foreground = labelBrush
+ };
+ PlayerList.Children.Add(playerLabel);
+ }
+ }));
+ }
+
+ private void ConnectButton_Click(object sender, RoutedEventArgs e)
+ {
+ // Do connection using props
+ if (client != null)
+ {
+ client.Close();
+ }
+ bool success = false;
+ try
+ {
+ client = new TcpClient(AddressTextBox.Text, Convert.ToInt32(PortTextBox.Text));
+ success = true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ if (success)
+ {
+ ConnectionGrid.Visibility = Visibility.Collapsed;
+ GameGrid.Visibility = Visibility.Visible;
+
+ ourPlayerIndex = 0;
+ currentTurn = 0;
+
+ List sendBytes = new();
+ sendBytes.Add(1 << 4);
+ sendBytes.AddRange(Encoding.UTF8.GetBytes(PlayerNameTextBox.Text));
+ sendBytes.Add(0xFF);
+ // Send through the player name
+ client.GetStream().Write(sendBytes.ToArray());
+ // Add listener for data
+ updateTimer = new Timer((t) =>
+ {
+ ClientUpdate();
+ });
+ updateTimer.Change(100, 33);
+ }
+
+ }
+ }
+
+
+
+ struct Player
+ {
+ public string name;
+ public int row;
+ public int column;
+ public long lastTime;
+
+ public Player(string name, int row, int column)
+ {
+ this.name = name;
+ this.row = row;
+ this.column = column;
+ this.lastTime = DateTime.Now.Ticks;
+ }
+
+ public override string ToString()
+ {
+ return name + " (" + column + ", " + row + ")";
+ }
+ }
+}
diff --git a/Pokemon Drinking Game/Pokemon Drinking Game.csproj b/Pokemon Drinking Game/Pokemon Drinking Game.csproj
new file mode 100644
index 0000000..3db3da3
--- /dev/null
+++ b/Pokemon Drinking Game/Pokemon Drinking Game.csproj
@@ -0,0 +1,20 @@
+
+
+
+ WinExe
+ net5.0-windows
+ Pokemon_Drinking_Game
+ true
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Pokemon Drinking Game/Pokemon Drinking Game.sln b/Pokemon Drinking Game/Pokemon Drinking Game.sln
new file mode 100644
index 0000000..7fe3656
--- /dev/null
+++ b/Pokemon Drinking Game/Pokemon Drinking Game.sln
@@ -0,0 +1,42 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31515.178
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokemon Drinking Game", "Pokemon Drinking Game.csproj", "{3BF04BFD-307C-4878-B420-47DFFAD97BE0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PDGServer", "..\PDGServer\PDGServer.csproj", "{FD6A1623-2695-4353-8B4E-7115BDCB34FD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PDGServer_WPF", "..\PDGServer_WPF\PDGServer_WPF.csproj", "{78CE20FA-E26B-4C12-9498-6EFB058586E5}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C5002D-A70D-4B97-8BDD-F02008F4B79B}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3BF04BFD-307C-4878-B420-47DFFAD97BE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3BF04BFD-307C-4878-B420-47DFFAD97BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3BF04BFD-307C-4878-B420-47DFFAD97BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3BF04BFD-307C-4878-B420-47DFFAD97BE0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD6A1623-2695-4353-8B4E-7115BDCB34FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD6A1623-2695-4353-8B4E-7115BDCB34FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD6A1623-2695-4353-8B4E-7115BDCB34FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD6A1623-2695-4353-8B4E-7115BDCB34FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {78CE20FA-E26B-4C12-9498-6EFB058586E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {78CE20FA-E26B-4C12-9498-6EFB058586E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {78CE20FA-E26B-4C12-9498-6EFB058586E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {78CE20FA-E26B-4C12-9498-6EFB058586E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {35DA9C3E-6A70-4E39-85A6-1843C79A07D4}
+ EndGlobalSection
+EndGlobal
diff --git a/RBG.Helpers/ImageProcessing.cs b/RBG.Helpers/ImageProcessing.cs
new file mode 100644
index 0000000..3f88666
--- /dev/null
+++ b/RBG.Helpers/ImageProcessing.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace RBG.Helpers
+{
+ public class ImageProcessing
+ {
+ ///
+ /// Returns a copy of the original bitmap with a specified per-channel colour multiplier
+ /// Each channel expects a rate 0-1 (will handle other values, with unexpected results)
+ /// Defaults to 1.0 for each channel (no modification)
+ ///
+ /// The source bitmap
+ /// The red-channel multiplier
+ /// The green-channel multiplier
+ /// The blue-channel multiplier
+ /// The alpha-channel multiplier
+ ///
+ public static BitmapSource UpdatePixelColours(BitmapSource sourceBitmap, double redRate = 1.0, double greenRate = 1.0, double blueRate = 1.0, double alphaRate = 1.0)
+ {
+ // Create a writeable bitmap from the source bitmap
+ WriteableBitmap wbmp = new(sourceBitmap);
+ // Ensure 32 bpp
+ if (wbmp.Format.BitsPerPixel != 32) return null;
+ // Lock the bitmap so the buffer cannot change
+ wbmp.Lock();
+ // Enter unsafe code (used over marshalled pointer access so that it is more performant, but has a
+ // higher security risk as this is raw [but still bounded] memory access ( => data may be changed by a third party)
+ // As the buffer is of a known size, and not terminated by a specific symbol, modified data in the buffer is
+ // unlikely to cause a novel vulnerability in the software ( => vulnerability exists in BitmapSource, can be
+ // exploited with any bitmap image being loaded)
+ unsafe
+ {
+ // Retrieve the buffer pointer
+ int* arrayptr = (int*)wbmp.BackBuffer;
+ // Iterate through each pixel
+ for (int i = 0; i < wbmp.PixelWidth * wbmp.PixelHeight; i++)
+ {
+ // Dereference the exact pixel value
+ int pixel = *(arrayptr + i);
+ // decompose the colour channels
+ int a = (pixel >> 24) & 0xFF;
+ int r = (pixel >> 16) & 0xFF;
+ int g = (pixel >> 8) & 0xFF;
+ int b = pixel & 0xFF;
+ // Calc new values & clamp to a max of 0xFF (8 bits per channel)
+ int newB = (int)Math.Min((uint)(b * blueRate), 255);
+ int newG = (int)Math.Min((uint)(g * greenRate), 255);
+ int newR = (int)Math.Min((uint)(r * redRate), 255);
+ int newA = (int)Math.Min((uint)(a * alphaRate), 255);
+ // Set the pixel to the new value
+ *(arrayptr + i) = (newA << 24) | (newR << 16) | (newG << 8) | newB;
+ }
+ }
+ // Invalidate the full current image, so that the new buffer data is fully applied
+ wbmp.AddDirtyRect(new Int32Rect(0, 0, wbmp.PixelWidth, wbmp.PixelHeight));
+ // Unlock the image, as everything has finished changing
+ wbmp.Unlock();
+ return wbmp;
+ }
+
+ ///
+ /// Retrieves a cropped area of the provided bitmap
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static BitmapSource RetrieveSubImage(BitmapSource source, int row, int col, int rowSize, int colSize, int colsIncluded = 1, int rowsIncluded = 1)
+ {
+ // Get number of pixels per segment
+ int rowPixelsPerTile = (source.PixelWidth / rowSize) - 1;
+ int colPixelsPerTile = (source.PixelHeight / colSize) - 1;
+ // Create a cropping bound
+ Int32Rect pixelRect = new(colPixelsPerTile * col, rowPixelsPerTile * row, colPixelsPerTile * colsIncluded, rowPixelsPerTile * rowsIncluded);
+ return new CroppedBitmap(source, pixelRect);
+ }
+
+
+ }
+}
diff --git a/RBG.Helpers/RBG.Helpers.csproj b/RBG.Helpers/RBG.Helpers.csproj
new file mode 100644
index 0000000..29f3391
--- /dev/null
+++ b/RBG.Helpers/RBG.Helpers.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net5.0
+
+
+
+ true
+
+
+
+ true
+
+
+
+
+
+
diff --git a/RBG_Server.Core/GameServer.cs b/RBG_Server.Core/GameServer.cs
new file mode 100644
index 0000000..1d0eedd
--- /dev/null
+++ b/RBG_Server.Core/GameServer.cs
@@ -0,0 +1,428 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace RBG_Server
+{
+ public class GameServer
+ {
+ private readonly byte startRow, startColumn, rowSize, columnSize, zoomRow, zoomCol, zoomRowSpan, zoomColSpan;
+ private readonly Dictionary boardImages;
+
+
+ ///
+ /// Initialise a new server, with the provided starting row & column
+ ///
+ ///
+ /// The starting row of the board
+ /// The starting column of the board
+ /// The full-scale board image
+ public GameServer(int startRow, int startColumn, int rowSize, int columnSize, int zoomRow, int zoomCol, int zoomRowSpan, int zoomColSpan, Stream boardImage)
+ {
+ this.startRow = (byte)startRow;
+ this.startColumn = (byte)startColumn;
+ this.rowSize = (byte)rowSize;
+ this.columnSize = (byte)columnSize;
+ this.zoomRow = (byte)zoomRow;
+ this.zoomCol = (byte)zoomCol;
+ this.zoomRowSpan = (byte)zoomRowSpan;
+ this.zoomColSpan = (byte)zoomColSpan;
+ BitmapImage gameBoardImage = new();
+ gameBoardImage.StreamSource = boardImage;
+ // Hold images
+ Dictionary sourceImages = new();
+ // Create a JPEG encoder
+ JpegBitmapEncoder encoder = new()
+ {
+ QualityLevel = 100,
+ };
+ // Add the base frame to the encoder and retrieve its byte array
+ encoder.Frames.Add(BitmapFrame.Create(gameBoardImage));
+ using (MemoryStream stream = new())
+ {
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(-1, frameData);
+ encoder.Frames.Clear();
+ }
+
+ // Add a resized version for each "common" scale
+ if (gameBoardImage.PixelHeight > 2160)
+ {
+ double rate = 2160 / gameBoardImage.PixelHeight;
+ TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ using MemoryStream stream = new();
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(2160, frameData);
+ encoder.Frames.Clear();
+ }
+ if (gameBoardImage.PixelHeight > 1440)
+ {
+ double rate = 1440 / gameBoardImage.PixelHeight;
+ TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ using MemoryStream stream = new();
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(1440, frameData);
+ encoder.Frames.Clear();
+ }
+ if (gameBoardImage.PixelHeight > 1080)
+ {
+ double rate = 1080 / gameBoardImage.PixelHeight;
+ TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ using MemoryStream stream = new();
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(1080, frameData);
+ encoder.Frames.Clear();
+ }
+ if (gameBoardImage.PixelHeight > 720)
+ {
+ double rate = 720 / gameBoardImage.PixelHeight;
+ TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ using MemoryStream stream = new();
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(720, frameData);
+ encoder.Frames.Clear();
+ }
+ // Send a 240p thumbnail
+ {
+ double rate = 240 / gameBoardImage.PixelHeight;
+ TransformedBitmap modified = new(gameBoardImage, new ScaleTransform(gameBoardImage.PixelWidth * rate, gameBoardImage.PixelHeight * rate));
+ encoder.Frames.Add(BitmapFrame.Create(modified));
+ using MemoryStream stream = new();
+ encoder.Save(stream);
+ byte[] frameData = stream.ToArray();
+ sourceImages.Add(240, frameData);
+ encoder.Frames.Clear();
+ }
+
+ // add the board images to the
+ boardImages = sourceImages;
+ }
+
+ public List Players { get; } = new();
+ // Map players to connections/clients
+ Dictionary clients = new();
+ // Clients that have connected, but are not yet added to the list
+ ConcurrentQueue awaitingAdds = new();
+
+ void Init(IPAddress localAddr, int port)
+ {
+ TcpListener listener = new(localAddr, port);
+ listener.Start();
+ while (true)
+ {
+ TcpClient client = listener.AcceptTcpClient();
+ // Delegate to another thread
+ _ = Task.Run(() =>
+ {
+ AcceptConnections(client);
+ });
+ }
+ }
+ ///
+ /// Accept the client, and do the preprocessing
+ ///
+ ///
+ void AcceptConnections(TcpClient client)
+ {
+ // Ensure we send data immediately
+ client.NoDelay = true;
+ // In the case the client
+ NetworkStream stream = client.GetStream();
+ // Await the mode switch data from the client
+ int command;
+ do
+ {
+ command = stream.ReadByte();
+ break;
+ }
+ while (stream.DataAvailable);
+ if (command == 0xFD)
+ {
+ // Connect to game; send request
+ }
+ else if (command == 0xFC)
+ {
+ // Next byte contains the screen size
+ int screenSize = stream.ReadByte() << 8 | stream.ReadByte();
+ // Send three images, the low res, and the screen res, and the full res (in that order, so the client may display something
+ SendImage(240, stream);
+ // Send the customized screen res image
+ if (screenSize <= 720)
+ {
+ SendImage(720, stream);
+ }
+ else if (screenSize <= 1080)
+ {
+ SendImage(1080, stream);
+ }
+ else if (screenSize <= 1440)
+ {
+ SendImage(1440, stream);
+ }
+ else if (screenSize <= 2160)
+ {
+ SendImage(2160, stream);
+ }
+ // Finally, send the full-screen image
+ SendImage(-1, stream);
+ }
+ }
+
+ void SendImage(int index, NetworkStream stream)
+ {
+ byte[] dataSize = new byte[] { (byte)(boardImages[index].Length >> 24), (byte)(boardImages[index].Length >> 16), (byte)(boardImages[index].Length >> 8), (byte)(boardImages[index].Length & 0xFF) };
+ stream.Write(dataSize);
+ stream.Write(boardImages[index]);
+ }
+
+ static int currentTurn = 0;
+
+ void ServerUpdate()
+ {
+ // ***************************
+ // ** Accept new connections**
+ // ***************************
+ while (!awaitingAdds.IsEmpty)
+ {
+ // try to dequeue a client
+ if (awaitingAdds.TryDequeue(out TcpClient poppedClient))
+ {
+ // TODO:
+ // Check that the player isn't reconnecting
+ Player newPlayer = new("", startRow, startColumn);
+ Players.Add(newPlayer);
+ clients.Add(poppedClient, newPlayer);
+ Console.WriteLine("New player added.");
+ }
+ }
+ // Create a list of clients that are no longer connected, so they may be closed at the end of this
+ // loop
+ ConcurrentQueue closedClients = new();
+
+ bool turnCompleted = false;
+
+ // ***************************
+ // ***** Recieve Data *****
+ // ***************************
+ // Data are recieved in parallel, so that it may be processed quickly
+ Parallel.ForEach(clients, (item) =>
+ {
+ Player player = item.Value;
+ if (!item.Key.Connected)
+ {
+ // Client no longer connected, add to the removed list
+ closedClients.Enqueue(item.Key);
+ return;
+ }
+ else
+ {
+ // Game logic
+ // If there are available bytes to be read, we can process them
+ if (item.Key.Available > 0)
+ {
+ // Get number of bytes
+ int availableBytes = item.Key.Available;
+ // Store read bytes in an array
+ byte[] receivedBytes = new byte[player.UnhandledBuffer.Length + availableBytes];
+ // Grab the stream to read from/write to
+ NetworkStream clientStream = item.Key.GetStream();
+ // read the buffer to the end
+ clientStream.Read(receivedBytes, player.UnhandledBuffer.Length, availableBytes);
+
+ // Copy the unhandled buffer to the recieved bytes
+ player.UnhandledBuffer.CopyTo(receivedBytes, 0);
+ // Reset the unhandled array
+ player.UnhandledBuffer = Array.Empty();
+
+ int pos = 0;
+ // While we still haven't reached the end of the buffer, read bytes
+ while (pos < receivedBytes.Length)
+ {
+ if (pos == receivedBytes.Length - 1)
+ {
+ player.UnhandledBuffer = new byte[] { receivedBytes[^1] };
+ break;
+ }
+ int command = receivedBytes[pos] >> 4; // Get the four command bits
+ if ((receivedBytes[pos++] & 0xF) != Players.IndexOf(item.Value))
+ {
+ Console.WriteLine("Unexpected client ID: {0}", (receivedBytes[pos++] & 0xF));
+ }
+
+ switch (command)
+ {
+ case 0: // Heartbeat from client {0x0, 0x1, 0xFF}
+ // In case of buffer overflow, take the last command and paste it into
+ // the unhandled buffer
+ if (pos + 2 > receivedBytes.Length)
+ {
+ player.UnhandledBuffer = new byte[receivedBytes.Length - pos + 1];
+ for (int i = 0; i < receivedBytes.Length - pos + 1; i++)
+ {
+ player.UnhandledBuffer[i] = receivedBytes[(pos - 1) + i];
+ }
+ // Finally, set the pos to the end of the buffer
+ pos = receivedBytes.Length;
+ }
+ else
+ {
+ // Buffer won't overflow, process normally
+ player.LastTime = DateTime.Now.Ticks;
+ pos += 2; // Move two bytes
+ }
+ break;
+ case 1: // Player sets name
+ List strBytes = new();
+ bool endReached = false;
+ while (pos < receivedBytes.Length && receivedBytes[pos] != 0xFF)
+ {
+ byte nextByte = receivedBytes[pos++];
+ if (pos != receivedBytes.Length - 1 && receivedBytes[pos + 1] == 0xFF)
+ {
+ endReached = true;
+ }
+ else
+ {
+ strBytes.Add(nextByte);
+ }
+ }
+ if (endReached)
+ {
+ // Buffer contained full string, move to the next command
+ player.PlayerName = Encoding.UTF8.GetString(strBytes.ToArray());
+ }
+ else
+ {
+ player.UnhandledBuffer = new byte[receivedBytes.Length - strBytes.Count + 1];
+ for (int i = 0; i < receivedBytes.Length - strBytes.Count + 1; i++)
+ {
+ player.UnhandledBuffer[i] = receivedBytes[(strBytes.Count - 1) + i];
+ }
+ // Pos is implicitely
+ }
+ break;
+ case 2: // update player position
+ if (Players[currentTurn].Equals(player))
+ {
+ // It is our turn, move
+ byte nextByte = receivedBytes[pos++];
+ int row = nextByte >> 4;
+ int column = nextByte & 0xF;
+ player.Column = column;
+ player.Row = row;
+ // Can't update turn until we are synchronised, else we would
+ // inadvertently accept turns from the next player
+ turnCompleted = true;
+ }
+ break;
+ default:
+ Console.WriteLine("Unknown client command");
+ break;
+ }
+ }
+ }
+ // If we haven't recieved data from this client for some time, send a packet that requests
+ // a response
+ else if (TimeSpan.FromTicks(DateTime.Now.Ticks - item.Value.LastTime) > TimeSpan.FromSeconds(5))
+ {
+ item.Key.GetStream().Write(new byte[] { (byte)(Players.IndexOf(item.Value)), 0xFF }); // Heartbeat is 0x0, 0x1
+ Player curr = item.Value;
+ curr.LastTime = DateTime.Now.Ticks;
+ }
+ }
+ });
+ // Synchronised, update player turn
+ if (turnCompleted)
+ {
+ currentTurn++;
+ currentTurn %= Players.Count;
+ }
+ // ***************************
+ // ** Remove old connections**
+ // ***************************
+ foreach (var item in closedClients)
+ {
+ _ = Players.Remove(clients[item]);
+ _ = clients.Remove(item);
+ }
+ // ***************************
+ // ******* Send Data *******
+ // ***************************
+ IReadOnlyCollection dataPacket;
+ {
+ List allData = new();
+ allData.Add(1); // command bits; gets modulated when sent
+ byte playersTurn = (byte)(Players.Count << 4 | currentTurn); // Player count & turn
+ allData.Add(playersTurn);
+ // Data about each player
+ for (int i = 0; i < Players.Count; i++)
+ {
+ // Data begins with the UTF-8 formatted player-name string (and termination char)
+ allData.AddRange(Encoding.UTF8.GetBytes(Players[i].PlayerName));
+ allData.Add(0xFE); // EoString
+ // Next data byte is the row & column the player is in
+ byte pos = (byte)(Players[i].Row << 4 | Players[i].Column);
+ allData.Add(pos);
+ // End of player is implicit
+ }
+ // Finally, terminate the command
+ allData.Add(0xFF);
+ dataPacket = allData.AsReadOnly();
+ }
+ // Create a personalised data packet for each client (tells the client its order in the game)
+ // Sent in parallel, to prevent a misbehaving connection from delaying everyone
+ _ = Parallel.ForEach(clients, (x) =>
+ {
+ Player item = x.Value;
+ // Alert any further ops that there is a pending data send
+ if (item.ObtainLock() == 0)
+ {
+ List clientPacket = new(dataPacket);
+ clientPacket[0] = (byte)(clientPacket[0] << 4 | Players.IndexOf(item));
+ long attemptStart = DateTime.Now.Ticks;
+ int attemptCount = 0;
+ while (attemptCount < 10)
+ {
+ try
+ {
+ x.Key.GetStream().Write(clientPacket.ToArray(), 0, clientPacket.Count);
+ break;
+ }
+ catch (IOException e)
+ {
+ Console.WriteLine(e);
+ _ = Task.Delay(100);
+ attemptCount++;
+ }
+ }
+ // Reset the lock;
+ item.ReleaseLock();
+ }
+ else
+ {
+ // client hasn't recieved old data; avoid sending new state updates
+ Console.WriteLine("Client is still recieving old data");
+ }
+ });
+ }
+ }
+}
diff --git a/RBG_Server.Core/Player.cs b/RBG_Server.Core/Player.cs
new file mode 100644
index 0000000..56052c9
--- /dev/null
+++ b/RBG_Server.Core/Player.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+
+namespace RBG_Server
+{
+ ///
+ /// Data class containing information about a player. Drawn directly to the screen, hence the inheritance
+ /// of Image, which allows this entire object to be
+ ///
+ public class Player : Image
+ {
+ // C# uses implicit field definitions; i.e. declaring a property creates an implicit private field
+ // Note that it is also possible to assign values, and specify access modifiers for the get & set independantly
+ public string PlayerName { get; set; }
+ public int Row { get; set; }
+ public int Column { get; set; }
+ public long LastTime { get; set; } = DateTime.Now.Ticks;
+ public bool Connected { get; private set; }
+ public byte[] UnhandledBuffer { get; set; }
+ private int processing;
+
+ public int ObtainLock()
+ {
+ return System.Threading.Interlocked.CompareExchange(ref processing, 1, 0);
+ }
+
+ public void ReleaseLock()
+ {
+ _ = System.Threading.Interlocked.Exchange(ref processing, 0);
+ }
+
+ // public Image sprite; // Sprite is now set as the implementation of this class
+
+ public Player(string name, int row, int column) : base() // Call the base constructor at the same time
+ {
+ PlayerName = name;
+ Row = row;
+ Column = column;
+ LastTime = DateTime.Now.Ticks;
+ Connected = true;
+ UnhandledBuffer = Array.Empty();
+ }
+
+ public new bool Equals(object obj)
+ {
+ return (obj as Player).GetHashCode() == GetHashCode();
+
+ }
+
+ public new int GetHashCode()
+ {
+ int res = (base.GetHashCode() + PlayerName.GetHashCode()) >> 8; // Right-shift the original hashcode by one byte & use our row & column
+ int rcByte = ((Row & 0xF) << 4) | (Column & 0xF);
+ return (rcByte << 24) | res;
+ }
+ }
+}
diff --git a/RBG_Server.Core/RBG_Server.csproj b/RBG_Server.Core/RBG_Server.csproj
new file mode 100644
index 0000000..76214ae
--- /dev/null
+++ b/RBG_Server.Core/RBG_Server.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net5.0-windows10.0.19041.0
+
+
+
+
+
+
+
diff --git a/Remote Board Game.sln b/Remote Board Game.sln
new file mode 100644
index 0000000..febc632
--- /dev/null
+++ b/Remote Board Game.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31515.178
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RBG Server WPF", "PDGServer_WPF\RBG Server WPF.csproj", "{E6B5F89A-2D77-46CE-8DC2-C2F99AF9B74E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokemon Drinking Game", "Pokemon Drinking Game\Pokemon Drinking Game.csproj", "{AE1C89D8-A6F6-425A-B4C1-AEBECE850198}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RBG_Server", "RBG_Server.Core\RBG_Server.csproj", "{0C203CD4-5139-4CB2-A55D-83405B72DAEF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4B56FDC3-E973-462F-BEEC-BB33A0E3F290}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RBG.Helpers", "RBG.Helpers\RBG.Helpers.csproj", "{B3366717-065E-4025-ABCD-5B8104FE2E0E}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E6B5F89A-2D77-46CE-8DC2-C2F99AF9B74E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6B5F89A-2D77-46CE-8DC2-C2F99AF9B74E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6B5F89A-2D77-46CE-8DC2-C2F99AF9B74E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6B5F89A-2D77-46CE-8DC2-C2F99AF9B74E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE1C89D8-A6F6-425A-B4C1-AEBECE850198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE1C89D8-A6F6-425A-B4C1-AEBECE850198}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE1C89D8-A6F6-425A-B4C1-AEBECE850198}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE1C89D8-A6F6-425A-B4C1-AEBECE850198}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C203CD4-5139-4CB2-A55D-83405B72DAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C203CD4-5139-4CB2-A55D-83405B72DAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C203CD4-5139-4CB2-A55D-83405B72DAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C203CD4-5139-4CB2-A55D-83405B72DAEF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B3366717-065E-4025-ABCD-5B8104FE2E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B3366717-065E-4025-ABCD-5B8104FE2E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B3366717-065E-4025-ABCD-5B8104FE2E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B3366717-065E-4025-ABCD-5B8104FE2E0E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {07E43435-7C22-47A1-B830-5ECFAC6F6CD9}
+ EndGlobalSection
+EndGlobal