Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 0 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +0,0 @@
# Docs

```mermaid
graph TD
subgraph "Typical.Console (Presentation Layer)"
direction LR
A[Program.cs] --> B[GameRunner/UI Loop];
B --> C[MarkupGenerator];
C --> D[Spectre.Console];
B --> E[Typical.Core];
end

subgraph "Typical.Core (Business Logic Layer)"
direction LR
E[TyperGame Engine] --> H(ITextProvider Interface);
end

subgraph "Typical.DataAccess (Data Access Layer)"
direction LR
I[FileTextProvider] --> H;
end
```

---

```mermaid
sequenceDiagram
participant Program as Program.cs
participant Engine as TyperGame (Core)
participant Runner as ConsoleGameRunner
participant Spectre as Spectre.Console

Program->>Engine: new TyperGame(provider, options)
Program->>Engine: StartNewGame()
Program->>Runner: new ConsoleGameRunner(Engine)
Program->>Runner: Run()
Runner->>Spectre: Live(...).Start()
loop Game Loop
Runner->>Engine: ProcessKeystroke()
Runner->>Engine: GetStats()
Runner->>Spectre: ctx.Refresh()
end
Spectre-->>Runner: Loop Ends
Runner-->>Program: Run() Completes
Program->>Spectre: AnsiConsole.MarkupLine("Game Over")
```
4 changes: 4 additions & 0 deletions src/Typical.Core/CharacterStats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Typical.Core;

// A simple record to hold the results of GetCharacterStats
public record CharacterStats(int Correct, int Incorrect, int Extra);
93 changes: 93 additions & 0 deletions src/Typical.Core/GameStats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Diagnostics;

namespace Typical.Core;

public class GameStats(TimeProvider? timeProvider = null)
{
private readonly KeystrokeHistory _keystrokeHistory = [];
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
private long? _startTimestamp;
private long? _endTimestamp;
private bool _statsAreDirty = true; // Start dirty
private double _cachedWpm;
private double _cachedAccuracy;
private CharacterStats _cachedChars = new(0, 0, 0);
public double WordsPerMinute
{
get
{
if (_statsAreDirty)
RecalculateAllStats();
return _cachedWpm;
}
}

public double Accuracy
{
get
{
if (_statsAreDirty)
RecalculateAllStats();
return _cachedAccuracy;
}
}

public CharacterStats Chars
{
get
{
if (_statsAreDirty)
RecalculateAllStats();
return _cachedChars;
}
}
public bool IsRunning => _startTimestamp.HasValue && !_endTimestamp.HasValue;
public TimeSpan ElapsedTime =>
_timeProvider.GetElapsedTime(
_startTimestamp ?? 0,
_endTimestamp ?? _timeProvider.GetTimestamp()
);

public void Start()
{
Reset();
_startTimestamp = _timeProvider.GetTimestamp();
}

public void Reset()
{
_startTimestamp = null;
_endTimestamp = null;
_keystrokeHistory.Clear();
_cachedWpm = 0;
_cachedAccuracy = 100;
_cachedChars = new CharacterStats(0, 0, 0);
}

public void Stop()
{
if (IsRunning)
{
_endTimestamp = _timeProvider.GetTimestamp();
}
}

private void RecalculateAllStats()
{
_cachedWpm = _keystrokeHistory.CalculateWpm(ElapsedTime);
_cachedAccuracy = _keystrokeHistory.CalculateAccuracy();
_cachedChars = _keystrokeHistory.GetCharacterStats();

_statsAreDirty = false; // The stats are now fresh
}

internal void LogKeystroke(char keyChar, KeystrokeType extra)
{
if (!IsRunning)
{
Start();
}
_keystrokeHistory.Add(new KeystrokeLog(keyChar, extra, _timeProvider.GetTimestamp()));
_statsAreDirty = true; // Mark stats as dirty
}
}
77 changes: 77 additions & 0 deletions src/Typical.Core/KeystrokeHistory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Collections;

namespace Typical.Core;

public class KeystrokeHistory : IEnumerable<KeystrokeLog>
{
private readonly List<KeystrokeLog> _logs = new();

public int Count => _logs.Count;
public int CorrectCount => _logs.Count(log => log.Type == KeystrokeType.Correct);
public int IncorrectCount => _logs.Count(log => log.Type == KeystrokeType.Incorrect);
public int ExtraCount => _logs.Count(log => log.Type == KeystrokeType.Extra);

private (int Correct, int Incorrect, int Extra) GetCounts()
{
int correct = 0;
int incorrect = 0;
int extra = 0;

foreach (var log in _logs)
{
switch (log.Type)
{
case KeystrokeType.Correct:
correct++;
break;
case KeystrokeType.Incorrect:
incorrect++;
break;

case KeystrokeType.Extra:
extra++;
break;
}
}
return (correct, incorrect, extra);
}

public void Add(KeystrokeLog log)
{
_logs.Add(log);
}

public void Clear()
{
_logs.Clear();
}

public double CalculateWpm(TimeSpan duration) =>
duration.TotalMinutes == 0
? 0
: _logs.Count(log => log.Type == KeystrokeType.Correct) / 5.0 / duration.TotalMinutes;

public double CalculateAccuracy()
{
if (Count == 0)
return 100.0;

var (correct, incorrect, _) = GetCounts();
int totalChars = correct + incorrect;
return totalChars == 0 ? 100.0 : (double)correct / totalChars * 100.0;
}

public CharacterStats GetCharacterStats()
{
var counts = GetCounts();
return new CharacterStats(
Correct: counts.Correct,
Incorrect: counts.Incorrect,
Extra: counts.Extra
);
}

public IEnumerator<KeystrokeLog> GetEnumerator() => _logs.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
3 changes: 3 additions & 0 deletions src/Typical.Core/KeystrokeLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Typical.Core;

public record struct KeystrokeLog(char Character, KeystrokeType Type, long Timestamp);
8 changes: 8 additions & 0 deletions src/Typical.Core/KeystrokeType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Typical.Core;

public enum KeystrokeType
{
Correct,
Incorrect,
Extra,
}
3 changes: 3 additions & 0 deletions src/Typical.Core/Typical.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
<DebugType>embedded</DebugType>
<MinVerDefaultPreReleaseIdentifiers>preview</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Typical.Tests" />
</ItemGroup>
</Project>
34 changes: 24 additions & 10 deletions src/Typical.Core/TypicalGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ public class TypicalGame
private readonly StringBuilder _userInput;
private readonly ITextProvider _textProvider;
private readonly GameOptions _gameOptions;
private string _targetText = string.Empty;

public TypicalGame(ITextProvider textProvider)
: this(textProvider, new GameOptions()) { }
Expand All @@ -17,18 +16,22 @@ public TypicalGame(ITextProvider textProvider, GameOptions gameOptions)
_textProvider = textProvider ?? throw new ArgumentNullException(nameof(textProvider));
_gameOptions = gameOptions;
_userInput = new StringBuilder();
Stats = new GameStats(); // IT CREATES ITS OWN STATS OBJECT
}

public string TargetText => _targetText;
public string TargetText { get; private set; } = string.Empty;
public string UserInput => _userInput.ToString();
public bool IsOver { get; private set; }
public bool IsRunning => !IsOver && Stats.IsRunning;
public int TargetFrameDelayMilliseconds => 1000 / _gameOptions.TargetFrameRate;
public GameStats Stats { get; }

public bool ProcessKeyPress(ConsoleKeyInfo key)
{
if (key.Key == ConsoleKey.Escape)
{
IsOver = true;
Stats.Stop();
return false;
}

Expand All @@ -38,31 +41,42 @@ public bool ProcessKeyPress(ConsoleKeyInfo key)
}
else if (!char.IsControl(key.KeyChar))
{
if (_gameOptions.ForbidIncorrectEntries)
int currentPos = _userInput.Length;
if (currentPos >= TargetText.Length)
{
int currentPos = _userInput.Length;
if (currentPos < _targetText.Length && key.KeyChar == _targetText[currentPos])
{
_userInput.Append(key.KeyChar);
}
Stats.LogKeystroke(key.KeyChar, KeystrokeType.Extra);
}
else if (key.KeyChar == TargetText[currentPos])
{
Stats.LogKeystroke(key.KeyChar, KeystrokeType.Correct);
}
else
{
Stats.LogKeystroke(key.KeyChar, KeystrokeType.Incorrect);
}

if (
!_gameOptions.ForbidIncorrectEntries
|| (currentPos < TargetText.Length && key.KeyChar == TargetText[currentPos])
)
{
_userInput.Append(key.KeyChar);
}
}

if (_userInput.ToString() == _targetText)
if (_userInput.ToString() == TargetText)
{
IsOver = true;
Stats.Stop();
}

return true;
}

public async Task StartNewGame()
{
_targetText = await _textProvider.GetTextAsync();
TargetText = await _textProvider.GetTextAsync();
Stats.Start();
_userInput.Clear();
IsOver = false;
}
Expand Down
Loading