Compare commits

..

12 Commits

Author SHA1 Message Date
72275c491a Added build script 2023-01-27 15:20:10 +01:00
64fe3d69dd Added score notification 2023-01-27 15:19:50 +01:00
9c328059fe Input fixed (event path sometimes undefined on some browsers) 2022-11-16 17:22:34 +01:00
531c0e1344 Added username validation
Auto hide enter/leave room
Username starts as UNNAMED now
Show roomnumber for connected room
2022-11-04 15:13:56 +01:00
391b5569a4 Code cleanup 2022-11-04 13:04:32 +01:00
2fb395a8d4 Keep track of score and reset game after score
Client: only set direction on change
2022-11-04 13:00:54 +01:00
cd7a1f15a9 Added continuous reflection on top and lower border 2022-11-04 12:04:05 +01:00
f728240db9 Added crude client rendering with pixi.js
Added keyboard controlling
Added fixed ball reflections on right paddle
added reflections on screen borders
Added goal (miss the paddle) detection
2022-11-04 04:05:20 +01:00
16d001c084 Updated libman and fixed client side libs 2022-11-04 02:00:42 +01:00
f31d18bd0b Blind and likely wrong untested game logic
Added gameloop worker
2022-11-04 01:46:48 +01:00
a4e22a4c52 Added game state, allow paddle movement 2022-11-04 00:13:00 +01:00
462e27c88a Implemented basic Pong lobby system 2022-11-03 12:29:35 +01:00
33 changed files with 31647 additions and 5 deletions

View File

@ -0,0 +1,8 @@
namespace PongGame;
public enum GameStatus {
WaitingForPlayers,
InProgress,
Finished,
Paused,
}

View File

@ -0,0 +1,7 @@
namespace PongGame.Hubs;
public interface IPongClient {
Task GameStateChanged(GameStatus state);
Task ReceiveGameState(PongGameState state);
Task UsernameChanged(string value);
}

View File

@ -0,0 +1,148 @@
using System.Drawing;
using System.Reflection;
using MessagePack;
namespace PongGame.Hubs;
/// <summary>
/// Pong game state saving the positions of the paddles and the ball.
/// The Pong board has aspect ratio 1:2 with height 500 and width 1000.
/// </summary>
public struct PongGameState {
public const float HEIGHT = 500.0f;
public const float WIDTH = 2 * HEIGHT;
public const float PADDLE1_OFFSET = WIDTH / 20;
public const float PADDLE2_OFFSET = WIDTH - PADDLE1_OFFSET;
public static readonly PongGameState Initial = new() {
BallState = PongBallState.Initial,
Paddle1 = PongPaddleState.Initial,
Paddle2 = PongPaddleState.Initial,
Status = GameStatus.WaitingForPlayers
};
public PongPaddleState Paddle1;
public PongPaddleState Paddle2;
public PongBallState BallState;
public GameStatus Status;
public static void Update(ref PongGameState state) {
if (state.Status is not GameStatus.InProgress) return;
PongPaddleState.Update(ref state.Paddle1);
PongPaddleState.Update(ref state.Paddle2);
PongBallState.Update(ref state.BallState);
Collide(in state.Paddle1, ref state.BallState, true);
Collide(in state.Paddle2, ref state.BallState, false);
switch (state.BallState.Pos.X) {
case > WIDTH:
AnnounceScore(ref state, ref state.Paddle1);
return;
case < 0:
AnnounceScore(ref state, ref state.Paddle2);
return;
}
}
private static void AnnounceScore(ref PongGameState state, ref PongPaddleState winner) {
winner.Score++;
state.Paddle1.ResetPosition();
state.Paddle2.ResetPosition();
state.BallState.ResetPosition();
}
private static void Collide(in PongPaddleState paddle, ref PongBallState ballState, bool left) {
var paddleX = left ? PADDLE1_OFFSET : PADDLE2_OFFSET;
var intersection = RectangleF.Intersect(paddle.GetCollider(paddleX), ballState.GetCollider());
if (intersection.IsEmpty) return;
// TODO: continuous collision
var ratio = (ballState.Pos.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH;
// TODO: lesser angles
var upAngle = left ? MathF.PI * 3 / 8 : MathF.PI * 5 / 8;
var downAngle = left ? -MathF.PI * 3 / 8 : MathF.PI * 11 / 8;
// TODO: reflect ball on surface instead of launching
ballState.BallAngle = ratio * downAngle + (1 - ratio) * upAngle;
}
public struct PongBallState {
public const float BALL_SPEED = 2 * BALL_RADIUS;
public const float BALL_RADIUS = HEIGHT / 125;
public static readonly PongBallState Initial = new() {
BallAngle = 0.0f,
Pos = new() {
X = WIDTH / 2,
Y = HEIGHT / 2
}
};
public PointF Pos;
public float BallAngle;
public static void Update(ref PongBallState state) {
var (dy, dx) = MathF.SinCos(state.BallAngle);
state.Pos.X += BALL_SPEED * dx;
state.Pos.Y -= BALL_SPEED * dy;
// If reflected from lower or upper border calculate continuous reflection
// This prevents the ball from temporarily glitching into paddles
if (state.Pos.Y < BALL_RADIUS) {
state.Pos.Y = 2 * BALL_RADIUS - state.Pos.Y;
state.BallAngle = 2 * MathF.PI - state.BallAngle;
} else if (state.Pos.Y > HEIGHT - BALL_RADIUS) {
state.Pos.Y = 2 * (HEIGHT - BALL_RADIUS) - state.Pos.Y;
state.BallAngle = 2 * MathF.PI - state.BallAngle;
}
}
public RectangleF GetCollider() => new(Pos.X - BALL_RADIUS, Pos.Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS);
public void ResetPosition() {
BallAngle = 0.0f;
Pos.X = WIDTH / 2;
Pos.Y = HEIGHT / 2;
}
}
public struct PongPaddleState {
public const float PADDLE_LENGTH = HEIGHT / 6;
public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2;
public const float PADDLE_WIDTH = PADDLE_LENGTH / 5;
public const float PADDLE_SPEED = 8;
private const float PADDLE_INITIAL_HEIGHT = HEIGHT / 2;
public static readonly PongPaddleState Initial = new() {
Score = 0,
Direction = PongPaddleDirection.Stop,
Height = PADDLE_INITIAL_HEIGHT
};
public float Height;
public PongPaddleDirection Direction;
[IgnoreMember] private int _score;
public int Score {
get => _score; set {
if (_score != value) {
_score = value;
_ = Task.Run(() => ScoreChanged.Invoke(this, value));
}
}
}
public event EventHandler<int> ScoreChanged;
public static void Update(ref PongPaddleState state)
=> state.Height = Math.Clamp(state.Height + ((int)state.Direction) * PADDLE_SPEED, PADDLE_HALF_LENGTH, HEIGHT - PADDLE_HALF_LENGTH);
public RectangleF GetCollider(float x) => new(x - PADDLE_WIDTH / 2, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH);
public void ResetPosition() {
Direction = PongPaddleDirection.Stop;
Height = PADDLE_INITIAL_HEIGHT;
}
}
}

68
PongGame/Hubs/PongHub.cs Normal file
View File

@ -0,0 +1,68 @@
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongHub : Hub<IPongClient> {
private const string PLAYER_KEY = "PLAYER";
private readonly PongLobby Lobby;
private readonly ILogger<PongHub> Logger;
public PongHub(PongLobby lobby, ILogger<PongHub> logger) : base() {
Lobby = lobby;
Logger = logger;
}
private PongPlayer Player {
get => (Context.Items.TryGetValue(PLAYER_KEY, out var player) ? player as PongPlayer : null)
?? throw new InvalidProgramException("Player was not assigned at connection start!");
set => Context.Items[PLAYER_KEY] = value;
}
public override Task OnConnectedAsync() {
Player = Lobby.CreatePlayer();
Player.Client = Clients.Client(Context.ConnectionId);
return Task.CompletedTask;
}
private void AssertNotInRoom() {
if (Player.ConnectedRoom is PongRoom currentRoom)
throw new HubException($"User is already connected to room [{currentRoom}]");
}
private PongRoom AssertInRoom() {
if (Player.ConnectedRoom is not PongRoom currentRoom)
throw new HubException($"User is not in any room!");
return currentRoom;
}
public Task<string> CreateRoom() {
AssertNotInRoom();
var room = Lobby.CreateRoom(Player);
return Task.FromResult(room.ID);
}
public async Task<string> JoinRoom(string roomId) {
AssertNotInRoom();
var room = await Lobby.JoinRoom(Player, roomId);
return room.ID;
}
public Task MovePaddle(int dir) {
var room = AssertInRoom();
var direction = (PongPaddleDirection)dir;
if (!Enum.IsDefined(direction))
throw new HubException($"Invalid direction: {dir}!");
return room.MovePaddle(Player, direction);
}
public Task LeaveRoom()
=> Lobby.LeaveRoom(Player);
public Task RequestUsernameChange(string username)
=> Lobby.ChangeUsername(Player, username);
public override Task OnDisconnectedAsync(Exception? exception) {
Lobby.RemovePlayer(Player);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,95 @@
using System.Numerics;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongLobby {
private readonly HashSet<PongPlayer> connectedPlayers = new();
private readonly Dictionary<string, PongRoom> PongRooms = new();
public PongLobby(ILogger<PongLobby> logger)
=> Logger = logger;
public const int ROOM_ID_LENGTH = 4;
public PongPlayer CreatePlayer() {
var player = new PongPlayer();
lock (connectedPlayers)
connectedPlayers.Add(player);
return player;
}
public void RemovePlayer(PongPlayer player) {
if (player.ConnectedRoom is PongRoom room)
room.Leave(player);
lock (connectedPlayers)
_ = connectedPlayers.Remove(player);
}
public PongRoom CreateRoom(PongPlayer player) {
PongRoom room;
lock (PongRooms) {
room = new(GenerateRoomId(), Logger);
PongRooms.Add(room.ID, room);
}
room.Join(player);
return room;
}
public Task<PongRoom> JoinRoom(PongPlayer player, string roomId) {
PongRoom? room;
lock (PongRooms) {
room = PongRooms.GetValueOrDefault(roomId);
}
if (room is null) throw new HubException($"Room [{roomId}] not found!");
room.Join(player);
return Task.FromResult(room);
}
public Task LeaveRoom(PongPlayer player) {
if (player.ConnectedRoom is PongRoom room) {
room.Leave(player);
if (room.IsEmpty)
lock (PongRooms)
_ = PongRooms.Remove(room.ID);
}
return Task.CompletedTask;
}
private readonly Random random = new();
private readonly ILogger<PongLobby> Logger;
private const string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private string GenerateRoomId() {
string id;
do {
id = string.Concat(Enumerable.Range(0, ROOM_ID_LENGTH).Select(_ => ALPHABET[random.Next(ALPHABET.Length)]));
} while (PongRooms.ContainsKey(id));
return id;
}
public async Task ChangeUsername(PongPlayer player, string username) {
username = ValidateUsername(username);
if (player.Username == username) {
await player.Client.UsernameChanged(username);
return;
}
lock (connectedPlayers) {
// TODO: separate hashset for usernames
if (connectedPlayers.Select(i => i.Username).Contains(username))
throw new HubException($"Username {username} is already taken!");
}
Logger.LogInformation("Player {Player} requested username change to [{username}]", player, username);
player.Username = username;
}
private static readonly Regex UsernameRegex = new(@"^(?!.*[._ -]{2})[\w._ -]{3,20}$", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200));
private static string ValidateUsername(string username) {
username = username.Trim();
if (!UsernameRegex.IsMatch(username))
throw new HubException($"At most 20 characters, no two consecutive symbols");
return username;
}
}

View File

@ -0,0 +1,7 @@
namespace PongGame;
public enum PongPaddleDirection {
Up = -1,
Stop,
Down,
}

View File

@ -0,0 +1,22 @@
using System.Diagnostics;
namespace PongGame.Hubs;
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class PongPlayer {
private string username = "UNNAMED";
public PongRoom? ConnectedRoom { get; internal set; }
public string Username {
get => username;
internal set {
if (username != value) {
username = value;
Task.Run(() => Client.UsernameChanged(value));
}
}
}
public IPongClient Client { get; internal set; } = default!;
public override string ToString() => $"[{Username}]";
}

100
PongGame/Hubs/PongRoom.cs Normal file
View File

@ -0,0 +1,100 @@
using System.ComponentModel;
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongRoom {
public string ID { get; }
private readonly ILogger Logger;
private const string JOIN_LOG_TEMPLATE = "[{ID}] {Player} joined pong room as player {Number}!";
private const string LEAVE_LOG_TEMPLATE = "[{ID}] Player {Number} {Player} left pong room!";
private const string DIRECTION_LOG_TEMPLATE = "[{ID}] Player {Number} {player} moves paddle in direction: {direction}!";
public PongRoom(string id, ILogger logger) {
ID = id;
Logger = logger;
gameWorker.DoWork += GameLoop;
}
public PongPlayer? Player1 { get; private set; }
public PongPlayer? Player2 { get; private set; }
public bool IsEmpty => Player1 is null && Player2 is null;
private PongGameState State = PongGameState.Initial;
public void Join(PongPlayer player) {
// TODO: synchronize this
if (Player1 is null) {
Player1 = player;
Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1);
} else if (Player2 is null) {
Player2 = player;
Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1);
} else
throw new HubException($"Lobby [{ID}] is already full!");
_ = Task.Run(PlayersChanged);
player.ConnectedRoom = this;
}
public void Leave(PongPlayer player) {
if (Player1 == player) {
Player1 = null;
Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 1, player);
} else if (Player2 == player) {
Player2 = null;
Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 2, player);
}
player.ConnectedRoom = null;
_ = Task.Run(PlayersChanged);
}
private readonly BackgroundWorker gameWorker = new() {
WorkerSupportsCancellation = true
};
private void GameLoop(object? sender, DoWorkEventArgs e) {
while (!gameWorker.CancellationPending
&& Player1 is PongPlayer p1
&& Player2 is PongPlayer p2
&& State.Status is GameStatus.InProgress) {
PongGameState.Update(ref State);
_ = Task.Run(() => Task.WhenAll(
p1.Client.ReceiveGameState(State),
p2.Client.ReceiveGameState(State)));
Thread.Sleep(1000 / 60);
}
}
private Task PlayersChanged() {
if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2);
ResumeGame();
} else if (IsEmpty) {
CloseRoom();
} else
PauseGame();
return Task.CompletedTask;
}
private void ResumeGame() {
State.Status = GameStatus.InProgress;
if (!gameWorker.IsBusy) gameWorker.RunWorkerAsync();
}
private void PauseGame() => State.Status = GameStatus.Paused;
private void CloseRoom() => State.Status = GameStatus.Finished;
public override string ToString() => $"[{ID}]";
public Task MovePaddle(PongPlayer player, PongPaddleDirection direction) {
if (Player1 == player) {
State.Paddle1.Direction = direction;
Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction);
} else if (Player2 == player) {
State.Paddle2.Direction = direction;
Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction);
} else
throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted.");
return Task.CompletedTask;
}
}

View File

@ -6,4 +6,5 @@
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Welcome</h1> <h1 class="display-4">Welcome</h1>
<a asp-area="" asp-page="/Pong">Pong</a>
</div> </div>

View File

@ -9,7 +9,7 @@ public class IndexModel : PageModel {
_logger = logger; _logger = logger;
} }
public void OnGet() { public IActionResult OnGet() {
return RedirectToPage("Pong");
} }
} }

View File

@ -0,0 +1,34 @@
@page
@model PongModel
@{
ViewData["Title"] = "Pong";
}
<div class="text-center">
<h1 class="display-4">Pong</h1>
<h3 id="connection">Connection Status</h3>
<button id="createlobby" class="btn btn-primary mb-3">Create Room</button>
<button id="leavelobby" class="btn btn-primary mb-3 d-none">Leave Room</button>
<div id="joinroomdiv" class="input-group mb-3">
<input id="roomid" type="text" class="form-control" placeholder="Room ID" aria-label="Room ID" aria-describedby="joinroom">
<button id="joinroom" class="btn btn-outline-secondary" type="button">Join Room</button>
</div>
<div class="input-group mb-3 has-validation">
<input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername">
<button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button>
<div id="usernameerror" class="invalid-feedback"></div>
</div>
<div>
<h4>Room <span id="connectedroomid"></span></h4>
<div id="canvas-container" class="mb-3"></div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
<script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script>
<script src="~/lib/pixi/dist/pixi.min.js"></script>
<script src="~/js/pong.js"></script>

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PongGame.Pages;
public class PongModel : PageModel {
private readonly ILogger<PongModel> _logger;
public PongModel(ILogger<PongModel> logger) {
_logger = logger;
}
public void OnGet() {
}
}

View File

@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PongGame.Pages; namespace PongGame.Pages;
public class PrivacyModel : PageModel { public class PrivacyModel : PageModel {

View File

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,7 +1,13 @@
using PongGame.Hubs;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddSingleton<PongLobby>(services
=> new(services.GetRequiredService<ILogger<PongLobby>>()));
builder.Services.AddSignalR()
.AddMessagePackProtocol();
var app = builder.Build(); var app = builder.Build();
@ -15,6 +21,9 @@ app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
app.MapRazorPages(); app.UseEndpoints(endpoints => {
endpoints.MapRazorPages();
endpoints.MapHub<PongHub>("/pong/hub");
});
app.Run(); app.Run();

53
PongGame/build.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
from argparse import ArgumentParser
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Version:
major: int = 1
minor: int = 0
patch: int = 0
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
def __init__(self, version_str: str):
try:
match list(map(int, version_str.split("."))):
case [major, minor, patch]:
self.major, self.minor, self.patch = major, minor, patch
return
case _:
raise ValueError("Version file must contain 3 parts (SemVer)!")
except ValueError as e:
raise ValueError(f"Version must contain numbers only!") from e
def parse_args():
parser = ArgumentParser("build.py", description="Docker multi-arch build helper.")
version_group = parser.add_mutually_exclusive_group()
version_group.add_argument("--major", "-m", action="store_true", help="Increase the major version before building.")
version_group.add_argument("--minor", "-n", action="store_true", help="Increase the minor version before building.")
version_group.add_argument("--patch", "-p", action="store_true", help="Increase the patch version before building.")
parser.add_argument("--file", "-f", type=str, default="version.txt", help="File to store the current version in.")
return parser.parse_args()
def save_version(version_file: Path, version: Version):
with version_file.open("w", encoding="utf-8") as f:
f.write(str(version))
def load_version(version_file: Path):
try:
with version_file.open("r", encoding="utf-8") as f:
return Version(f.read().strip())
except FileNotFoundError:
return Version()
def main():
args = parse_args()
version_file = Path(args.file)
version = load_version(version_file)
save_version(version_file, version)
if __name__ == '__main__':
main()

37
PongGame/libman.json Normal file
View File

@ -0,0 +1,37 @@
{
"version": "1.0",
"defaultProvider": "jsdelivr",
"libraries": [
{
"library": "@microsoft/signalr-protocol-msgpack@6.0.10",
"destination": "wwwroot/lib/signalr/",
"files": [
"dist/browser/signalr-protocol-msgpack.js",
"dist/browser/signalr-protocol-msgpack.js.map",
"dist/browser/signalr-protocol-msgpack.min.js",
"dist/browser/signalr-protocol-msgpack.min.js.map"
]
},
{
"library": "@microsoft/signalr@6.0.10",
"destination": "wwwroot/lib/signalr/",
"files": [
"dist/browser/signalr.js",
"dist/browser/signalr.js.map",
"dist/browser/signalr.min.js",
"dist/browser/signalr.min.js.map"
]
},
{
"provider": "jsdelivr",
"library": "pixi.js@7.0.2",
"destination": "wwwroot/lib/pixi/",
"files": [
"dist/pixi.js",
"dist/pixi.js.map",
"dist/pixi.min.js.map",
"dist/pixi.min.js"
]
}
]
}

1
PongGame/version.txt Normal file
View File

@ -0,0 +1 @@
1.0.0

207
PongGame/wwwroot/js/pong.js Normal file
View File

@ -0,0 +1,207 @@
"use strict";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/pong/hub")
.withAutomaticReconnect()
.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
.build();
function getElement(id) {
return document.getElementById(id) ?? console.error(`Element #${id} not found!`)
}
const connectionStatus = getElement("connection");
const createroom = getElement("createlobby");
const roomidinput = getElement("roomid");
const joinroom = getElement("joinroom");
const joinroomdiv = getElement("joinroomdiv");
const usernameinput = getElement("username");
const setusername = getElement("setusername");
const leaveroom = getElement("leavelobby");
const usernameerror = getElement("usernameerror");
const connectedroomid = getElement("connectedroomid");
connection.onclose(function (err) {
if (err) {
connectionStatus.textContent = "Unexpected error!";
return console.error(`Connection aborted: ${err.message}`);
}
console.info("Disconnected!");
connectionStatus.textContent = "Closed!";
});
connection.onreconnecting(function (err) {
if (err) {
connectionStatus.textContent = "Reconnecting!";
return console.error(`Connection reconnecting: ${err.message}`);
}
console.info("Reconnecting!");
connectionStatus.textContent = "Reconnecting!";
});
connection.onreconnected(function (connectionId) {
console.info(`Connected as ${connectionId}!`);
connectionStatus.textContent = "Connected!";
});
function show(elem) { elem.classList.remove("d-none"); }
function hide(elem) { elem.classList.add("d-none"); }
function roomJoined(roomId) {
roomidinput.value = roomId;
connectedroomid.textContent = roomId;
console.info(`Joined room [${roomId}]`);
hide(createroom);
hide(joinroomdiv);
show(leaveroom);
}
createroom.addEventListener("click", function (event) {
connection.invoke("CreateRoom").then(roomJoined).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
connection.on("GameStateChanged", function (state) {
console.info(`Game is now in state ${state}`);
});
connection.on("UsernameChanged", function (username) {
console.info(`Username is now ${username}`);
usernameinput.value = username;
usernameinput.classList.remove("is-invalid");
usernameinput.classList.add("is-valid");
});
joinroom.addEventListener("click", function (event) {
connection.invoke("JoinRoom", roomidinput.value).then(roomJoined).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
function hubExceptionMessage(msg) {
const needle = "HubException: ";
let idx = msg.lastIndexOf(needle);
if (idx < 0) return msg;
return msg.substr(idx + needle.length);
}
usernameinput.addEventListener("input", function (event) {
usernameinput.classList.remove("is-valid");
});
setusername.addEventListener("click", function (event) {
connection.invoke("RequestUsernameChange", usernameinput.value).catch(function (err) {
usernameerror.textContent = hubExceptionMessage(err.message);
usernameinput.classList.add("is-invalid");
return console.error(err.toString());
});
event.preventDefault();
});
leaveroom.addEventListener("click", function (event) {
connection.invoke("LeaveRoom").then(function () {
roomidinput.value = "";
show(createroom);
show(joinroomdiv);
hide(leaveroom);
}).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
function movePaddle(direction) {
connection.invoke("MovePaddle", direction).catch(function (err) {
return console.error(err.toString());
});
}
// Create the application helper and add its render target to the page
let app = new PIXI.Application({ width: 1000, height: 500 });
getElement("canvas-container").appendChild(app.view);
let graphics = new PIXI.Graphics();
app.stage.addChild(graphics);
function renderPaddle(graphics, state, xMid) {
var xLeft = xMid - 5;
graphics.beginFill(0x00FFFF);
graphics.drawRect(xLeft, state.Height - 25, 10, 50);
graphics.endFill();
}
function renderBall(graphics, state) {
graphics.beginFill(0xFF00FF);
graphics.drawCircle(state.Pos.X, state.Pos.Y, 4);
graphics.endFill();
}
function renderGameState(graphics, state) {
graphics.clear();
renderPaddle(graphics, state.Paddle1, 50);
renderPaddle(graphics, state.Paddle2, 1000 - 50);
renderBall(graphics, state.BallState);
}
connection.on("ReceiveGameState", function (state) {
renderGameState(graphics, state);
});
const keyEvent = (function () {
var upPressed = false;
var downPressed = false;
var direction = 0;
function setDirection(dir) {
if (direction != dir) {
direction = dir;
movePaddle(direction);
}
}
function moveUpdated() {
if (upPressed == downPressed) setDirection(0);
else if (upPressed) setDirection(-1);
else if (downPressed) setDirection(1);
else console.error("unknown move!");
}
function handler(event) {
var pressed = event.type == "keydown";
// W Key is 87, Up arrow is 87
// S Key is 83, Down arrow is 40
switch (event.keyCode) {
case 87:
case 38:
upPressed = pressed;
break;
case 83:
case 40:
downPressed = pressed;
break;
default: return;
}
if (event.repeat) return;
if (event.target.tagName == 'INPUT') return; // dont use key if on input
event.preventDefault();
moveUpdated();
}
return handler;
})();
document.addEventListener("keydown", keyEvent);
document.addEventListener("keyup", keyEvent);
connection.start().then(function () {
console.info(`Connected!`);
connectionStatus.textContent = "Connected!";
}).catch(function (err) {
connectionStatus.textContent = "Connection failed!";
return console.error(err.toString());
});

24183
PongGame/wwwroot/lib/pixi/dist/pixi.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1116
PongGame/wwwroot/lib/pixi/dist/pixi.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

324
package-lock.json generated Normal file
View File

@ -0,0 +1,324 @@
{
"name": "PongGame",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"@microsoft/signalr": "^6.0.10",
"@microsoft/signalr-protocol-msgpack": "^6.0.10"
}
},
"node_modules/@microsoft/signalr": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.10.tgz",
"integrity": "sha512-ND9LiIYac+ZDgCgW2QzpNfe9BTiOtjc2AX/2GtFIhRGhEzx5CixcNANg2VGj27IAxycAPPnEoy7+QA31Eil7QQ==",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^1.0.7",
"fetch-cookie": "^0.11.0",
"node-fetch": "^2.6.7",
"ws": "^7.4.5"
}
},
"node_modules/@microsoft/signalr-protocol-msgpack": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-6.0.10.tgz",
"integrity": "sha512-Xj3QuH/HMcLGtc+iZjE8BH/auQVn4FQY9adv7M/wsMpT9TEe1iQJKjD0Hlh+Y42ioNaO0MFVlfFXhkYUK7y5QQ==",
"dependencies": {
"@microsoft/signalr": ">=6.0.10",
"@msgpack/msgpack": "^2.7.0"
}
},
"node_modules/@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz",
"integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/fetch-cookie": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz",
"integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==",
"dependencies": {
"tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"engines": {
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@microsoft/signalr": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.10.tgz",
"integrity": "sha512-ND9LiIYac+ZDgCgW2QzpNfe9BTiOtjc2AX/2GtFIhRGhEzx5CixcNANg2VGj27IAxycAPPnEoy7+QA31Eil7QQ==",
"requires": {
"abort-controller": "^3.0.0",
"eventsource": "^1.0.7",
"fetch-cookie": "^0.11.0",
"node-fetch": "^2.6.7",
"ws": "^7.4.5"
}
},
"@microsoft/signalr-protocol-msgpack": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-6.0.10.tgz",
"integrity": "sha512-Xj3QuH/HMcLGtc+iZjE8BH/auQVn4FQY9adv7M/wsMpT9TEe1iQJKjD0Hlh+Y42ioNaO0MFVlfFXhkYUK7y5QQ==",
"requires": {
"@microsoft/signalr": ">=6.0.10",
"@msgpack/msgpack": "^2.7.0"
}
},
"@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ=="
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "^5.0.0"
}
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"eventsource": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz",
"integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA=="
},
"fetch-cookie": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz",
"integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==",
"requires": {
"tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0"
}
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
},
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
}
}
}

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"@microsoft/signalr": "^6.0.10",
"@microsoft/signalr-protocol-msgpack": "^6.0.10"
}
}