Projektdateien hinzufügen.

This commit is contained in:
Kevin Kandlbinder 2024-06-21 13:11:34 +02:00
parent 72ecb03642
commit 76fbb232a7
48 changed files with 3779 additions and 0 deletions

30
.dockerignore Normal file
View file

@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

62
BusinessLogic/BaseData.cs Normal file
View file

@ -0,0 +1,62 @@

namespace WebEMSim
{
internal class BaseData
{
private static Team[] _teams = [
new Team("France", 2, "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Flag_of_France.svg/320px-Flag_of_France.svg.png"),
new Team("Belgium", 3, "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Belgium.svg/277px-Flag_of_Belgium.svg.png"),
new Team("England", 4, "https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Flag_of_England.svg/320px-Flag_of_England.svg.png"),
new Team("Portugal", 6, "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_Portugal.svg/320px-Flag_of_Portugal.svg.png"),
new Team("Netherlands", 7, "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/320px-Flag_of_the_Netherlands.svg.png"),
new Team("Spain", 8, "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Flag_of_Spain.svg/320px-Flag_of_Spain.svg.png"),
new Team("Italy", 9, "https://upload.wikimedia.org/wikipedia/commons/thumb/0/03/Flag_of_Italy.svg/320px-Flag_of_Italy.svg.png"),
new Team("Croatia", 10, "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Flag_of_Croatia.svg/320px-Flag_of_Croatia.svg.png"),
new Team("Germany", 16, "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/320px-Flag_of_Germany.svg.png"),
new Team("Switzerland", 19, "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Flag_of_Switzerland.svg/240px-Flag_of_Switzerland.svg.png"),
new Team("Denmark", 21, "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Flag_of_Denmark.svg/318px-Flag_of_Denmark.svg.png"),
new Team("Ukraine", 22, "https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Flag_of_Ukraine.svg/320px-Flag_of_Ukraine.svg.png"),
new Team("Austria", 25, "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Flag_of_Austria.svg/320px-Flag_of_Austria.svg.png"),
new Team("Hungary", 26, "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Flag_of_Hungary.svg/320px-Flag_of_Hungary.svg.png"),
new Team("Poland", 28, "https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/Flag_of_Poland.svg/320px-Flag_of_Poland.svg.png"),
new Team("Serbia", 33, "https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Flag_of_Serbia.svg/320px-Flag_of_Serbia.svg.png"),
new Team("Czechia", 36, "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_Czech_Republic.svg/320px-Flag_of_the_Czech_Republic.svg.png"),
new Team("Scotland", 39, "https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Flag_of_Scotland.svg/320px-Flag_of_Scotland.svg.png"),
new Team("Türkiye", 40, "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Flag_of_Turkey.svg/320px-Flag_of_Turkey.svg.png"),
new Team("Romania", 46, "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Flag_of_Romania.svg/320px-Flag_of_Romania.svg.png"),
new Team("Slovakia", 48, "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Flag_of_Slovakia.svg/320px-Flag_of_Slovakia.svg.png"),
new Team("Slovenia", 57, "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Flag_of_Slovenia.svg/320px-Flag_of_Slovenia.svg.png"),
new Team("Albania", 66, "https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Flag_of_Albania.svg/320px-Flag_of_Albania.svg.png"),
new Team("Georgia", 75, "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Flag_of_Georgia.svg/320px-Flag_of_Georgia.svg.png"),
];
public static Team[] Teams { get => _teams.Select(team => new Team(team.Name, team.WorldRank, team.FlagUrl)).ToArray(); }
public static List<List<Team>> GetBuckets()
{
if (Teams.Length != 24)
{
throw new Exception("Invalid BaseData - Team count off.");
}
List<List<Team>> buckets = [];
int teamIterator = 0;
for (int bucketIdx = 0; bucketIdx < 4; bucketIdx++)
{
List<Team> bucket = [];
for (int bucketTeamIdx = 0; bucketTeamIdx < 6; bucketTeamIdx++)
{
bucket.Add(Teams[teamIterator]);
teamIterator++;
}
buckets.Add(bucket);
}
return buckets;
}
}
}

43
BusinessLogic/Group.cs Normal file
View file

@ -0,0 +1,43 @@

namespace WebEMSim
{
public class Group
{
private int _number;
private List<Team> _teams = [];
private Table _table;
public Group(int number)
{
_number = number;
_table = new Table(_teams);
}
public void AddTeam(Team team)
{
team.AssignGroup(_number);
_teams.Add(team);
_table.AddTeam(team);
}
public void PerformGroupPlayout()
{
List<Match> matches = Helpers.GenerateMatches(_teams);
foreach (Match match in matches)
{
match.Play();
}
}
public override string ToString()
{
return String.Format("Group {0} { {1} }", _number, _teams.ToArray().ToString());
}
public Table Table { get => _table; }
public char Letter { get => Helpers.NumberToLetter(_number); }
}
}

41
BusinessLogic/Helpers.cs Normal file
View file

@ -0,0 +1,41 @@

namespace WebEMSim
{
internal class Helpers
{
public static T GetRandomFromList<T>(List<T> list)
{
return list[Random.Shared.Next(list.Count)];
}
public static T GetRandomFromListAndRemove<T>(List<T> list)
{
T chosenOne = GetRandomFromList(list);
list.Remove(chosenOne);
return chosenOne;
}
public static List<Match> GenerateMatches(List<Team> teams)
{
List<Match> matches = [];
for (int teamAIdx = 0; teamAIdx < teams.Count - 1; teamAIdx++)
{
for (int teamBIdx = teamAIdx + 1; teamBIdx < teams.Count; teamBIdx++)
{
matches.Add(new Match(teams[teamAIdx], teams[teamBIdx]));
}
}
return matches.OrderBy(x => Random.Shared.Next()).ToList(); // why x ??
}
public static char NumberToLetter(int number)
{
return (char)(number+0x41);
}
}
}

101
BusinessLogic/Match.cs Normal file
View file

@ -0,0 +1,101 @@

namespace WebEMSim
{
public class Match
{
public enum MatchResult
{
TEAM_A_WIN = 0, TEAM_B_WIN = 1, DRAW = 2
}
private Team[] _teams = new Team[2];
private int[] _goals = new int[2];
private MatchResult _result;
public Match(Team team1, Team team2)
{
_teams[0] = team1;
_teams[1] = team2;
}
private void CommitStats()
{
for (int teamIdx = 0; teamIdx < _teams.Length; teamIdx++)
{
int goalsFor = _goals[teamIdx];
int goalsAgainst = _goals[1 - teamIdx];
int points = 0;
if(_result == (MatchResult)teamIdx) //vergleiche enum
{
points = 3;
}
if(_result == MatchResult.DRAW)
{
points = 1;
}
_teams[teamIdx].LogStats(points, goalsFor, goalsAgainst);
}
}
private void CalculateResult()
{
if (_goals[0] > _goals[1])
{
_result = MatchResult.TEAM_A_WIN;
return;
}
if (_goals[1] > _goals[0])
{
_result = MatchResult.TEAM_B_WIN;
return;
}
_result = MatchResult.DRAW;
}
public void Play()
{
for (int goalsIdx = 0; goalsIdx < _goals.Length; goalsIdx++)
{
_goals[goalsIdx] = Random.Shared.Next(6);
}
CalculateResult();
CommitStats();
}
private Team GetWinner()
{
if (_result == Match.MatchResult.TEAM_A_WIN)
{
return _teams[0];
}
if (_result == Match.MatchResult.TEAM_B_WIN)
{
return _teams[1];
}
Table drawResolver = new([_teams[0], _teams[1]]);
return drawResolver.GetTable()[0];
}
public MatchResult Result { get => _result; }
public Team Winner { get => GetWinner(); }
public Team TeamA { get => _teams[0]; }
public Team TeamB { get => _teams[1]; }
public int GoalsA { get => _goals[0]; }
public int GoalsB { get => _goals[1]; }
}
}

96
BusinessLogic/Table.cs Normal file
View file

@ -0,0 +1,96 @@

namespace WebEMSim
{
public class Table
{
private List<Team> _teams;
private Sorter _sorter = new Sorter();
public Dictionary<int, Team> Rankings { get => GetTable().Select((team, index) => new {team, index}).ToDictionary(x => x.index, x => x.team); }
public Table(List<Team> teams) {
_teams = new(teams);
}
public void AddTeam(Team team)
{
_teams.Add(team);
}
public List<Team> GetTable()
{
_teams.Sort(_sorter);
return _teams;
}
public List<Team> GetTruncatedTable(int cutoff)
{
List<Team> sortedTeams = new(GetTable());
sortedTeams.RemoveRange(cutoff, sortedTeams.Count - cutoff);
return sortedTeams;
}
/*public DataTable AsDataTable()
{
DataTable dataTable = new DataTable();
dataTable.Clear();
dataTable.Columns.Add("Name");
dataTable.Columns.Add("Wins");
dataTable.Columns.Add("Losses");
dataTable.Columns.Add("Draws");
dataTable.Columns.Add("For");
dataTable.Columns.Add("Against");
dataTable.Columns.Add("Goal Diff");
dataTable.Columns.Add("World Rank");
dataTable.Columns.Add("Points");
dataTable.Columns.Add("Group");
List<Team> teams = GetTable();
foreach (Team team in teams)
{
dataTable.Rows.Add([
team.Name,
team.Wins,
team.Losses,
team.Draws,
team.For,
team.Against,
team.GoalDelta,
team.WorldRank,
team.Points,
Helpers.NumberToLetter(team.Group)
]);
}
return dataTable;
}*/
private class Sorter : IComparer<Team>
{
public int Compare(Team? a, Team? b)
{
int rank = b.Points - a.Points;
if(rank == 0)
{
rank = b.GoalDelta - a.GoalDelta;
}
if(rank == 0)
{
rank = b.For - a.For;
}
return rank;
}
}
}
}

55
BusinessLogic/Team.cs Normal file
View file

@ -0,0 +1,55 @@

namespace WebEMSim
{
public class Team
{
private string _name;
private string _flagUrl;
private int _wins = 0;
private int _losses = 0;
private int _draws = 0;
private int _worldRank = 0;
private int _for = 0;
private int _against = 0;
private int _points = 0;
private int _group = -1;
public Team(string name, int worldRank, string flagUrl)
{
_name = name;
_worldRank = worldRank;
_flagUrl = flagUrl;
}
public void AssignGroup(int group)
{
_group = group;
}
public void LogStats(int points, int goalsFor, int goalsAgainst)
{
_points += points;
if (points == 3) { _wins += 1; }
if (points == 1) { _draws += 1; }
if (points == 0) { _losses += 1; }
_for += goalsFor;
_against += goalsAgainst;
}
public int For { get => _for; }
public int Points { get => _points; }
public int GoalDelta { get => _for - _against; }
public int Group { get => _group; }
public string Name { get => _name; }
public string FlagUrl { get => _flagUrl; }
public int Wins { get => _wins; }
public int Losses { get => _losses; }
public int Against { get => _against; }
public int Draws { get => _draws; }
public int WorldRank { get => _worldRank; }
}
}

222
BusinessLogic/Tournament.cs Normal file
View file

@ -0,0 +1,222 @@

namespace WebEMSim
{
public class Tournament
{
private readonly string _id;
private List<Group> _groups;
private Dictionary<string, Team> _ranks;
private Dictionary<string, Match> _matches;
private Table _overallTable = new([]);
private bool _hasPlayed = false;
public Tournament()
{
_id = System.Guid.NewGuid().ToString();
_ranks = new Dictionary<string, Team>();
_matches = new Dictionary<string, Match>();
_groups = [];
GenerateGroups();
}
public string ID { get => _id; }
public bool HasPlayed { get => _hasPlayed; }
public Dictionary<string, Match> Matches { get => _matches; }
private void GenerateGroups()
{
List<List<Team>> buckets = BaseData.GetBuckets();
for (int i = 0; i < 6; i++)
{
Group group = new(i);
foreach (var bucket in buckets)
{
Team chosenTeam = Helpers.GetRandomFromListAndRemove(bucket);
group.AddTeam(chosenTeam);
_overallTable.AddTeam(chosenTeam);
}
_groups.Add(group);
}
}
private string MakeRankString(char letter, int rank)
{
return String.Format("{0}{1}", rank, letter);
}
private void PerformGroupPhase(Table thirdPlaceTable)
{
foreach (var group in _groups)
{
group.PerformGroupPlayout();
List<Team> rankedTeams = group.Table.GetTable();
for (int rank = 0; rank < 2; rank++)
{
_ranks.Add(MakeRankString(group.Letter, rank + 1), rankedTeams[rank]);
}
thirdPlaceTable.AddTeam(rankedTeams[2]);
}
}
private void ChooseThirdPlaces(Table thirdPlaceTable)
{
List<Team> teams = thirdPlaceTable.GetTruncatedTable(4); // TODO: Check this
if (teams.Count != 4) throw new Exception("FUCK");
while(true)
{
teams = teams.OrderBy(x => Random.Shared.Next()).ToList();
int matchB = teams[0].Group;
int matchC = teams[1].Group;
int matchE = teams[2].Group;
int matchF = teams[3].Group;
if (!new List<int> { 0, 3, 4, 5 /* ADEF */ }.Contains(matchB))
{
continue;
}
if (!new List<int> { 3, 4, 5 /* DEF */ }.Contains(matchC))
{
continue;
}
if (!new List<int> { 0, 1, 2, 3 /* ABCD */ }.Contains(matchE))
{
continue;
}
if (!new List<int> { 0, 1, 2 /* ABC */ }.Contains(matchF))
{
continue;
}
break;
}
_ranks.Add("3ADEF", teams[0]);
_ranks.Add("3DEF", teams[1]);
_ranks.Add("3ABCD", teams[2]);
_ranks.Add("3ABC", teams[3]);
}
private Match DoMatch(string teamA, string teamB)
{
Team teamAObj = _ranks[teamA];
Team teamBObj = _ranks[teamB];
Match match = new(teamAObj, teamBObj);
match.Play();
return match;
}
public record PlayoutMatch(string teamA, string teamB, string result);
private void PlayoutGeneric(List<PlayoutMatch> playoutMatches)
{
foreach (var playoutMatch in playoutMatches)
{
Match match = DoMatch(playoutMatch.teamA, playoutMatch.teamB);
Team winner = match.Winner;
_ranks.Add(playoutMatch.result, winner);
_matches.Add(playoutMatch.result, match);
}
}
public List<PlayoutMatch> Playout16Matches = [
new PlayoutMatch("1B", "3ADEF", "W39"),
new PlayoutMatch("1A", "2C", "W37"),
new PlayoutMatch("1F", "3ABC", "W41"),
new PlayoutMatch("2D", "2E", "W42"),
new PlayoutMatch("1E", "3ABCD", "W43"),
new PlayoutMatch("1D", "2F", "W44"),
new PlayoutMatch("1C", "3DEF", "W40"),
new PlayoutMatch("2A", "2B", "W38"),
];
private void Playout16()
{
PlayoutGeneric(Playout16Matches);
}
public List<PlayoutMatch> Playout4Matches = [
new PlayoutMatch("W39", "W37", "W45"),
new PlayoutMatch("W41", "W42", "W46"),
new PlayoutMatch("W43", "W44", "W47"),
new PlayoutMatch("W40", "W38", "W48"),
];
private void Playout4()
{
PlayoutGeneric(Playout4Matches);
}
public List<PlayoutMatch> Playout2Matches = [
new PlayoutMatch("W45", "W46", "W49"),
new PlayoutMatch("W47", "W48", "W50"),
];
private void Playout2()
{
PlayoutGeneric(Playout2Matches);
}
public List<PlayoutMatch> PlayoutFinalMatches = [
new PlayoutMatch("W49", "W50", "WIN"),
];
private void PlayoutFinal()
{
PlayoutGeneric(PlayoutFinalMatches);
}
public Team GetTeamForRank(string rank)
{
return _ranks[rank];
}
public Match GetMatchForRank(string rank)
{
return _matches[rank];
}
public void PlayoutTournament()
{
Table thirdPlaceTable = new([]);
_hasPlayed = true;
PerformGroupPhase(thirdPlaceTable);
ChooseThirdPlaces(thirdPlaceTable);
Playout16();
Playout4();
Playout2();
PlayoutFinal();
}
public List<Group> Groups { get => _groups; }
public Table OverallTable { get => _overallTable; }
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
namespace WebEMSim.Controllers
{
[ApiController]
[Route("error")]
public class ErrorController : ControllerBase
{
[HttpGet("{statusCode}", Name = "HandleError")]
public IActionResult Handle(int statusCode)
{
if (statusCode == 404)
{
var fallbackFile = Path.Combine(Environment.CurrentDirectory, "webem-ui/build/404.html");
return PhysicalFile(fallbackFile, "text/html");
}
return StatusCode(statusCode);
}
}
}

View file

@ -0,0 +1,78 @@
using Microsoft.AspNetCore.Mvc;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace WebEMSim.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TournamentController : ControllerBase
{
// GET: api/<TournamentController>
[HttpGet]
public IEnumerable<string> Get()
{
return Storage.Shared.ListTournaments();
}
// GET api/<TournamentController>/5
[HttpGet("{id}")]
public IActionResult Get(string id)
{
Tournament? tournament = Storage.Shared.GetTournament(id);
if(tournament == null)
return NotFound();
return Ok(tournament);
}
// POST api/<TournamentController>
[HttpPost]
public IActionResult Post(/*[FromBody] string value*/)
{
Tournament? newTournament = Storage.Shared.CreateTournament();
if(newTournament == null)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
newTournament.PlayoutTournament(); // TODO: Remove this
return Ok(newTournament);
}
// POST api/<TournamentController>/{id}/play
[HttpPost("{id}/play")]
public IActionResult Post(string id)
{
Tournament? tournament = Storage.Shared.GetTournament(id);
if (tournament == null)
return NotFound();
if (tournament.HasPlayed == true)
return BadRequest();
tournament.PlayoutTournament();
return Ok(tournament);
}
// DELETE api/<TournamentController>/5
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
bool? success = Storage.Shared.DeleteTournament(id);
if (success == null)
return NotFound();
if(success != true)
return StatusCode(StatusCodes.Status500InternalServerError);
return Ok();
}
}
}

25
Dockerfile Normal file
View file

@ -0,0 +1,25 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebEMSim.csproj", "."]
RUN dotnet restore "./WebEMSim.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./WebEMSim.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebEMSim.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
COPY ./webem-ui/build /app/publish/webem-ui/build
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebEMSim.dll"]

52
Program.cs Normal file
View file

@ -0,0 +1,52 @@
using Microsoft.Extensions.FileProviders;
namespace WebEMSim
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}
app.UseAuthorization();
var uiFileProvider = new PhysicalFileProvider(
Path.Combine(Environment.CurrentDirectory, "webem-ui/build"));
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseDefaultFiles(new DefaultFilesOptions
{
FileProvider = uiFileProvider
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = uiFileProvider,
RequestPath = ""
});
app.MapControllers();
app.Run();
}
}
}

View file

@ -0,0 +1,40 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5295"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:38064",
"sslPort": 0
}
}
}

50
Storage.cs Normal file
View file

@ -0,0 +1,50 @@

namespace WebEMSim
{
public class Storage
{
public static Storage Shared = new Storage();
private Dictionary<string, Tournament> tournamentStore = new Dictionary<string, Tournament>();
public Tournament? CreateTournament()
{
Tournament tournament = new();
string id = tournament.ID;
bool success = tournamentStore.TryAdd(id, tournament);
if (!success) return null;
return tournament;
}
public Tournament? GetTournament(string id)
{
Tournament? tournament = null;
bool success = tournamentStore.TryGetValue(id, out tournament);
if(!success)
return null;
return tournament;
}
public List<string> ListTournaments()
{
return [.. tournamentStore.Keys];
}
public bool? DeleteTournament(string id)
{
Tournament? tournament = null;
bool found = tournamentStore.TryGetValue(id, out tournament);
if (!found)
return null;
return tournamentStore.Remove(id);
}
}
}

13
WeatherForecast.cs Normal file
View file

@ -0,0 +1,13 @@
namespace WebEMSim
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

16
WebEMSim.csproj Normal file
View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>

6
WebEMSim.http Normal file
View file

@ -0,0 +1,6 @@
@WebEMSim_HostAddress = http://localhost:5295
GET {{WebEMSim_HostAddress}}/weatherforecast/
Accept: application/json

25
WebEMSim.sln Normal file
View file

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.34928.147
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebEMSim", "WebEMSim.csproj", "{501B090D-2706-4748-88C4-8B43E9395823}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{501B090D-2706-4748-88C4-8B43E9395823}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{501B090D-2706-4748-88C4-8B43E9395823}.Debug|Any CPU.Build.0 = Debug|Any CPU
{501B090D-2706-4748-88C4-8B43E9395823}.Release|Any CPU.ActiveCfg = Release|Any CPU
{501B090D-2706-4748-88C4-8B43E9395823}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A16A0DAF-4ACA-4E0A-A212-3568C68D55E0}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

21
webem-ui/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
webem-ui/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

4
webem-ui/.prettierignore Normal file
View file

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
webem-ui/.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
webem-ui/README.md Normal file
View file

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

1894
webem-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
webem-ui/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "webem-ui",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.0.0-next.1",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"@fontsource-variable/montserrat": "^5.0.19",
"@sveltejs/adapter-static": "^3.0.2",
"lucide-svelte": "^0.395.0"
}
}

13
webem-ui/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
webem-ui/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { EventHandler } from 'svelte/elements';
import { LoaderPinwheel } from 'lucide-svelte';
let {
onclick,
children,
loading = $bindable(false)
}: {
onclick: EventHandler<MouseEvent>;
children: Snippet<[]>;
loading: boolean;
} = $props();
</script>
<div class="loaderButton" class:loading>
<button {onclick} disabled={loading}>
{@render children()}
</button>
<div class="loader">
<LoaderPinwheel />
</div>
</div>
<style>
.loaderButton {
position: relative;
border: thin solid currentColor;
border-radius: var(--radius);
button {
font: inherit;
color: inherit;
border: none;
background-color: transparent;
padding: calc(var(--padding) / 2) var(--padding);
cursor: pointer;
transition: filter 0.25s;
}
.loader {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
opacity: 0;
transition: opacity 0.25s;
& > :global(svg) {
animation-name: loaderButtonAnimation;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
width: 45px;
height: 45px;
stroke-width: 1px;
}
}
&.loading {
button {
filter: blur(2px);
}
.loader {
opacity: 1;
pointer-events: auto;
}
}
}
@keyframes loaderButtonAnimation {
0% {
rotate: 0deg;
}
100% {
rotate: 360deg;
}
}
</style>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import type { Match } from '$lib/tournamentApi';
import TeamComponent from './TeamComponent.svelte';
let {
match = $bindable(),
matchId = $bindable()
}: {
match: Match;
matchId: string;
} = $props();
</script>
<div class="match">
<span class="matchId">{matchId}</span>
<TeamComponent team={match.teamA} score={match.goalsA} />
<TeamComponent team={match.teamB} score={match.goalsB} />
</div>
<style>
.match {
display: flex;
flex-direction: column;
border: thin solid currentColor;
padding: calc(var(--padding) / 2);
gap: var(--gap);
border-radius: var(--radius);
.matchId {
text-align: center;
font-weight: 900;
}
}
</style>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { createTournament } from '$lib/tournamentApi';
import LoaderButton from './LoaderButton.svelte';
let loading = $state(false);
const startSimulation = async () => {
loading = true;
const tournament = await createTournament();
if (tournament == null) {
alert('something went wrong - fuck');
loading = false;
return;
}
loading = false;
goto(`/tournament/${tournament.id}`);
};
</script>
<div class="centerCta">
<LoaderButton onclick={startSimulation} bind:loading>Start a new tournament!</LoaderButton>
</div>
<style>
.centerCta {
padding: var(--padding);
display: flex;
justify-content: center;
}
</style>

View file

@ -0,0 +1,76 @@
<script lang="ts">
import { numberToLetter } from '$lib';
import type { Table, Team } from '$lib/tournamentApi';
let {
table = $bindable()
}: {
table: Table;
} = $props();
</script>
<div class="table">
<table>
<thead>
<tr>
<th>Rank</th>
<th>Group</th>
<th>Team</th>
<th>Points</th>
<th>Wins</th>
<th>Losses</th>
<th>Draws</th>
<th>For</th>
<th>Against</th>
<th>Goal Diff</th>
<th>World Rank</th>
</tr>
</thead>
<tbody>
{#each Object.keys(table.rankings).map((rank) => {
return { rank, team: table.rankings[rank as unknown as number] };
}) as entry}
<tr>
<td>{entry.rank}</td>
<td>{numberToLetter(entry.team.group)}</td>
<td>{entry.team.name}</td>
<td>{entry.team.points}</td>
<td>{entry.team.wins}</td>
<td>{entry.team.losses}</td>
<td>{entry.team.draws}</td>
<td>{entry.team.for}</td>
<td>{entry.team.against}</td>
<td>{entry.team.goalDelta}</td>
<td>{entry.team.worldRank}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<style>
table {
width: 100%;
th,
td {
text-align: center;
padding: 5px;
}
border-collapse: collapse;
th {
border-bottom: thin solid currentColor;
}
tr:nth-child(2n) {
background-color: color-mix(in display-p3, 20% var(--color-fg), var(--color-bg));
}
}
.table {
padding: var(--padding);
overflow: auto;
}
</style>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import type { Team } from '$lib/tournamentApi';
let {
team = $bindable(),
score = $bindable()
}: {
team: Team;
score?: number;
} = $props();
</script>
<div class="team">
<img src={team.flagUrl} alt={team.name} />
<span class="name">{team.name}</span>
{#if score != null}
<span class="score">{score}</span>
{/if}
</div>
<style>
.team {
display: flex;
align-items: center;
gap: var(--gap);
img {
width: 35px;
height: 35px;
object-fit: cover;
object-position: center;
border-radius: 100%;
}
.name {
font-weight: 500;
}
.score {
margin-left: auto;
}
}
</style>

View file

@ -0,0 +1,3 @@
export const numberToLetter = (number: number) => {
return (number + 10).toString(36).toUpperCase();
};

View file

@ -0,0 +1,23 @@
type Round = {
name: string;
matches: string[];
};
export const rounds: Round[] = [
{
name: 'Round of 16',
matches: ['W39', 'W37', 'W41', 'W42', 'W43', 'W44', 'W40', 'W38']
},
{
name: 'Quarter-Finals',
matches: ['W45', 'W46', 'W47', 'W48']
},
{
name: 'Semi-Finals',
matches: ['W49', 'W50']
},
{
name: 'Final',
matches: ['WIN']
}
];

View file

@ -0,0 +1,86 @@
type Tournament = {
id: string;
hasPlayed: boolean;
matches: Matches;
groups: Group[];
overallTable: Table;
};
type Matches = {
[matchId: string]: Match;
};
export const MATCH_RESULT_TEAM_A_WIN = 0,
MATCH_RESULT_TEAM_B_WIN = 1,
MATCH_RESULT_DRAW = 2;
export type Match = {
result:
| typeof MATCH_RESULT_TEAM_A_WIN
| typeof MATCH_RESULT_TEAM_B_WIN
| typeof MATCH_RESULT_DRAW;
winner: Team;
teamA: Team;
teamB: Team;
goalsA: number;
goalsB: number;
};
type Group = {
table: Table;
letter: string;
};
export type Table = {
rankings: {
[rank: number]: Team;
};
};
export type Team = {
name: string;
flagUrl: string;
group: number;
for: number;
points: number;
goalDelta: number;
wins: number;
losses: number;
against: number;
draws: number;
worldRank: number;
};
export const getTournament = async (id: string) => {
const response = await fetch(`/api/Tournament/${id}`);
if (!response.ok) return null;
const tournament = (await response.json()) as Tournament;
return tournament;
};
export const createTournament = async () => {
const response = await fetch(`/api/Tournament`, {
method: 'POST'
});
if (!response.ok) return null;
const tournament = (await response.json()) as Tournament;
return tournament;
};
export const deleteTournament = async (id: string) => {
const response = await fetch(`/api/Tournament/${id}`, {
method: 'DELETE'
});
if (!response.ok) return null;
const success = (await response.json()) as boolean;
return success;
};

View file

@ -0,0 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message || 'Something went wrong'}</h1>

View file

@ -0,0 +1,73 @@
<script lang="ts">
import type { LayoutData } from './$types';
import './global.css';
export let data: LayoutData;
</script>
<div class="layout">
<div class="navigation">
<nav>
<a href="/">WebEM Sim ❤</a>
</nav>
</div>
<div class="main">
<slot />
</div>
<footer>
<span> Made with ❤ by Lynn and Kevin </span>
</footer>
</div>
<style>
.navigation {
nav {
justify-content: center;
display: flex;
a {
padding: var(--padding);
font-weight: 900;
font-size: 2em;
text-decoration: none;
}
}
}
.layout {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main {
flex-grow: 1;
}
.main,
.navigation,
footer {
display: grid;
max-width: 100vw;
overflow: hidden;
grid-template-columns:
[full-start] minmax(max(4vmin, var(--gap)), auto)
[wide-start] minmax(auto, 240px)
[main-start] min(var(--width), calc(100% - max(8vmin, calc(var(--gap) * 2))))
[main-end] minmax(auto, 240px)
[wide-end] minmax(max(4vmin, var(--gap)), auto)
[full-end];
& > :global(*) {
grid-column: main-start/main-end;
min-width: 0;
}
}
footer {
padding: var(--padding);
text-align: center;
}
</style>

View file

@ -0,0 +1,7 @@
import type { LayoutLoad } from './$types';
export const load = (async () => {
return {};
}) satisfies LayoutLoad;
export const prerender = true;

View file

@ -0,0 +1,28 @@
<script>
import { goto } from '$app/navigation';
import LoaderButton from '$lib/components/LoaderButton.svelte';
import StartTournamentButton from '$lib/components/StartTournamentButton.svelte';
import { createTournament } from '$lib/tournamentApi';
</script>
<section>
<h1>WebEM Sim</h1>
<p>
Welcome to the best EM simulator you will ever see. I'm serious, this is the pinnacle of EM
simulation. Actually come to think of it... this is actually the best application ever written
overall.
</p>
<p>
Having just now come to the realization I've written the best piece of software ever to be
written, I'm feeling proud and accomplished. IT is over, it's finished, you can go home now.
</p>
<p>
Anyways, how about you start your simulation instead of reading the B.S. I've written here to
make the page feel less like my soul (empty).
</p>
<StartTournamentButton />
</section>

View file

@ -0,0 +1,49 @@
/* montserrat-latin-wght-normal */
@font-face {
font-family: 'Montserrat Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/montserrat/files/montserrat-latin-wght-normal.woff2)
format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}
:root {
--color-a: color(display-p3 0.86237 0.99601 0.87596);
--color-b: color(display-p3 0.01112 0.55902 0.74888);
--color-fg: var(--color-a);
--color-bg: var(--color-b);
--padding: 30px;
--gap: 20px;
--radius: 20px;
--width: 700px;
}
* {
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: var(--color-fg) color-mix(in display-p3, 20% var(--color-fg), var(--color-bg));
}
html,
body {
margin: 0;
padding: 0;
min-height: 100vh;
}
body {
background-color: var(--color-bg);
color: var(--color-fg);
font-family: 'Montserrat Variable', sans-serif;
}
a {
color: inherit;
text-decoration: underline dotted currentColor;
}

View file

@ -0,0 +1,143 @@
<script lang="ts">
import MatchComponent from '$lib/components/MatchComponent.svelte';
import StartTournamentButton from '$lib/components/StartTournamentButton.svelte';
import TableOutlet from '$lib/components/TableOutlet.svelte';
import TeamComponent from '$lib/components/TeamComponent.svelte';
import { rounds } from '$lib/parameters';
import type { PageData } from './$types';
let activeRound = $state(0);
let activeGroupTable = $state<number | 'overall'>(0);
const { data }: { data: PageData } = $props();
</script>
<section>
<div class="winner">
<h2>Your Winner:</h2>
<TeamComponent team={data.tournament.matches['WIN'].winner} />
</div>
</section>
<section>
<h2>Rounds</h2>
<div class="tabSwitcher">
{#each rounds as round, roundIdx}
<button
onclick={() => {
activeRound = roundIdx;
}}
class:active={activeRound == roundIdx}
>
{round.name}
</button>
{/each}
</div>
{#each rounds as round, roundIdx}
<div class="round" class:active={activeRound == roundIdx}>
{#each round.matches as match}
<MatchComponent match={data.tournament.matches[match]} matchId={match} />
{/each}
</div>
{/each}
</section>
<section class="tableSection">
<div class="title">
<h2>Tables</h2>
</div>
<div class="tabSwitcher">
<button
onclick={() => {
activeGroupTable = 'overall';
}}
class:active={activeGroupTable == 'overall'}
>
Overall
</button>
{#each data.tournament.groups as groupTable, groupTableIdx}
<button
onclick={() => {
activeGroupTable = groupTableIdx;
}}
class:active={activeGroupTable == groupTableIdx}
>
Group {groupTable.letter}
</button>
{/each}
</div>
<TableOutlet
table={activeGroupTable == 'overall'
? data.tournament.overallTable
: data.tournament.groups[activeGroupTable].table}
/>
</section>
<section>
<h2>One more time?</h2>
<p>
I guess you just can't get enough of this awesome website - that's fine, I know it's hard to say
goodbye to perfection. What do you think? Just one more match - for old time's sake?
</p>
<StartTournamentButton />
</section>
<style>
section {
margin: 20px 0;
}
.tableSection {
grid-column: wide-start/wide-end;
}
.winner {
display: flex;
gap: var(--gap);
}
.title {
margin: 0 auto;
max-width: var(--width);
}
.tabSwitcher {
display: flex;
justify-content: center;
white-space: nowrap;
overflow: auto;
width: 100%;
button {
padding: calc(var(--padding) / 2) var(--padding);
background-color: transparent;
border: none;
font: inherit;
color: inherit;
cursor: pointer;
border-bottom: 2px solid color-mix(in display-p3, var(--color-fg) 40%, var(--color-bg));
transition: border-bottom 0.25s;
&.active {
border-bottom: 2px solid var(--color-fg);
}
}
}
.round {
display: none;
flex-direction: column;
gap: var(--gap);
padding-top: var(--gap);
&.active {
display: flex;
}
}
</style>

View file

@ -0,0 +1,22 @@
import { getTournament } from '$lib/tournamentApi';
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const { id } = params;
const tournament = await getTournament(id);
if (!tournament) {
error(404, {
message: 'That tournament does not exist'
});
}
return {
tournament
};
}) satisfies PageLoad;
export const ssr = false;
export const prerender = false;

BIN
webem-ui/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

20
webem-ui/svelte.config.js Normal file
View file

@ -0,0 +1,20 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
fallback: '404.html'
})
}
};
export default config;

19
webem-ui/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

11
webem-ui/vite.config.ts Normal file
View file

@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
proxy: {
'/api': 'http://localhost:32768'
}
}
});