-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from afuersch/teams-ranking
Close #5 Teams ranking.
- Loading branch information
Showing
11 changed files
with
348 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Wuzlstats.Models; | ||
using Wuzlstats.Services; | ||
using Wuzlstats.ViewModels.Teams; | ||
|
||
namespace Wuzlstats.Controllers | ||
{ | ||
public class TeamsController : Controller | ||
{ | ||
private readonly Db _db; | ||
private readonly AppSettings _settings; | ||
private readonly TeamStatisticsService _statisticsService; | ||
|
||
public TeamsController(Db db, AppSettings settings) | ||
{ | ||
_settings = settings; | ||
_db = db; | ||
_statisticsService = new TeamStatisticsService(_db); | ||
} | ||
|
||
[Route("~/League/{league}/Teams")] | ||
public async Task<IActionResult> Index(string league, string sort, bool recent) | ||
{ | ||
var leagueEntity = _db.Leagues.FirstOrDefault(x => x.Name.ToLowerInvariant() == league.ToLowerInvariant()); | ||
if (leagueEntity == null) | ||
{ | ||
return RedirectToAction("Index", "Leagues"); | ||
} | ||
ViewBag.CurrentLeague = leagueEntity.Name; | ||
var teams = await _statisticsService.FindTeamsOfLeagueAsync(leagueEntity.Id, recent ? _settings.DaysForStatistics : default(int?)); | ||
|
||
switch (sort) | ||
{ | ||
case "best": | ||
teams = teams.OrderByDescending(x => x.Rank); | ||
break; | ||
case "worst": | ||
teams = teams.OrderBy(x => x.Rank); | ||
break; | ||
case "activity": | ||
teams = teams.OrderByDescending(x => x.Wins + x.Losses); | ||
break; | ||
default: | ||
teams = teams.OrderByDescending(x => x.LastGamePlayedOn); | ||
break; | ||
} | ||
|
||
return View(new IndexViewModel | ||
{ | ||
ActiveFilter = sort, | ||
Recent = recent, | ||
Days = _settings.DaysForStatistics, | ||
Teams = teams | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.EntityFrameworkCore; | ||
using Wuzlstats.Models; | ||
using Wuzlstats.ViewModels.Teams; | ||
|
||
namespace Wuzlstats.Services | ||
{ | ||
public class TeamStatisticsService | ||
{ | ||
private readonly Db _db; | ||
|
||
public TeamStatisticsService(Db db) | ||
{ | ||
_db = db; | ||
} | ||
|
||
public async Task<IEnumerable<TeamViewModel>> FindTeamsOfLeagueAsync(int leagueId, int? daysForStatistics) | ||
{ | ||
var gamesQuery = FetchGames(leagueId, daysForStatistics); | ||
|
||
var teams = new List<TeamViewModel>(); | ||
|
||
var players2 = await _db.Players.Where(x => x.LeagueId == leagueId).ToListAsync(); | ||
|
||
var players = players2.Select(player => new PlayerViewModel | ||
{ | ||
Id = player.Id, | ||
Name = player.Name, | ||
Image = player.Image == null || player.Image.Length <= 0 ? EmptyAvatar.Base64 : Convert.ToBase64String(player.Image) | ||
}).ToList(); | ||
|
||
foreach (var game in await gamesQuery.ToListAsync()) | ||
{ | ||
var positions = await (from position in _db.PlayerPositions.AsNoTracking() | ||
join player in _db.Players.AsNoTracking() on position.PlayerId equals player.Id | ||
where position.GameId == game.Id | ||
where position.Position != PlayerPositionTypes.Blue || position.Position != PlayerPositionTypes.Red | ||
select new | ||
{ | ||
position.Position, | ||
Player = player | ||
}).ToListAsync(); | ||
|
||
// team stats | ||
if (positions.Count(x => x.Position == PlayerPositionTypes.BlueDefense) == 1 | ||
&& positions.Count(x => x.Position == PlayerPositionTypes.BlueOffense) == 1 | ||
&& positions.Count(x => x.Position == PlayerPositionTypes.RedDefense) == 1 | ||
&& positions.Count(x => x.Position == PlayerPositionTypes.RedOffense) == 1) | ||
{ | ||
var redOffense = players.Single(x => x.Id == positions.Single(y => y.Position == PlayerPositionTypes.RedOffense).Player.Id); | ||
var redDefense = players.Single(x => x.Id == positions.Single(y => y.Position == PlayerPositionTypes.RedDefense).Player.Id); | ||
var blueOffense = players.Single(x => x.Id == positions.Single(y => y.Position == PlayerPositionTypes.BlueOffense).Player.Id); | ||
var blueDefense = players.Single(x => x.Id == positions.Single(y => y.Position == PlayerPositionTypes.BlueDefense).Player.Id); | ||
|
||
var redTeam = teams.FirstOrDefault(x => x.Equals(redOffense, redDefense)); | ||
if (redTeam == null) | ||
{ | ||
redTeam = CreateTeam(redOffense, redDefense); | ||
teams.Add(redTeam); | ||
} | ||
var blueTeam = teams.FirstOrDefault(x => x.Equals(blueOffense, blueDefense)); | ||
if (blueTeam == null) | ||
{ | ||
blueTeam = CreateTeam(blueOffense, blueDefense); | ||
teams.Add(blueTeam); | ||
} | ||
|
||
if (game.BlueWins) | ||
{ | ||
redTeam.Losses++; | ||
blueTeam.Wins++; | ||
} | ||
else if (game.RedWins) | ||
{ | ||
redTeam.Wins++; | ||
blueTeam.Losses++; | ||
} | ||
//Resolve date of last played game | ||
ResolveLastGamePlayedOn(redTeam, game.Date); | ||
ResolveLastGamePlayedOn(blueTeam, game.Date); | ||
} | ||
} | ||
|
||
return teams; | ||
} | ||
|
||
|
||
private void ResolveLastGamePlayedOn(TeamViewModel team, DateTime date) | ||
{ | ||
if (team.LastGamePlayedOn < date) | ||
{ | ||
team.LastGamePlayedOn = date; | ||
} | ||
} | ||
|
||
public static TeamViewModel CreateTeam(PlayerViewModel p1, PlayerViewModel p2) | ||
{ | ||
if (p1.Id <= p2.Id) | ||
{ | ||
return new TeamViewModel | ||
{ | ||
Player1 = p1, | ||
Player2 = p2 | ||
}; | ||
} | ||
return new TeamViewModel | ||
{ | ||
Player1 = p2, | ||
Player2 = p1 | ||
}; | ||
} | ||
|
||
private IQueryable<Game> FetchGames(int leagueId, int? daysForStatistics) | ||
{ | ||
var gamesQuery = _db.Games.AsNoTracking().Where(x => x.LeagueId == leagueId); | ||
if (!daysForStatistics.HasValue) | ||
{ | ||
return gamesQuery; | ||
} | ||
var date = DateTime.UtcNow.Date.AddDays(-daysForStatistics.Value); | ||
return gamesQuery.Where(x => x.Date >= date); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Wuzlstats.ViewModels.Teams | ||
{ | ||
public class IndexViewModel | ||
{ | ||
public string ActiveFilter { get; set; } | ||
public bool Recent { get; set; } | ||
public int Days { get; set; } | ||
public IEnumerable<TeamViewModel> Teams { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System; | ||
|
||
namespace Wuzlstats.ViewModels.Teams | ||
{ | ||
public class PlayerViewModel | ||
{ | ||
public int Id { get; set; } | ||
public string Name { get; set; } | ||
public string Image { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using System; | ||
|
||
namespace Wuzlstats.ViewModels.Teams | ||
{ | ||
public class TeamViewModel | ||
{ | ||
public PlayerViewModel Player1 { get; set; } | ||
public PlayerViewModel Player2 { get; set; } | ||
public int Wins { get; set; } | ||
public int Losses { get; set; } | ||
public DateTime LastGamePlayedOn { get; set; } | ||
public int GamesCount => Wins + Losses; | ||
|
||
// ReSharper disable once PossibleLossOfFraction | ||
public double Rank => Losses == 0 ? Wins : (Wins == 0 ? 0.1d / Losses : (double)Wins / Losses); | ||
|
||
public bool Equals(PlayerViewModel p1, PlayerViewModel p2) | ||
{ | ||
if (Player1 == null || Player2 == null) | ||
{ | ||
return false; | ||
} | ||
if (p1.Id <= p2.Id) | ||
{ | ||
return Player1.Id == p1.Id && Player2.Id == p2.Id; | ||
} | ||
return Player2.Id == p1.Id && Player1.Id == p2.Id; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
@model Wuzlstats.ViewModels.Teams.IndexViewModel | ||
|
||
@if (Model.Recent) | ||
{ | ||
<p class="help-block"> | ||
These are the teams of the last @Model.Days days. All of them.<br />This is pretty sweet, but it gets better: There's also | ||
<a href="@Url.Action("Index", "Teams", new { sort = Model.ActiveFilter, recent = false })">this very same statistic, but for all of the time</a>. All of it. Holy crap! | ||
</p> | ||
} | ||
else | ||
{ | ||
<p class="help-block"> | ||
These are all the teams. Ever.<br />No one will be forgotten. Shame and fame will last till the very end of days, or at least until a database crash with no backups. <br /> | ||
There's the same statistic, <a href="@Url.Action("Index", "Teams", new { sort = Model.ActiveFilter, recent = true })">but only for the last @Model.Days days</a>, too. | ||
</p> | ||
} | ||
|
||
<div class="input-group"> | ||
Order By | ||
<div class="btn-group"> | ||
<a href="@Url.Action("Index", "Teams", new { recent = Model.Recent })" class="btn btn-default btn-sm @(string.IsNullOrEmpty(Model.ActiveFilter) ? "active" : "")">Last played</a> | ||
<a href="@Url.Action("Index", "Teams", new { sort = "best", recent = Model.Recent })" class="btn btn-default btn-sm @(Model.ActiveFilter == "best" ? "active" : "")">Best</a> | ||
<a href="@Url.Action("Index", "Teams", new { sort = "worst", recent = Model.Recent })" class="btn btn-default btn-sm @(Model.ActiveFilter == "worst" ? "active" : "")">Worst</a> | ||
<a href="@Url.Action("Index", "Teams", new { sort = "activity", recent = Model.Recent })" class="btn btn-default btn-sm @(Model.ActiveFilter == "activity" ? "active" : "")">Most active</a> | ||
</div> | ||
</div> | ||
|
||
<table class="table table-striped ranking teams"> | ||
<thead> | ||
<tr> | ||
<th></th> | ||
<th>Team</th> | ||
<th>Score</th> | ||
<th>Last game</th> | ||
<th>Count</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
@{var rank = 1; } | ||
@foreach (var team in Model.Teams) | ||
{ | ||
<tr> | ||
<td> | ||
@(rank++) | ||
</td> | ||
<td class="team"> | ||
@Html.Partial("_Player", team.Player1) | ||
@Html.Partial("_Player", team.Player2) | ||
</td> | ||
<td> | ||
<div class="player-score"> | ||
<span class="ranking-wins">@team.Wins<span class="glyphicon glyphicon-thumbs-up"></span></span> | ||
<span class="ranking-losses">@team.Losses<span class="glyphicon glyphicon-thumbs-down"></span></span> | ||
</div> | ||
</td> | ||
<td> | ||
@team.LastGamePlayedOn.ToShortDateString() | ||
</td> | ||
<td> | ||
@team.GamesCount | ||
</td> | ||
</tr> | ||
} | ||
</tbody> | ||
</table> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
@model Wuzlstats.ViewModels.Teams.PlayerViewModel | ||
|
||
<div class="player"> | ||
<a href="@Url.Action("Index", "Player", new { id = Model.Id })"> | ||
<img src="data:image/png;base64,@Model.Image" alt="@Model.Name" class="ranking-image" /> | ||
</a> | ||
<div class="player-name">@Model.Name</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.