diff --git a/src/Wuzlstats/Controllers/PlayersController.cs b/src/Wuzlstats/Controllers/PlayersController.cs new file mode 100644 index 0000000..38690cc --- /dev/null +++ b/src/Wuzlstats/Controllers/PlayersController.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Microsoft.AspNet.Mvc; +using Wuzlstats.Models; +using Wuzlstats.ViewModels.Players; +using System; +using System.Threading.Tasks; +using Wuzlstats.Services; + +namespace Wuzlstats.Controllers +{ + public class PlayersController : Controller + { + private readonly Db _db; + private readonly AppSettings _settings; + private readonly PlayersService _statisticsService; + + public PlayersController(Db db, AppSettings settings) + { + _settings = settings; + _db = db; + _statisticsService = new PlayersService(_db); + } + + [Route("~/League/{league}/Players")] + public async Task Index(string league, string sort, bool recent) + { + var leagueEntity = _db.Leagues.FirstOrDefault(x => x.Name.ToLower() == league.ToLower()); + if (leagueEntity == null) + { + return RedirectToAction("Index", "Leagues"); + } + ViewBag.CurrentLeague = leagueEntity.Name; + var players = await _statisticsService.FindPlayersOfLeague(leagueEntity.Id, recent ?_settings.DaysForStatistics : default(int?)); + + switch (sort) + { + case "best": + players = players.OrderByDescending(x => x.Rank); + break; + case "worst": + players = players.OrderBy(x => x.Rank); + break; + case "activity": + players = players.OrderByDescending(x => x.Wins + x.Losses); + break; + default: + break; + } + + return View(new IndexViewModel + { + ActiveFilter = sort, + Recent = recent, + Players = players.Select(player => new PlayerViewModel + { + PlayerId = player.Id, + Name = player.Name, + Image = player.Image == null || player.Image.Length <= 0 ? EmptyAvatar.Base64 : Convert.ToBase64String(player.Image), + Wins = player.Wins, + Losses = player.Losses, + SingleGames = player.SingleGames, + TeamGames = player.TeamGames, + LastGamePlayedOn = player.LatestGame + }) + }); + } + + } +} diff --git a/src/Wuzlstats/Services/PlayersService.cs b/src/Wuzlstats/Services/PlayersService.cs new file mode 100644 index 0000000..e736c74 --- /dev/null +++ b/src/Wuzlstats/Services/PlayersService.cs @@ -0,0 +1,119 @@ +using Microsoft.Data.Entity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Wuzlstats.Models; + +namespace Wuzlstats.Services +{ + public class PlayersService + { + private readonly Db _db; + + public PlayersService(Db db) + { + _db = db; + } + + public async Task> FindPlayersOfLeague(int leagueId, int? daysForStatistics) + { + var gamesQuery = _db.Games.AsNoTracking().Where(x => x.LeagueId == leagueId); + if (daysForStatistics.HasValue) + { + var date = DateTime.UtcNow.Date.AddDays(-daysForStatistics.Value); + gamesQuery = gamesQuery.Where(x => x.Date >= date); + } + + // EF7 beta4 does not support navigation properties in queries yet + // this complicates the code a lot, because we need joins :( + + var players = new List(); + + 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 + select new + { + position.Position, + Player = player + }).ToListAsync(); + // player stats + foreach (var position in positions) + { + var player = players.FirstOrDefault(x => x.Equals(position.Player)); + if (player == null) + { + player = PlayerDto.Create(position.Player); + players.Add(player); + } + // calculate count of single or team games + if (position.Position == PlayerPositionTypes.Blue || position.Position == PlayerPositionTypes.Red) + { + player.SingleGames++; + } + else + { + player.TeamGames++; + } + + // don't count ties + if (game.BlueWins && (position.Position == PlayerPositionTypes.Blue || position.Position == PlayerPositionTypes.BlueDefense || position.Position == PlayerPositionTypes.BlueOffense)) + { + player.Wins++; + } + else if (game.RedWins && (position.Position == PlayerPositionTypes.Red || position.Position == PlayerPositionTypes.RedDefense || position.Position == PlayerPositionTypes.RedOffense)) + { + player.Wins++; + } + else if (game.RedWins && + (position.Position == PlayerPositionTypes.Blue || position.Position == PlayerPositionTypes.BlueDefense || position.Position == PlayerPositionTypes.BlueOffense)) + { + player.Losses++; + } + else if (game.BlueWins && + (position.Position == PlayerPositionTypes.Red || position.Position == PlayerPositionTypes.RedDefense || position.Position == PlayerPositionTypes.RedOffense)) + { + player.Losses++; + } + if (game.Date > player.LatestGame) + { + player.LatestGame = game.Date; + } + } + } + return players.OrderByDescending(x => x.LatestGame); + } + } + + public class PlayerDto + { + public int Id { get; set; } + public string Name { get; set; } + public byte[] Image { get; set; } + public int Wins { get; set; } + public int Losses { get; set; } + public int SingleGames { get; set; } + public int TeamGames { get; set; } + public DateTime LatestGame { get; set; } + + public double Rank => Losses == 0 ? Wins : (Wins == 0 ? 0.1d / Losses : (double)Wins / Losses); + + public bool Equals(Player p) + { + return Id == p.Id; + } + + public static PlayerDto Create(Player p) + { + return new PlayerDto + { + Id = p.Id, + Name = p.Name, + Image = p.Image + }; + } + } +} diff --git a/src/Wuzlstats/ViewModels/Players/IndexViewModel.cs b/src/Wuzlstats/ViewModels/Players/IndexViewModel.cs new file mode 100644 index 0000000..d843f84 --- /dev/null +++ b/src/Wuzlstats/ViewModels/Players/IndexViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Wuzlstats.ViewModels.Players +{ + public class IndexViewModel + { + public string ActiveFilter { get; set; } + public bool Recent { get; set; } + public IEnumerable Players { get; set; } + } +} diff --git a/src/Wuzlstats/ViewModels/Players/PlayerViewModel.cs b/src/Wuzlstats/ViewModels/Players/PlayerViewModel.cs new file mode 100644 index 0000000..891680e --- /dev/null +++ b/src/Wuzlstats/ViewModels/Players/PlayerViewModel.cs @@ -0,0 +1,16 @@ +using System; + +namespace Wuzlstats.ViewModels.Players +{ + public class PlayerViewModel + { + public int PlayerId { get; set; } + public string Name { get; set; } + public string Image { get; set; } + public int Wins { get; set; } + public int Losses { get; set; } + public DateTime LastGamePlayedOn { get; set; } + public int SingleGames { get; set; } + public int TeamGames { get; set; } + } +} diff --git a/src/Wuzlstats/Views/Home/Index.cshtml b/src/Wuzlstats/Views/Home/Index.cshtml index 8f0c221..9acb4f6 100644 --- a/src/Wuzlstats/Views/Home/Index.cshtml +++ b/src/Wuzlstats/Views/Home/Index.cshtml @@ -31,7 +31,7 @@
-

The best players

+

The best players

  • @@ -39,7 +39,7 @@
-

The worst players

+

The worst players

  • diff --git a/src/Wuzlstats/Views/Players/Index.cshtml b/src/Wuzlstats/Views/Players/Index.cshtml new file mode 100644 index 0000000..c0a4e7f --- /dev/null +++ b/src/Wuzlstats/Views/Players/Index.cshtml @@ -0,0 +1,55 @@ +@model Wuzlstats.ViewModels.Players.IndexViewModel + +

    These are all the players. Ever.
    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.

    + + + + + + + + + + + + + + + + @{var rank = 1; } + @foreach (var player in Model.Players) + { + + + + + + + + + } + +
    PlayerScoreLast game# 1 vs. 1# 2 vs. 2
    + @(rank++) + + + @player.Name + +
    @player.Name
    +
    +
    + @player.Wins + @player.Losses +
    +
    + @player.LastGamePlayedOn.ToShortDateString() + + @player.SingleGames + + @player.TeamGames +
    diff --git a/src/Wuzlstats/wwwroot/css/site.css b/src/Wuzlstats/wwwroot/css/site.css index 1354425..f558cc7 100644 --- a/src/Wuzlstats/wwwroot/css/site.css +++ b/src/Wuzlstats/wwwroot/css/site.css @@ -135,3 +135,16 @@ footer { @media (max-width: 768px) { footer { margin-top: 50px; } } + +.filter li.active { + font-weight: bold; } + +.players td > a:focus, .players td > a:hover { + text-decoration: none; } + +.players .player-name { + font-weight: bold; + display: inline; } + +.players .player-score { + color: #999; } diff --git a/src/Wuzlstats/wwwroot/css/site.scss b/src/Wuzlstats/wwwroot/css/site.scss index e2672d8..965078e 100644 --- a/src/Wuzlstats/wwwroot/css/site.scss +++ b/src/Wuzlstats/wwwroot/css/site.scss @@ -204,3 +204,27 @@ footer { margin-top: 50px; } } + +//Players page +.filter { + li.active { + font-weight: bold; + } +} + +.players { + td > a { + &:focus, &:hover { + text-decoration: none; + } + } + + .player-name { + font-weight: bold; + display: inline; + } + + .player-score { + color: #999; + } +}